From 927f8ec51de59332d838ab7448728c30e423aab5 Mon Sep 17 00:00:00 2001 From: shellrow Date: Tue, 17 Feb 2026 23:57:44 +0900 Subject: [PATCH 01/21] refactor: update scan pipeline, upgrade deps --- Cargo.lock | 2 +- Cargo.toml | 4 +- src/capture/pcap.rs | 6 +- src/cli/host.rs | 19 ++-- src/cli/mod.rs | 63 ++++++++++--- src/cli/ping.rs | 6 +- src/cli/port.rs | 16 ++-- src/cmd/common.rs | 72 +++++++++++++++ src/cmd/domain.rs | 16 +++- src/cmd/host.rs | 69 +++++++------- src/cmd/interface.rs | 4 +- src/cmd/mod.rs | 9 +- src/cmd/nei.rs | 36 ++++---- src/cmd/ping.rs | 29 +++--- src/cmd/port.rs | 157 +++++++++++++++++++------------- src/cmd/trace.rs | 29 +++--- src/db/mod.rs | 112 +++++++++++++++++------ src/db/os.rs | 19 ++-- src/db/oui.rs | 2 +- src/db/service.rs | 69 +++++++++----- src/db/tls.rs | 19 +++- src/dns/mod.rs | 33 ++++--- src/dns/probe.rs | 39 +++++--- src/dns/resolver.rs | 10 ++- src/endpoint.rs | 106 +++++++++++++++++----- src/interface.rs | 2 +- src/log.rs | 23 ++--- src/main.rs | 44 ++++----- src/nei/arp.rs | 29 +++--- src/nei/mod.rs | 7 +- src/nei/ndp.rs | 32 ++++--- src/os/mod.rs | 9 +- src/os/probe/tcp.rs | 51 ++++++----- src/output/domain.rs | 9 +- src/output/host.rs | 8 +- src/output/interface.rs | 4 +- src/output/mod.rs | 19 ++-- src/output/nei.rs | 17 +++- src/output/ping.rs | 30 +++++-- src/output/port.rs | 85 +++++++++++++----- src/output/progress.rs | 5 +- src/output/trace.rs | 48 ++++++---- src/packet/arp.rs | 6 +- src/packet/icmp.rs | 6 +- src/packet/mod.rs | 6 +- src/packet/ndp.rs | 6 +- src/packet/tcp.rs | 8 +- src/packet/udp.rs | 25 +++--- src/ping/mod.rs | 33 ++++--- src/ping/pinger.rs | 9 +- src/ping/probe/icmp.rs | 63 ++++++++----- src/ping/probe/tcp.rs | 80 +++++++++++------ src/ping/probe/udp.rs | 76 ++++++++++------ src/ping/result.rs | 15 +++- src/ping/setting.rs | 22 ++--- src/protocol.rs | 10 +-- src/scan/mod.rs | 21 ++++- src/scan/probe/icmp.rs | 51 +++++------ src/scan/probe/mod.rs | 4 +- src/scan/probe/quic.rs | 168 +++++++++++++++++++---------------- src/scan/probe/tcp.rs | 150 ++++++++++++++----------------- src/scan/probe/udp.rs | 59 ++++++------ src/service/mod.rs | 86 ++++++++++-------- src/service/payload.rs | 23 +++-- src/service/probe/dns.rs | 117 +++++++++++++++++------- src/service/probe/generic.rs | 37 +++++--- src/service/probe/http.rs | 149 ++++++++++++++++++++++--------- src/service/probe/mod.rs | 20 +++-- src/service/probe/null.rs | 42 ++++++--- src/service/probe/quic.rs | 108 +++++++++++++++------- src/service/probe/tls.rs | 55 ++++++++---- src/time.rs | 4 +- src/trace/mod.rs | 6 +- src/trace/probe/udp.rs | 80 +++++++++++------ src/util/json.rs | 4 +- 75 files changed, 1854 insertions(+), 1063 deletions(-) create mode 100644 src/cmd/common.rs diff --git a/Cargo.lock b/Cargo.lock index a81fa9a..69e41dc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1359,7 +1359,7 @@ dependencies = [ "nex", "num_cpus", "quinn", - "rand 0.8.5", + "rand 0.9.2", "regex", "rustls", "rustls-native-certs 0.7.3", diff --git a/Cargo.toml b/Cargo.toml index a90cf14..4644445 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/shellrow/nrev" homepage = "https://github.com/shellrow/nrev" documentation = "https://github.com/shellrow/nrev" readme = "README.md" -keywords = ["network"] +keywords = ["network","security","scan","cli","cross-platform"] categories = ["network-programming"] license = "MIT" @@ -33,7 +33,7 @@ hickory-proto = "0.25" hickory-resolver = { version = "0.25" } chrono = { version = "0.4", features = ["serde"] } uuid = { version = "1.3", features = ["v4","v5","fast-rng","macro-diagnostics"] } -rand = "0.8" +rand = "0.9" clap = { version = "4.5", features = ["derive", "cargo"] } indicatif = { version = "0.18" } tracing-indicatif = "0.3" diff --git a/src/capture/pcap.rs b/src/capture/pcap.rs index f527bc1..15ad5b1 100644 --- a/src/capture/pcap.rs +++ b/src/capture/pcap.rs @@ -1,4 +1,5 @@ -use std::net::IpAddr; +use crate::interface; +use futures::stream::StreamExt; use nex::datalink::async_io::AsyncRawReceiver; use nex::net::interface::Interface; use nex::packet::frame::Frame; @@ -6,11 +7,10 @@ use nex::packet::frame::ParseOption; use nex::packet::{ethernet::EtherType, ip::IpNextProtocol}; use serde::{Deserialize, Serialize}; use std::collections::HashSet; +use std::net::IpAddr; use std::time::Duration; use std::time::Instant; -use futures::stream::StreamExt; use tokio::sync::oneshot; -use crate::interface; /// Packet capture options #[derive(Serialize, Deserialize, Debug, Clone)] diff --git a/src/cli/host.rs b/src/cli/host.rs index 23285d8..8488daa 100644 --- a/src/cli/host.rs +++ b/src/cli/host.rs @@ -1,8 +1,8 @@ -use anyhow::{Result, Context}; -use std::{net::IpAddr, path::Path}; +use crate::endpoint::Host; +use anyhow::{Context, Result}; use ipnet::IpNet; use std::fs; -use crate::endpoint::Host; +use std::{net::IpAddr, path::Path}; /// Resolve one target specification line (CIDR / IP / hostname) async fn expand_one_target(t: &str) -> Result> { @@ -24,7 +24,10 @@ async fn expand_one_target(t: &str) -> Result> { } // Hostname - let ips = resolver.lookup_ip(t).await.with_context(|| format!("resolve {t}"))?; + let ips = resolver + .lookup_ip(t) + .await + .with_context(|| format!("resolve {t}"))?; for ip in ips { out.push(Host::with_hostname(ip, t.to_string())); } @@ -42,7 +45,9 @@ async fn expand_file(path: &Path) -> Result> { for line in text.lines() { let s = line.trim(); - if s.is_empty() || s.starts_with('#') { continue; } // Skip empty lines/comments + if s.is_empty() || s.starts_with('#') { + continue; + } // Skip empty lines/comments nested_inputs.push(s.to_string()); } @@ -61,7 +66,9 @@ pub async fn parse_target_hosts(inputs: &[String]) -> Result> { for raw in inputs { let s = raw.trim(); - if s.is_empty() { continue; } + if s.is_empty() { + continue; + } // 1. Check if it's a file (with '@' hint or existing file path) let (is_file_hint, path_str) = if let Some(stripped) = s.strip_prefix('@') { diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 085d4c0..55233d5 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,12 +1,16 @@ -pub mod port; pub mod host; pub mod ping; +pub mod port; use std::path::PathBuf; -use clap::{command, value_parser, ArgAction, Args, Parser, Subcommand, ValueEnum}; +use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum, value_parser}; -use crate::{config::default::{DEFAULT_BASE_TARGET_UDP_PORT, DEFAULT_PORTS_CONCURRENCY}, endpoint::TransportProtocol, protocol::Protocol}; +use crate::{ + config::default::{DEFAULT_BASE_TARGET_UDP_PORT, DEFAULT_PORTS_CONCURRENCY}, + endpoint::TransportProtocol, + protocol::Protocol, +}; /// nrev - Fast Network Mapper #[derive(Parser, Debug)] @@ -91,11 +95,45 @@ pub enum Command { /// Port scan methods. Default: Connect #[derive(Copy, Clone, Debug, ValueEnum, Eq, PartialEq)] -pub enum PortScanMethod { Connect, Syn } +pub enum PortScanMethod { + Connect, + Syn, +} + +/// Port scan transport. +#[derive(Copy, Clone, Debug, ValueEnum, Eq, PartialEq)] +pub enum PortScanTransport { + Tcp, + Udp, + Quic, +} + +impl PortScanTransport { + /// Convert to TransportProtocol. + pub fn to_transport(self) -> TransportProtocol { + match self { + PortScanTransport::Tcp => TransportProtocol::Tcp, + PortScanTransport::Udp => TransportProtocol::Udp, + PortScanTransport::Quic => TransportProtocol::Quic, + } + } + /// Convert to lowercase name. + pub fn as_str(self) -> &'static str { + match self { + PortScanTransport::Tcp => "tcp", + PortScanTransport::Udp => "udp", + PortScanTransport::Quic => "quic", + } + } +} /// Host scan protocols. Default: ICMP #[derive(Copy, Clone, Debug, ValueEnum, Eq, PartialEq)] -pub enum HostScanProto { Icmp, Udp, Tcp } +pub enum HostScanProto { + Icmp, + Udp, + Tcp, +} impl HostScanProto { /// Convert to TransportProtocol (if applicable) @@ -118,7 +156,9 @@ impl HostScanProto { /// Traceroute protocol (currently only UDP is supported) #[derive(Copy, Clone, Debug, ValueEnum, Eq, PartialEq)] -pub enum TraceProto { Udp } +pub enum TraceProto { + Udp, +} impl TraceProto { /// Convert to &str @@ -146,9 +186,9 @@ pub struct PortScanArgs { #[arg(short, long, default_value = "top-1000")] pub ports: String, - /// Transport to scan (now tcp only; udp/quic later) - #[arg(long, default_value = "tcp", value_parser = ["tcp","udp","quic"])] - pub proto: String, + /// Transport to scan + #[arg(long, value_enum, default_value_t = PortScanTransport::Tcp)] + pub proto: PortScanTransport, /// Scanning method (default: connect) #[arg(long, value_enum, default_value_t = PortScanMethod::Connect)] @@ -183,7 +223,7 @@ pub struct PortScanArgs { #[arg(long, value_parser = value_parser!(u64).range(1..=10_000))] pub connect_timeout_ms: Option, - /// Read timeout in ms (auto-adapted by RTT) + /// Service probe timeout in ms (used with --service-detect) #[arg(long, value_parser = value_parser!(u64).range(1..=10_000))] pub read_timeout_ms: Option, @@ -272,7 +312,6 @@ pub struct PingArgs { pub interface: Option, } - /// Traceroute arguments #[derive(Args, Debug)] pub struct TraceArgs { @@ -313,7 +352,7 @@ pub struct NeighborArgs { pub target: String, /// Network interface name to bind - #[arg(short='i', long)] + #[arg(short = 'i', long)] pub interface: Option, /// Timeout waiting for replies (ms) diff --git a/src/cli/ping.rs b/src/cli/ping.rs index c7841a3..06ec442 100644 --- a/src/cli/ping.rs +++ b/src/cli/ping.rs @@ -1,7 +1,7 @@ use std::net::IpAddr; -use anyhow::{Result, Context}; use crate::endpoint::Host; +use anyhow::{Context, Result}; /// Parse a single target host (IP or hostname) pub async fn parse_target_host(host_str: &str) -> Result { @@ -9,7 +9,9 @@ pub async fn parse_target_host(host_str: &str) -> Result { match host_str.parse::() { Ok(ip) => Ok(Host::new(ip)), Err(_) => { - let ips = resolver.lookup_ip(host_str).await + let ips = resolver + .lookup_ip(host_str) + .await .with_context(|| format!("resolve {host_str}"))?; // If multiple IPs are returned, use the first one (ips: LookupIp) for ip in ips { diff --git a/src/cli/port.rs b/src/cli/port.rs index 24f6db7..fde8ff6 100644 --- a/src/cli/port.rs +++ b/src/cli/port.rs @@ -1,6 +1,6 @@ +use crate::endpoint::{Port, TransportProtocol}; use anyhow::{Result, bail}; use std::collections::BTreeSet; -use crate::endpoint::{Port, TransportProtocol}; /// Get top N ports from the default port list fn top_ports(n: usize) -> Vec { @@ -14,14 +14,20 @@ pub fn parse_ports(spec: &str, tr: TransportProtocol) -> Result> { if let Some(nstr) = spec.strip_prefix("top-") { let n: usize = nstr.parse()?; - for p in top_ports(n) { set.insert(Port::new(p, tr)); } + for p in top_ports(n) { + set.insert(Port::new(p, tr)); + } } else { for part in spec.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()) { - if let Some((a,b)) = part.split_once('-') { + if let Some((a, b)) = part.split_once('-') { let start: u16 = a.parse()?; let end: u16 = b.parse()?; - if start > end { bail!("invalid range: {part}"); } - for p in start..=end { set.insert(Port::new(p, tr)); } + if start > end { + bail!("invalid range: {part}"); + } + for p in start..=end { + set.insert(Port::new(p, tr)); + } } else { let p: u16 = part.parse()?; set.insert(Port::new(p, tr)); diff --git a/src/cmd/common.rs b/src/cmd/common.rs new file mode 100644 index 0000000..5dc430f --- /dev/null +++ b/src/cmd/common.rs @@ -0,0 +1,72 @@ +use std::collections::BTreeMap; +use std::time::Duration; + +use anyhow::Result; +use netdev::Interface; + +use crate::endpoint::{Endpoint, Host, Port}; + +/// Resolve the interface from CLI option. Falls back to default interface. +pub fn resolve_interface(interface_name: Option<&str>) -> Result { + if let Some(name) = interface_name { + return crate::interface::get_interface_by_name(name.to_string()) + .ok_or_else(|| anyhow::anyhow!("interface not found: {}", name)); + } + + netdev::get_default_interface() + .map_err(|e| anyhow::anyhow!("failed to get default interface: {}", e)) +} + +/// Build endpoints by applying the same port list to each host. +pub fn build_endpoints(hosts: Vec, ports: &[Port]) -> Vec { + hosts + .into_iter() + .map(|host| { + let mut endpoint = Endpoint::new(host.ip); + endpoint.hostname = host.hostname; + for port in ports { + endpoint.upsert_port(*port); + } + endpoint + }) + .collect() +} + +/// Merge duplicate endpoints by IP and de-duplicate ports. +pub fn merge_endpoints(endpoints: Vec) -> Vec { + let mut by_ip: BTreeMap = BTreeMap::new(); + for endpoint in endpoints { + by_ip + .entry(endpoint.ip) + .and_modify(|existing| existing.merge(endpoint.clone())) + .or_insert(endpoint); + } + by_ip.into_values().collect() +} + +/// Ensure concurrency is always at least 1. +pub fn normalize_concurrency(v: usize) -> usize { + v.max(1) +} + +/// Derive connect timeout from initial RTT unless explicitly configured. +pub fn derive_connect_timeout(initial_rtt: Duration, override_ms: Option) -> Duration { + match override_ms { + Some(ms) => Duration::from_millis(ms), + None => { + let adapted = (initial_rtt.as_millis() as f64 * 1.5) as u64; + Duration::from_millis(adapted.clamp(50, 5000)) + } + } +} + +/// Derive wait time from initial RTT unless explicitly configured. +pub fn derive_wait_time(initial_rtt: Duration, override_ms: Option) -> Duration { + match override_ms { + Some(ms) => Duration::from_millis(ms), + None => { + let adapted = (initial_rtt.as_millis() as f64 * 2.0) as u64; + Duration::from_millis(adapted.clamp(100, 5000)) + } + } +} diff --git a/src/cmd/domain.rs b/src/cmd/domain.rs index cf6edbb..6b2fe32 100644 --- a/src/cmd/domain.rs +++ b/src/cmd/domain.rs @@ -1,7 +1,11 @@ use std::{path::PathBuf, time::Duration}; +use crate::cmd::common::normalize_concurrency; +use crate::{ + cli::DomainScanArgs, + util::json::{JsonStyle, save_json_output}, +}; use anyhow::Result; -use crate::{cli::DomainScanArgs, util::json::{save_json_output, JsonStyle}}; /// Run subdomain scan pub async fn run(args: DomainScanArgs, no_stdout: bool, output: Option) -> Result<()> { @@ -11,13 +15,17 @@ pub async fn run(args: DomainScanArgs, no_stdout: bool, output: Option) base_domain: base.name.clone(), word_list: if let Some(wl_path) = args.wordlist { let content = std::fs::read_to_string(wl_path)?; - content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect() + content + .lines() + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect() } else { crate::db::domain::get_subdomain_wordlist() }, timeout: Duration::from_millis(args.timeout_ms), resolve_timeout: resolve_timeout, - concurrent_limit: args.concurrency, + concurrent_limit: normalize_concurrency(args.concurrency), }; let scanner = crate::dns::probe::DomainScanner::new(settings); let result = scanner.run().await?; @@ -30,7 +38,7 @@ pub async fn run(args: DomainScanArgs, no_stdout: bool, output: Option) if !no_stdout { tracing::info!("JSON output saved to {}", path.display()); } - }, + } Err(e) => tracing::error!("Failed to save JSON output: {}", e), } } diff --git a/src/cmd/host.rs b/src/cmd/host.rs index e0001ec..326b94a 100644 --- a/src/cmd/host.rs +++ b/src/cmd/host.rs @@ -1,14 +1,22 @@ -use std::{path::PathBuf, time::Duration}; -use rand::seq::SliceRandom; -use rand::thread_rng; -use anyhow::Result; -use crate::{cli::{HostScanArgs, HostScanProto}, endpoint::{Endpoint, Host, Port, TransportProtocol}, output::ScanResult, scan::HostScanner, util::json::{save_json_output, JsonStyle}}; +use crate::cmd::common::{build_endpoints, normalize_concurrency, resolve_interface}; use crate::probe::ProbeSetting; +use crate::{ + cli::{HostScanArgs, HostScanProto}, + endpoint::{Host, Port, TransportProtocol}, + output::ScanResult, + scan::HostScanner, + util::json::{JsonStyle, save_json_output}, +}; +use anyhow::Result; +use rand::seq::SliceRandom; +use std::{path::PathBuf, time::Duration}; /// Run host scan pub async fn run(args: HostScanArgs, no_stdout: bool, output: Option) -> Result<()> { let mut target_hosts: Vec = crate::cli::host::parse_target_hosts(&args.target).await?; - if target_hosts.is_empty() { anyhow::bail!("no targets resolved"); } + if target_hosts.is_empty() { + anyhow::bail!("no targets resolved"); + } let mut ports: Vec = Vec::new(); match args.proto { @@ -20,38 +28,20 @@ pub async fn run(args: HostScanArgs, no_stdout: bool, output: Option) - if !args.ordered { // Randomize the order of targets and ports - target_hosts.shuffle(&mut thread_rng()); - ports.shuffle(&mut thread_rng()); + let mut rng = rand::rng(); + target_hosts.shuffle(&mut rng); + ports.shuffle(&mut rng); } - let mut target_endpoints: Vec = Vec::new(); - - for host in target_hosts { - let mut endpoint = Endpoint::new(host.ip); - endpoint.hostname = host.hostname; - for port in &ports { - endpoint.upsert_port(port.clone()); - } - target_endpoints.push(endpoint); - } - - let interface: netdev::Interface = if let Some(if_name) = args.interface { - match crate::interface::get_interface_by_name(if_name.to_string()) { - Some(iface) => iface, - None => anyhow::bail!("interface not found"), - } - } else { - match netdev::get_default_interface() { - Ok(iface) => iface, - Err(_) => anyhow::bail!("failed to get default interface"), - } - }; + let target_endpoints = build_endpoints(target_hosts, &ports); + let interface = resolve_interface(args.interface.as_deref())?; + let concurrency = normalize_concurrency(args.concurrency); let probe_setting = ProbeSetting { if_index: interface.index, target_endpoints: target_endpoints, - host_concurrency: args.concurrency, - port_concurrency: args.concurrency, + host_concurrency: concurrency, + port_concurrency: concurrency, task_timeout: Duration::from_secs(30), connect_timeout: Duration::from_millis(args.timeout_ms), wait_time: Duration::from_millis(args.wait_ms), @@ -60,11 +50,20 @@ pub async fn run(args: HostScanArgs, no_stdout: bool, output: Option) - let host_scanner = HostScanner::new(probe_setting.clone(), args.proto); if !probe_setting.target_endpoints.is_empty() { - tracing::info!("Starting {} host scan. Target: {} host(s), {} port(s)", args.proto.as_str().to_uppercase(), probe_setting.target_endpoints.len(), probe_setting.target_endpoints[0].ports.len()); + tracing::info!( + "Starting {} host scan. Target: {} host(s), {} port(s)", + args.proto.as_str().to_uppercase(), + probe_setting.target_endpoints.len(), + probe_setting.target_endpoints[0].ports.len() + ); } let mut hostscan_result: ScanResult = host_scanner.run().await?; hostscan_result.sort_endpoints(); - tracing::info!("{} Host scan completed in {:?}", args.proto.as_str().to_uppercase(), hostscan_result.scan_time); + tracing::info!( + "{} Host scan completed in {:?}", + args.proto.as_str().to_uppercase(), + hostscan_result.scan_time + ); // Print result as a tree if !no_stdout { @@ -76,7 +75,7 @@ pub async fn run(args: HostScanArgs, no_stdout: bool, output: Option) - if !no_stdout { tracing::info!("JSON output saved to {}", path.display()); } - }, + } Err(e) => tracing::error!("Failed to save JSON output: {}", e), } } diff --git a/src/cmd/interface.rs b/src/cmd/interface.rs index 17d8f8a..0dc0ab1 100644 --- a/src/cmd/interface.rs +++ b/src/cmd/interface.rs @@ -1,6 +1,6 @@ -use netdev::Interface; -use anyhow::Result; use crate::cli::InterfaceArgs; +use anyhow::Result; +use netdev::Interface; /// Show network interfaces pub fn show(args: &InterfaceArgs) -> Result<()> { diff --git a/src/cmd/mod.rs b/src/cmd/mod.rs index cef52d8..960bd43 100644 --- a/src/cmd/mod.rs +++ b/src/cmd/mod.rs @@ -1,7 +1,8 @@ -pub mod port; +pub mod common; +pub mod domain; pub mod host; +pub mod interface; +pub mod nei; pub mod ping; +pub mod port; pub mod trace; -pub mod nei; -pub mod domain; -pub mod interface; diff --git a/src/cmd/nei.rs b/src/cmd/nei.rs index f28ea0e..b4d1a23 100644 --- a/src/cmd/nei.rs +++ b/src/cmd/nei.rs @@ -1,21 +1,17 @@ use std::{net::IpAddr, path::PathBuf, time::Duration}; -use crate::{cli::NeighborArgs, endpoint::Host, nei::NeighborDiscoveryResult, util::json::{save_json_output, JsonStyle}}; +use crate::cmd::common::resolve_interface; +use crate::{ + cli::NeighborArgs, + endpoint::Host, + nei::NeighborDiscoveryResult, + util::json::{JsonStyle, save_json_output}, +}; use anyhow::Result; /// Run neighbor discovery (ARP for IPv4, NDP for IPv6) pub async fn run(args: NeighborArgs, no_stdout: bool, output: Option) -> Result<()> { - let interface: netdev::Interface = if let Some(if_name) = args.interface { - match crate::interface::get_interface_by_name(if_name.to_string()) { - Some(iface) => iface, - None => anyhow::bail!("interface not found"), - } - } else { - match netdev::get_default_interface() { - Ok(iface) => iface, - Err(_) => anyhow::bail!("failed to get default interface"), - } - }; + let interface = resolve_interface(args.interface.as_deref())?; let dst_host: Host = crate::cli::ping::parse_target_host(&args.target).await?; let nd_result: NeighborDiscoveryResult = match dst_host.ip { IpAddr::V4(ipv4) => { @@ -24,11 +20,12 @@ pub async fn run(args: NeighborArgs, no_stdout: bool, output: Option) - match dst_host.hostname { Some(hostname) => { arp_result.hostname = Some(hostname); - }, + } None => { let timeout = Duration::from_millis(200); - arp_result.hostname = crate::dns::reverse_lookup(IpAddr::V4(ipv4), timeout).await; - }, + arp_result.hostname = + crate::dns::reverse_lookup(IpAddr::V4(ipv4), timeout).await; + } } arp_result } @@ -38,11 +35,12 @@ pub async fn run(args: NeighborArgs, no_stdout: bool, output: Option) - match dst_host.hostname { Some(hostname) => { ndp_result.hostname = Some(hostname); - }, + } None => { let timeout = Duration::from_millis(200); - ndp_result.hostname = crate::dns::reverse_lookup(IpAddr::V6(ipv6), timeout).await; - }, + ndp_result.hostname = + crate::dns::reverse_lookup(IpAddr::V6(ipv6), timeout).await; + } } ndp_result } @@ -56,7 +54,7 @@ pub async fn run(args: NeighborArgs, no_stdout: bool, output: Option) - if !no_stdout { tracing::info!("JSON output saved to {}", path.display()); } - }, + } Err(e) => tracing::error!("Failed to save JSON output: {}", e), } } diff --git a/src/cmd/ping.rs b/src/cmd/ping.rs index b0570e8..ba88da5 100644 --- a/src/cmd/ping.rs +++ b/src/cmd/ping.rs @@ -1,21 +1,18 @@ use std::{path::PathBuf, time::Duration}; -use crate::{cli::PingArgs, endpoint::Host, ping::{pinger::Pinger, setting::PingSetting}, protocol::Protocol, util::json::{save_json_output, JsonStyle}}; +use crate::cmd::common::resolve_interface; +use crate::{ + cli::PingArgs, + endpoint::Host, + ping::{pinger::Pinger, setting::PingSetting}, + protocol::Protocol, + util::json::{JsonStyle, save_json_output}, +}; use anyhow::Result; /// Run ping command pub async fn run(args: PingArgs, no_stdout: bool, output: Option) -> Result<()> { - let interface: netdev::Interface = if let Some(if_name) = args.interface { - match crate::interface::get_interface_by_name(if_name.to_string()) { - Some(iface) => iface, - None => anyhow::bail!("interface not found"), - } - } else { - match netdev::get_default_interface() { - Ok(iface) => iface, - Err(_) => anyhow::bail!("failed to get default interface"), - } - }; + let interface = resolve_interface(args.interface.as_deref())?; let dst_host: Host = crate::cli::ping::parse_target_host(&args.target).await?; let mut ping_setting: PingSetting = match args.proto { Protocol::Icmp => PingSetting::icmp_ping(&interface, dst_host, args.count)?, @@ -29,7 +26,11 @@ pub async fn run(args: PingArgs, no_stdout: bool, output: Option) -> Re ping_setting.receive_timeout = Duration::from_millis(args.timeout_ms); let pinger = Pinger::new(ping_setting); - tracing::info!("Pinging {} with {}...", args.target, args.proto.as_str().to_uppercase()); + tracing::info!( + "Pinging {} with {}...", + args.target, + args.proto.as_str().to_uppercase() + ); let ping_result = pinger.run().await?; if !no_stdout { crate::output::ping::print_ping_tree(&ping_result); @@ -40,7 +41,7 @@ pub async fn run(args: PingArgs, no_stdout: bool, output: Option) -> Re if !no_stdout { tracing::info!("JSON output saved to {}", path.display()); } - }, + } Err(e) => tracing::error!("Failed to save JSON output: {}", e), } } diff --git a/src/cmd/port.rs b/src/cmd/port.rs index 9697e69..6761aea 100644 --- a/src/cmd/port.rs +++ b/src/cmd/port.rs @@ -1,50 +1,52 @@ -use std::{path::PathBuf, time::Duration}; -use rand::seq::SliceRandom; -use rand::thread_rng; +use crate::cmd::common::{ + build_endpoints, derive_connect_timeout, derive_wait_time, merge_endpoints, + normalize_concurrency, resolve_interface, +}; +use crate::{ + cli::PortScanArgs, + endpoint::{Host, Port, PortState, TransportProtocol}, + output::{ + ScanResult, + port::{ScanReport, print_report_tree}, + }, + probe::ProbeSetting, + scan::PortScanner, + service::{ServiceDetector, ServiceProbeConfig}, + util::json::{JsonStyle, save_json_output}, +}; use anyhow::Result; -use crate::{cli::PortScanArgs, endpoint::{Endpoint, Host, Port, PortState, TransportProtocol}, output::{port::{print_report_tree, ScanReport}, ScanResult}, probe::ProbeSetting, scan::PortScanner, service::{ServiceDetector, ServiceProbeConfig}, util::json::{save_json_output, JsonStyle}}; +use rand::seq::SliceRandom; +use std::{path::PathBuf, time::Duration}; /// Run port scan pub async fn run(args: PortScanArgs, no_stdout: bool, output: Option) -> Result<()> { let mut rep = ScanReport::new(); // Parse target hosts let target_hosts: Vec = crate::cli::host::parse_target_hosts(&args.target).await?; - if target_hosts.is_empty() { anyhow::bail!("no targets resolved"); } + if target_hosts.is_empty() { + anyhow::bail!("no targets resolved"); + } let first_host = target_hosts[0].clone(); // Parse transport protocol - let transport: TransportProtocol = TransportProtocol::from_str(&args.proto).ok_or_else(|| anyhow::anyhow!("invalid transport"))?; + let transport: TransportProtocol = args.proto.to_transport(); // Parse ports let mut ports: Vec = crate::cli::port::parse_ports(&args.ports, transport)?; + if ports.is_empty() { + anyhow::bail!("no ports to scan"); + } if !args.ordered { // Randomize the order of ports - ports.shuffle(&mut thread_rng()); + let mut rng = rand::rng(); + ports.shuffle(&mut rng); } // Create target endpoints from hosts and ports - let mut target_endpoints: Vec = Vec::new(); - - for host in target_hosts { - let mut endpoint = Endpoint::new(host.ip); - endpoint.hostname = host.hostname; - for port in &ports { - endpoint.upsert_port(port.clone()); - } - target_endpoints.push(endpoint); - } + let target_endpoints = build_endpoints(target_hosts, &ports); // Get network interface - let interface: netdev::Interface = if let Some(if_name) = args.interface { - match crate::interface::get_interface_by_name(if_name.to_string()) { - Some(iface) => iface, - None => anyhow::bail!("interface not found"), - } - } else { - match netdev::get_default_interface() { - Ok(iface) => iface, - Err(_) => anyhow::bail!("failed to get default interface"), - } - }; + let interface = resolve_interface(args.interface.as_deref())?; + let concurrency = normalize_concurrency(args.concurrency); // Initial ping to check reachability and measure latency let initial_rtt = if args.no_ping { @@ -58,45 +60,39 @@ pub async fn run(args: PortScanArgs, no_stdout: bool, output: Option) - } } }; - - let conn_timeout = if let Some(ct) = args.connect_timeout_ms { - Duration::from_millis(ct) - } else { - // adapt timeout based on RTT - let adapted = (initial_rtt.as_millis() as f64 * 1.5) as u64; - Duration::from_millis(adapted.clamp(50, 5000)) - }; - let wait_time = if let Some(wt) = args.wait_ms { - Duration::from_millis(wt) - } else { - // adapt wait time based on RTT - let adapted = (initial_rtt.as_millis() as f64 * 2.0) as u64; - Duration::from_millis(adapted.clamp(100, 5000)) - }; + let conn_timeout = derive_connect_timeout(initial_rtt, args.connect_timeout_ms); + let wait_time = derive_wait_time(initial_rtt, args.wait_ms); // Create probe setting let probe_setting = ProbeSetting { if_index: interface.index, target_endpoints: target_endpoints, - host_concurrency: args.concurrency, - port_concurrency: args.concurrency, + host_concurrency: concurrency, + port_concurrency: concurrency, task_timeout: Duration::from_millis(args.task_timeout_ms), connect_timeout: conn_timeout, wait_time: wait_time, send_rate: Duration::from_millis(1), }; - let transport = TransportProtocol::from_str(&args.proto).unwrap(); - if !probe_setting.target_endpoints.is_empty() { - tracing::info!("Starting {} port scan on {} host(s), {} port(s)", args.proto.to_uppercase(), probe_setting.target_endpoints.len(), probe_setting.target_endpoints[0].ports.len()); + tracing::info!( + "Starting {} port scan on {} host(s), {} port(s)", + args.proto.as_str().to_uppercase(), + probe_setting.target_endpoints.len(), + probe_setting.target_endpoints[0].ports.len() + ); } - + // Run port scan let port_scanner = PortScanner::new(probe_setting.clone(), transport, args.method); let portscan_result: ScanResult = port_scanner.run().await?; - tracing::info!("{} Port scan completed in {:?}", args.proto.to_uppercase(), portscan_result.scan_time); + tracing::info!( + "{} Port scan completed in {:?}", + args.proto.as_str().to_uppercase(), + portscan_result.scan_time + ); let mut endpoint_results = portscan_result.endpoints.clone(); let mut active_endpoints = portscan_result.get_active_endpoints(); @@ -108,12 +104,14 @@ pub async fn run(args: PortScanArgs, no_stdout: bool, output: Option) - rep.apply_port_scan(portscan_result); if transport != TransportProtocol::Quic && args.quic { - let port_scanner = PortScanner::new(probe_setting.clone(), TransportProtocol::Quic, args.method); + let port_scanner = + PortScanner::new(probe_setting.clone(), TransportProtocol::Quic, args.method); let quic_portscan_result = port_scanner.run().await?; endpoint_results.extend(quic_portscan_result.endpoints.clone()); let active_quic_endpoints = quic_portscan_result.get_active_endpoints(); // Merge active QUIC endpoints with active TCP endpoints active_endpoints.extend(active_quic_endpoints); + active_endpoints = merge_endpoints(active_endpoints); rep.apply_port_scan(quic_portscan_result); } @@ -140,7 +138,10 @@ pub async fn run(args: PortScanArgs, no_stdout: bool, output: Option) - wait_time: probe_setting.wait_time, send_rate: probe_setting.send_rate, }; - tracing::info!("Starting OS detection on {} host(s)", os_probe_setting.target_endpoints.len()); + tracing::info!( + "Starting OS detection on {} host(s)", + os_probe_setting.target_endpoints.len() + ); let os_detector = crate::os::OsDetector::new(os_probe_setting); let os_probe_result = os_detector.run().await?; tracing::info!("OS detection completed in {:?}", os_probe_result.probe_time); @@ -153,29 +154,57 @@ pub async fn run(args: PortScanArgs, no_stdout: bool, output: Option) - rep.apply_os_probe(os_probe_result); } - + if args.service_detect { - // service detection + // service detection let service_probe_setting = ServiceProbeConfig { - timeout: Duration::from_secs(2), - max_concurrency: args.concurrency, + timeout: Duration::from_millis(args.read_timeout_ms.unwrap_or(2_000)), + max_concurrency: concurrency, max_read_size: 1024 * 1024, sni: true, skip_cert_verify: true, }; let service_detector = ServiceDetector::new(service_probe_setting); - if !active_endpoints.is_empty() { - tracing::info!("Starting service detection on {} host(s), {} port(s)", active_endpoints.len(), active_endpoints[0].ports.len()); + let service_targets = if let Some(sni_hostname) = &args.sni { + active_endpoints + .into_iter() + .map(|mut ep| { + ep.hostname = Some(sni_hostname.clone()); + ep + }) + .collect() + } else { + active_endpoints + }; + if !service_targets.is_empty() { + tracing::info!( + "Starting service detection on {} host(s), {} port(s)", + service_targets.len(), + service_targets[0].ports.len() + ); } - - let service_result = service_detector.run_service_detection(active_endpoints).await?; - tracing::info!("Service detection completed in {:?}", service_result.scan_time); + + let service_result = service_detector + .run_service_detection(service_targets) + .await?; + tracing::info!( + "Service detection completed in {:?}", + service_result.scan_time + ); service_result.results.iter().for_each(|result| { - tracing::debug!("[SERVICE] {}:{} {} {} {:?} {:?}", result.ip, result.port, result.transport.as_str().to_uppercase(), result.probe_id.as_str(), result.service_info.banner, result.service_info.cpes); + tracing::debug!( + "[SERVICE] {}:{} {} {} {:?} {:?}", + result.ip, + result.port, + result.transport.as_str().to_uppercase(), + result.probe_id.as_str(), + result.service_info.banner, + result.service_info.cpes + ); }); - + rep.apply_service_detection(service_result); } @@ -190,7 +219,7 @@ pub async fn run(args: PortScanArgs, no_stdout: bool, output: Option) - if !no_stdout { tracing::info!("JSON output saved to {}", path.display()); } - }, + } Err(e) => tracing::error!("Failed to save JSON output: {}", e), } } diff --git a/src/cmd/trace.rs b/src/cmd/trace.rs index c7a5940..17e6bb6 100644 --- a/src/cmd/trace.rs +++ b/src/cmd/trace.rs @@ -1,21 +1,18 @@ use std::{path::PathBuf, time::Duration}; -use crate::{cli::TraceArgs, endpoint::Host, protocol::Protocol, trace::{TraceSetting, Tracer}, util::json::{save_json_output, JsonStyle}}; +use crate::cmd::common::resolve_interface; +use crate::{ + cli::TraceArgs, + endpoint::Host, + protocol::Protocol, + trace::{TraceSetting, Tracer}, + util::json::{JsonStyle, save_json_output}, +}; use anyhow::Result; /// Run traceroute pub async fn run(args: TraceArgs, no_stdout: bool, output: Option) -> Result<()> { - let interface: netdev::Interface = if let Some(if_name) = args.interface { - match crate::interface::get_interface_by_name(if_name.to_string()) { - Some(iface) => iface, - None => anyhow::bail!("interface not found"), - } - } else { - match netdev::get_default_interface() { - Ok(iface) => iface, - Err(_) => anyhow::bail!("failed to get default interface"), - } - }; + let interface = resolve_interface(args.interface.as_deref())?; let dst_host: Host = crate::cli::ping::parse_target_host(&args.target).await?; let mut trace_setting: TraceSetting = match args.proto.to_protocol() { Protocol::Udp => TraceSetting::udp_trace(&interface, &dst_host)?, @@ -29,7 +26,11 @@ pub async fn run(args: TraceArgs, no_stdout: bool, output: Option) -> R trace_setting.receive_timeout = Duration::from_millis(args.timeout_ms); let tracer = Tracer::new(trace_setting); - tracing::info!("Trace route to {} with {}...", args.target, args.proto.as_str().to_uppercase()); + tracing::info!( + "Trace route to {} with {}...", + args.target, + args.proto.as_str().to_uppercase() + ); let trace_result = tracer.run().await?; tracing::info!("Trace complete."); if !no_stdout { @@ -41,7 +42,7 @@ pub async fn run(args: TraceArgs, no_stdout: bool, output: Option) -> R if !no_stdout { tracing::info!("JSON output saved to {}", path.display()); } - }, + } Err(e) => tracing::error!("Failed to save JSON output: {}", e), } } diff --git a/src/db/mod.rs b/src/db/mod.rs index 61ab9e3..606bd05 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,13 +1,13 @@ -pub mod service; +pub mod domain; pub mod os; +pub mod oui; pub mod port; +pub mod service; pub mod tls; -pub mod oui; -pub mod domain; -use std::time::{Duration, Instant}; -use futures::StreamExt; use anyhow::Result; +use futures::StreamExt; +use std::time::{Duration, Instant}; /// Initialization function type type InitFn = fn() -> Result<()>; @@ -18,14 +18,38 @@ struct DbTask { init: InitFn, } -const TASK_TCP_SERVICE: DbTask = DbTask { name: "tcp_service_db", init: || service::init_tcp_service_db() }; -const TASK_UDP_SERVICE: DbTask = DbTask { name: "udp_service_db", init: || service::init_udp_service_db() }; -const TASK_PORT_PROBE : DbTask = DbTask { name: "port_probe_db", init: || service::init_port_probe_db() }; -const TASK_SVC_PROBE : DbTask = DbTask { name: "service_probe_db", init: || service::init_service_probe_db() }; -const TASK_RESP_SIGS : DbTask = DbTask { name: "response_signatures", init: || service::init_response_signatures_db() }; -const TASK_TLS_OID : DbTask = DbTask { name: "tls_oid_map", init: || tls::init_tls_oid_map() }; -const TASK_OS_DB : DbTask = DbTask { name: "os_db", init: || os::init_os_db() }; -const TASK_OUI_DB : DbTask = DbTask { name: "oui_db", init: || oui::init_oui_db() }; +const TASK_TCP_SERVICE: DbTask = DbTask { + name: "tcp_service_db", + init: || service::init_tcp_service_db(), +}; +const TASK_UDP_SERVICE: DbTask = DbTask { + name: "udp_service_db", + init: || service::init_udp_service_db(), +}; +const TASK_PORT_PROBE: DbTask = DbTask { + name: "port_probe_db", + init: || service::init_port_probe_db(), +}; +const TASK_SVC_PROBE: DbTask = DbTask { + name: "service_probe_db", + init: || service::init_service_probe_db(), +}; +const TASK_RESP_SIGS: DbTask = DbTask { + name: "response_signatures", + init: || service::init_response_signatures_db(), +}; +const TASK_TLS_OID: DbTask = DbTask { + name: "tls_oid_map", + init: || tls::init_tls_oid_map(), +}; +const TASK_OS_DB: DbTask = DbTask { + name: "os_db", + init: || os::init_os_db(), +}; +const TASK_OUI_DB: DbTask = DbTask { + name: "oui_db", + init: || oui::init_oui_db(), +}; /// Database initialization result record #[derive(Debug, Clone)] @@ -50,23 +74,49 @@ pub struct DbInitializer { impl DbInitializer { /// Create new initializer - pub fn new() -> Self { Self { tasks: Vec::new() } } + pub fn new() -> Self { + Self { tasks: Vec::new() } + } /// Add TCP service DB - pub fn with_tcp_services(mut self) -> Self { self.tasks.push(&TASK_TCP_SERVICE); self } + pub fn with_tcp_services(mut self) -> Self { + self.tasks.push(&TASK_TCP_SERVICE); + self + } /// Add UDP service DB - pub fn with_udp_services(mut self) -> Self { self.tasks.push(&TASK_UDP_SERVICE); self } + pub fn with_udp_services(mut self) -> Self { + self.tasks.push(&TASK_UDP_SERVICE); + self + } /// Add port probe DB - pub fn with_port_probe(mut self) -> Self { self.tasks.push(&TASK_PORT_PROBE); self } + pub fn with_port_probe(mut self) -> Self { + self.tasks.push(&TASK_PORT_PROBE); + self + } /// Add service probe DB - pub fn with_service_probe(mut self)-> Self { self.tasks.push(&TASK_SVC_PROBE); self } + pub fn with_service_probe(mut self) -> Self { + self.tasks.push(&TASK_SVC_PROBE); + self + } /// Add response signatures DB - pub fn with_response_sigs(mut self)-> Self { self.tasks.push(&TASK_RESP_SIGS); self } + pub fn with_response_sigs(mut self) -> Self { + self.tasks.push(&TASK_RESP_SIGS); + self + } /// Add TLS OID map - pub fn with_tls_oids(mut self) -> Self { self.tasks.push(&TASK_TLS_OID); self } + pub fn with_tls_oids(mut self) -> Self { + self.tasks.push(&TASK_TLS_OID); + self + } /// Add OS DB - pub fn with_os_db(mut self) -> Self { self.tasks.push(&TASK_OS_DB); self } + pub fn with_os_db(mut self) -> Self { + self.tasks.push(&TASK_OS_DB); + self + } /// Add OUI DB - pub fn with_oui_db(mut self) -> Self { self.tasks.push(&TASK_OUI_DB); self } + pub fn with_oui_db(mut self) -> Self { + self.tasks.push(&TASK_OUI_DB); + self + } /// Add all databases pub fn with_all() -> Self { Self::new() @@ -99,9 +149,9 @@ impl DbInitializer { let results = stream::iter(uniq) .map(|task| async move { let start = Instant::now(); - let res = (task.init)(); - let ok = res.is_ok(); - let err = res.err().map(|e| e.to_string()); + let res = (task.init)(); + let ok = res.is_ok(); + let err = res.err().map(|e| e.to_string()); InitRecord { name: task.name, elapsed: start.elapsed(), @@ -119,11 +169,19 @@ impl DbInitializer { if r.ok { tracing::debug!("DB init ok: {} ({:?})", r.name, r.elapsed); } else { - tracing::error!("DB init failed: {} ({:?}) - {}", r.name, r.elapsed, r.error.as_deref().unwrap_or("?")); + tracing::error!( + "DB init failed: {} ({:?}) - {}", + r.name, + r.elapsed, + r.error.as_deref().unwrap_or("?") + ); } } tracing::debug!("DB init done: {:?} total", total); - InitReport { total, records: results } + InitReport { + total, + records: results, + } } } diff --git a/src/db/os.rs b/src/db/os.rs index ee0a34f..26c7945 100644 --- a/src/db/os.rs +++ b/src/db/os.rs @@ -1,13 +1,16 @@ -use std::{collections::HashMap, sync::OnceLock}; +use crate::{ + config, + os::{OsClass, OsClassTtl, OsDb}, +}; use anyhow::Result; -use crate::{config, os::{OsClass, OsClassTtl, OsDb}}; +use std::{collections::HashMap, sync::OnceLock}; pub static OS_DB: OnceLock = OnceLock::new(); /// Initialize OS database pub fn init_os_db() -> Result<()> { - let os_db: OsDb = serde_json::from_str(config::db::OS_DB_JSON) - .expect("Invalid nrev-os-db.json format"); + let os_db: OsDb = + serde_json::from_str(config::db::OS_DB_JSON).expect("Invalid nrev-os-db.json format"); OS_DB .set(os_db) .map_err(|_| anyhow::anyhow!("Failed to set OS_DB in OnceLock"))?; @@ -22,8 +25,8 @@ pub fn os_db() -> &'static OsDb { /// Get initial TTL to OS class mapping pub fn get_ttl_class_map() -> HashMap { let mut ttl_class_map: HashMap = HashMap::new(); - let ds_os_ttl: Vec = - serde_json::from_str(config::db::OS_CLASS_TTL_JSON).expect("Invalid os-class-ttl.json format"); + let ds_os_ttl: Vec = serde_json::from_str(config::db::OS_CLASS_TTL_JSON) + .expect("Invalid os-class-ttl.json format"); for os_ttl in ds_os_ttl { ttl_class_map.insert(os_ttl.initial_ttl, os_ttl.os_class.as_str().to_string()); } @@ -33,8 +36,8 @@ pub fn get_ttl_class_map() -> HashMap { /// Get initial TTL mapping pub fn get_class_ttl_map() -> HashMap { let mut class_ttl_map: HashMap = HashMap::new(); - let ds_os_ttl: Vec = - serde_json::from_str(config::db::OS_CLASS_TTL_JSON).expect("Invalid os-class-ttl.json format"); + let ds_os_ttl: Vec = serde_json::from_str(config::db::OS_CLASS_TTL_JSON) + .expect("Invalid os-class-ttl.json format"); for os_ttl in ds_os_ttl { class_ttl_map.insert(os_ttl.os_class, os_ttl.initial_ttl); } diff --git a/src/db/oui.rs b/src/db/oui.rs index c0f0ff4..a47a3a7 100644 --- a/src/db/oui.rs +++ b/src/db/oui.rs @@ -1,5 +1,5 @@ -use ndb_oui::OuiDb; use anyhow::Result; +use ndb_oui::OuiDb; use std::sync::OnceLock; pub static OUI_DB: OnceLock = OnceLock::new(); diff --git a/src/db/service.rs b/src/db/service.rs index 9c9621f..924748c 100644 --- a/src/db/service.rs +++ b/src/db/service.rs @@ -1,9 +1,16 @@ +use anyhow::Result; use ndb_tcp_service::TcpServiceDb; use ndb_udp_service::UdpServiceDb; -use anyhow::Result; use std::{collections::HashMap, sync::OnceLock}; -use crate::{config, endpoint::Port, service::probe::{PortProbeDb, ProbePayload, ProbePayloadDb, ResponseSignature, ResponseSignaturesDb, ServiceProbe}}; +use crate::{ + config, + endpoint::Port, + service::probe::{ + PortProbeDb, ProbePayload, ProbePayloadDb, ResponseSignature, ResponseSignaturesDb, + ServiceProbe, + }, +}; pub static TCP_SERVICE_DB: OnceLock = OnceLock::new(); pub static UDP_SERVICE_DB: OnceLock = OnceLock::new(); @@ -13,12 +20,16 @@ pub static RESPONSE_SIGNATURES_DB: OnceLock> = OnceLock:: /// Get a reference to the initialized TCP service database. pub fn tcp_service_db() -> &'static TcpServiceDb { - TCP_SERVICE_DB.get().expect("TCP_SERVICE_DB not initialized") + TCP_SERVICE_DB + .get() + .expect("TCP_SERVICE_DB not initialized") } /// Get a reference to the initialized UDP service database. pub fn udp_service_db() -> &'static UdpServiceDb { - UDP_SERVICE_DB.get().expect("UDP_SERVICE_DB not initialized") + UDP_SERVICE_DB + .get() + .expect("UDP_SERVICE_DB not initialized") } /// Get a reference to the initialized Port Probe database. @@ -28,12 +39,16 @@ pub fn port_probe_db() -> &'static HashMap> { /// Get a reference to the initialized Service Probe database. pub fn service_probe_db() -> &'static HashMap { - SERVICE_PROBE_DB.get().expect("SERVICE_PROBE_DB not initialized") + SERVICE_PROBE_DB + .get() + .expect("SERVICE_PROBE_DB not initialized") } /// Get a reference to the initialized Response Signatures database. pub fn response_signatures_db() -> &'static Vec { - RESPONSE_SIGNATURES_DB.get().expect("RESPONSE_SIGNATURES_DB not initialized") + RESPONSE_SIGNATURES_DB + .get() + .expect("RESPONSE_SIGNATURES_DB not initialized") } /// Initialize TCP Service database @@ -58,7 +73,7 @@ pub fn init_udp_service_db() -> Result<()> { pub fn init_port_probe_db() -> Result<()> { let port_probe_db: PortProbeDb = serde_json::from_str(config::db::PORT_PROBES_JSON) .expect("Invalid port-probes.json format"); - + let mut map: HashMap> = HashMap::new(); for (port, probes) in port_probe_db.map { let service_probes: Vec = probes @@ -82,8 +97,8 @@ pub fn init_service_probe_db() -> Result<()> { .expect("Invalid service-probes.json format"); let mut service_probe_map: HashMap = HashMap::new(); for probe_payload in probe_payload_db.probes { - let service_probe: ServiceProbe = ServiceProbe::from_str(&probe_payload.id) - .expect("Invalid service probe format"); + let service_probe: ServiceProbe = + ServiceProbe::from_str(&probe_payload.id).expect("Invalid service probe format"); service_probe_map.insert(service_probe, probe_payload); } SERVICE_PROBE_DB @@ -94,8 +109,9 @@ pub fn init_service_probe_db() -> Result<()> { /// Initialize Response Signatures database pub fn init_response_signatures_db() -> Result<()> { - let response_signatures_db: ResponseSignaturesDb = serde_json::from_str(config::db::SERVICE_DB_JSON) - .expect("Invalid nrev-service-db.json format"); + let response_signatures_db: ResponseSignaturesDb = + serde_json::from_str(config::db::SERVICE_DB_JSON) + .expect("Invalid nrev-service-db.json format"); RESPONSE_SIGNATURES_DB .set(response_signatures_db.signatures) .map_err(|_| anyhow::anyhow!("Failed to set RESPONSE_SIGNATURES_DB in OnceLock"))?; @@ -105,12 +121,10 @@ pub fn init_response_signatures_db() -> Result<()> { /// Get the service name for a given TCP port pub fn get_tcp_service_name(port: u16) -> Option { match TCP_SERVICE_DB.get() { - Some(db) => { - match db.get(port) { - Some(service) => Some(service.name.clone()), - None => None, - } - } + Some(db) => match db.get(port) { + Some(service) => Some(service.name.clone()), + None => None, + }, None => None, } } @@ -133,11 +147,16 @@ pub fn get_tcp_service_names(ports: &[u16]) -> HashMap { /// Get the map of port to service probes pub fn get_port_probes() -> HashMap> { let mut port_probe_map: HashMap> = HashMap::new(); - let port_probe_db: PortProbeDb = serde_json::from_str(config::db::PORT_PROBES_JSON).expect("Invalid os-ttl.json format"); + let port_probe_db: PortProbeDb = + serde_json::from_str(config::db::PORT_PROBES_JSON).expect("Invalid os-ttl.json format"); for (port, probes) in port_probe_db.map { for probe in probes { - let service_probe: ServiceProbe = ServiceProbe::from_str(&probe).expect("Invalid service probe format"); - port_probe_map.entry(port).or_insert_with(Vec::new).push(service_probe); + let service_probe: ServiceProbe = + ServiceProbe::from_str(&probe).expect("Invalid service probe format"); + port_probe_map + .entry(port) + .or_insert_with(Vec::new) + .push(service_probe); } } port_probe_map @@ -146,9 +165,11 @@ pub fn get_port_probes() -> HashMap> { /// Get the map of service probes to their payloads pub fn get_service_probes() -> HashMap { let mut service_probe_map: HashMap = HashMap::new(); - let probe_payload_db: ProbePayloadDb = serde_json::from_str(config::db::SERVICE_PROBES_JSON).expect("Invalid service-probes.json format"); + let probe_payload_db: ProbePayloadDb = serde_json::from_str(config::db::SERVICE_PROBES_JSON) + .expect("Invalid service-probes.json format"); for probe_payload in probe_payload_db.probes { - let service_probe: ServiceProbe = ServiceProbe::from_str(&probe_payload.id).expect("Invalid service probe format"); + let service_probe: ServiceProbe = + ServiceProbe::from_str(&probe_payload.id).expect("Invalid service probe format"); service_probe_map.insert(service_probe, probe_payload); } service_probe_map @@ -156,6 +177,8 @@ pub fn get_service_probes() -> HashMap { /// Get the list of response signatures pub fn get_service_response_signatures() -> Vec { - let response_signatures_db: ResponseSignaturesDb = serde_json::from_str(config::db::SERVICE_DB_JSON).expect("Invalid nrev-service-os-db.json format"); + let response_signatures_db: ResponseSignaturesDb = + serde_json::from_str(config::db::SERVICE_DB_JSON) + .expect("Invalid nrev-service-os-db.json format"); response_signatures_db.signatures } diff --git a/src/db/tls.rs b/src/db/tls.rs index 6bfda5c..5d456f9 100644 --- a/src/db/tls.rs +++ b/src/db/tls.rs @@ -23,17 +23,28 @@ pub fn tls_oid_map() -> &'static TlsOidMap { /// Initialize the TLS OID map from the bundled JSON data. pub fn init_tls_oid_map() -> Result<()> { - let map: TlsOidMap = serde_json::from_str(&TLS_OID_MAP_JSON).expect("invalid nrev-tls-oid-map.json"); - TLS_OID_MAP.set(map).map_err(|_| anyhow::anyhow!("Failed to set TLS_OID_MAP in OnceLock"))?; + let map: TlsOidMap = + serde_json::from_str(&TLS_OID_MAP_JSON).expect("invalid nrev-tls-oid-map.json"); + TLS_OID_MAP + .set(map) + .map_err(|_| anyhow::anyhow!("Failed to set TLS_OID_MAP in OnceLock"))?; Ok(()) } /// Get the name of a TLS version given its numeric representation. pub fn oid_sig_name(oid: &str) -> String { - tls_oid_map().sig.get(oid).cloned().unwrap_or_else(|| oid.to_string()) + tls_oid_map() + .sig + .get(oid) + .cloned() + .unwrap_or_else(|| oid.to_string()) } /// Get the name of a public key algorithm given its OID. pub fn oid_pubkey_name(oid: &str) -> String { - tls_oid_map().pubkey.get(oid).cloned().unwrap_or_else(|| oid.to_string()) + tls_oid_map() + .pubkey + .get(oid) + .cloned() + .unwrap_or_else(|| oid.to_string()) } diff --git a/src/dns/mod.rs b/src/dns/mod.rs index 475902c..1a69a1d 100644 --- a/src/dns/mod.rs +++ b/src/dns/mod.rs @@ -1,23 +1,31 @@ -use std::{net::IpAddr, time::Duration}; use anyhow::Result; use serde::{Deserialize, Serialize}; +use std::{net::IpAddr, time::Duration}; use crate::endpoint::Host; -pub mod resolver; pub mod probe; +pub mod resolver; /// Lookup a host by name or IP address string. pub async fn lookup_host(host: &str, timeout: Duration) -> Result { if let Ok(ip) = host.parse::() { // Reverse lookup for IP address - let hostname = reverse_lookup(ip, timeout).await.unwrap_or_else(|| ip.to_string()); - Ok(Host { hostname: Some(hostname), ip: ip }) + let hostname = reverse_lookup(ip, timeout) + .await + .unwrap_or_else(|| ip.to_string()); + Ok(Host { + hostname: Some(hostname), + ip: ip, + }) } else { // Resolve hostname to IP address let ips = lookup_ip(host, timeout).await.unwrap_or_default(); match ips.first() { - Some(ip) => Ok(Host { hostname: Some(host.to_string()), ip: *ip }), + Some(ip) => Ok(Host { + hostname: Some(host.to_string()), + ip: *ip, + }), None => Err(anyhow::anyhow!("failed to resolve host")), } } @@ -26,16 +34,16 @@ pub async fn lookup_host(host: &str, timeout: Duration) -> Result { /// Lookup a domain and return its associated IP addresses. pub async fn lookup_domain(hostname: &str, timeout: Duration) -> Domain { let ips = lookup_ip(hostname, timeout).await.unwrap_or_default(); - Domain { name: hostname.to_string(), ips } + Domain { + name: hostname.to_string(), + ips, + } } /// Perform a DNS lookup for the given hostname with a timeout. pub async fn lookup_ip(hostname: &str, timeout: Duration) -> Option> { let resolver = resolver::get_resolver().ok()?; - match tokio::time::timeout( - timeout, - async move { resolver.lookup_ip(hostname).await } - ).await { + match tokio::time::timeout(timeout, async move { resolver.lookup_ip(hostname).await }).await { Ok(Ok(ips)) => Some(ips.iter().collect()), _ => None, } @@ -44,10 +52,7 @@ pub async fn lookup_ip(hostname: &str, timeout: Duration) -> Option> /// Perform a reverse DNS lookup for the given IP address with a timeout. pub async fn reverse_lookup(ip: IpAddr, timeout: Duration) -> Option { let resolver = resolver::get_resolver().ok()?; - match tokio::time::timeout( - timeout, - async move { resolver.reverse_lookup(ip).await } - ).await { + match tokio::time::timeout(timeout, async move { resolver.reverse_lookup(ip).await }).await { Ok(Ok(names)) => names.iter().next().map(|n| n.to_string()), _ => None, } diff --git a/src/dns/probe.rs b/src/dns/probe.rs index 90ef6f2..a4b83d3 100644 --- a/src/dns/probe.rs +++ b/src/dns/probe.rs @@ -1,10 +1,10 @@ +use anyhow::Result; use futures::stream::{self, StreamExt}; -use rand::{distributions::Alphanumeric, Rng}; -use tokio::time::timeout; +use rand::{Rng, distr::Alphanumeric}; use std::sync::Arc; -use std::{net::IpAddr, time::Instant}; use std::time::Duration; -use anyhow::Result; +use std::{net::IpAddr, time::Instant}; +use tokio::time::timeout; use tracing_indicatif::span_ext::IndicatifSpanExt; use crate::dns::{Domain, DomainScanResult}; @@ -58,9 +58,16 @@ fn normalize_label(s: &str) -> String { } /// Check if the base domain has wildcard DNS records. -async fn is_wildcard_domain(resolver: &hickory_resolver::TokioResolver, base: &str, rt: Duration) -> bool { - let rand_label: String = rand::thread_rng() - .sample_iter(&Alphanumeric).take(10).map(char::from).collect(); +async fn is_wildcard_domain( + resolver: &hickory_resolver::TokioResolver, + base: &str, + rt: Duration, +) -> bool { + let rand_label: String = rand::rng() + .sample_iter(&Alphanumeric) + .take(10) + .map(char::from) + .collect(); let test = format!("{}.{}", rand_label, base); match timeout(rt, resolver.lookup_ip(test)).await { Ok(Ok(lip)) => !lip.as_lookup().is_empty(), @@ -72,7 +79,8 @@ async fn is_wildcard_domain(resolver: &hickory_resolver::TokioResolver, base: &s pub async fn scan_subdomain(setting: &DomainScanSetting) -> Result { let base = normalize_label(&setting.base_domain); - let target_domains: Vec = setting.word_list + let target_domains: Vec = setting + .word_list .iter() .map(|w| format!("{}.{}", normalize_label(w), base)) .collect(); @@ -96,7 +104,10 @@ pub async fn scan_subdomain(setting: &DomainScanSetting) -> Result { let mut uniq: Vec = lip.iter().collect(); @@ -109,7 +120,7 @@ pub async fn scan_subdomain(setting: &DomainScanSetting) -> Result Result= deadline { break; } + if now >= deadline { + break; + } let remaining = deadline - now; tokio::select! { @@ -145,7 +158,9 @@ pub async fn scan_subdomain(setting: &DomainScanSetting) -> Result Result { // Use system DNS configuration match TokioResolver::builder_tokio() { Ok(resolver) => Ok(resolver.build()), - Err(e) => Err(anyhow::anyhow!("Failed to create TokioAsyncResolver: {}", e)), + Err(e) => Err(anyhow::anyhow!( + "Failed to create TokioAsyncResolver: {}", + e + )), } } #[cfg(not(any(unix, target_os = "windows")))] pub fn get_resolver() -> Result { use hickory_resolver::name_server::TokioConnectionProvider; - let builder = TokioResolver::builder_with_config(ResolverConfig::default(), TokioConnectionProvider::default()); + let builder = TokioResolver::builder_with_config( + ResolverConfig::default(), + TokioConnectionProvider::default(), + ); return Ok(builder.build()); } diff --git a/src/endpoint.rs b/src/endpoint.rs index 9257c5a..75b2293 100644 --- a/src/endpoint.rs +++ b/src/endpoint.rs @@ -1,12 +1,13 @@ -use std::net::{IpAddr, SocketAddr}; -use std::collections::BTreeMap; use netdev::MacAddr; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::net::{IpAddr, SocketAddr}; mod ports_vec { use super::*; - use serde::{Serializer, Deserializer}; + use serde::{Deserializer, Serializer}; + #[allow(dead_code)] #[derive(Serialize, Deserialize)] struct Item { pub port: Port, @@ -15,13 +16,17 @@ mod ports_vec { } pub fn serialize(map: &BTreeMap, s: S) -> Result - where S: Serializer { + where + S: Serializer, + { let vec: Vec<&PortResult> = map.values().collect(); vec.serialize(s) } pub fn deserialize<'de, D>(d: D) -> Result, D::Error> - where D: Deserializer<'de> { + where + D: Deserializer<'de>, + { let vec = >::deserialize(d)?; Ok(vec.into_iter().map(|pr| (pr.port, pr)).collect()) } @@ -75,11 +80,25 @@ impl Port { } impl From<(u16, TransportProtocol)> for Port { - fn from(t: (u16, TransportProtocol)) -> Self { Self { number: t.0, transport: t.1 } } + fn from(t: (u16, TransportProtocol)) -> Self { + Self { + number: t.0, + transport: t.1, + } + } } impl std::fmt::Display for Port { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}:{}", match self.transport { TransportProtocol::Tcp=>"tcp", TransportProtocol::Udp=>"udp", TransportProtocol::Quic=>"quic" }, self.number) + write!( + f, + "{}:{}", + match self.transport { + TransportProtocol::Tcp => "tcp", + TransportProtocol::Udp => "udp", + TransportProtocol::Quic => "quic", + }, + self.number + ) } } @@ -243,11 +262,18 @@ impl Default for Host { impl Host { /// Create a new Host instance. pub fn new(ip: IpAddr) -> Self { - Self { ip, ..Default::default() } + Self { + ip, + ..Default::default() + } } /// Create a new Host instance with the specified hostname. pub fn with_hostname(ip: IpAddr, hostname: String) -> Self { - Self { ip, hostname: Some(hostname), ..Default::default() } + Self { + ip, + hostname: Some(hostname), + ..Default::default() + } } } @@ -276,11 +302,18 @@ impl Default for Endpoint { impl Endpoint { /// Create a new Endpoint instance. pub fn new(ip: IpAddr) -> Self { - Self { ip, ..Default::default() } + Self { + ip, + ..Default::default() + } } /// Create a new Endpoint instance with the specified hostname. pub fn with_hostname(ip: IpAddr, hostname: String) -> Self { - Self { ip, hostname: Some(hostname), ..Default::default() } + Self { + ip, + hostname: Some(hostname), + ..Default::default() + } } /// Add a port to the endpoint if it does not already exist. pub fn upsert_port(&mut self, port: Port) { @@ -290,8 +323,12 @@ impl Endpoint { } /// Merge another Endpoint into this one, combining tags and ports. pub fn merge(&mut self, other: Endpoint) { - if self.hostname.is_none() { self.hostname = other.hostname; } - if self.mac_addr.is_none() { self.mac_addr = other.mac_addr; } + if self.hostname.is_none() { + self.hostname = other.hostname; + } + if self.mac_addr.is_none() { + self.mac_addr = other.mac_addr; + } for t in other.tags { if !self.tags.contains(&t) { @@ -348,11 +385,18 @@ impl Default for EndpointResult { impl EndpointResult { /// Create a new EndpointResult instance. pub fn new(ip: IpAddr) -> Self { - Self { ip, ..Default::default() } + Self { + ip, + ..Default::default() + } } /// Create a new EndpointResult instance with the specified hostname. pub fn with_hostname(ip: IpAddr, hostname: String) -> Self { - Self { ip, hostname: Some(hostname), ..Default::default() } + Self { + ip, + hostname: Some(hostname), + ..Default::default() + } } /// Add or update a PortResult in the endpoint's ports map. pub fn upsert_port(&mut self, pr: PortResult) { @@ -360,9 +404,15 @@ impl EndpointResult { } /// Merge another EndpointResult into this one, combining tags, ports, and OS guess. pub fn merge(&mut self, other: EndpointResult) { - if self.hostname.is_none() { self.hostname = other.hostname; } - if self.mac_addr.is_none() { self.mac_addr = other.mac_addr; } - if self.vendor_name.is_none() { self.vendor_name = other.vendor_name; } + if self.hostname.is_none() { + self.hostname = other.hostname; + } + if self.mac_addr.is_none() { + self.mac_addr = other.mac_addr; + } + if self.vendor_name.is_none() { + self.vendor_name = other.vendor_name; + } //self.cpes = other.cpes; let incoming: Vec = other @@ -440,13 +490,15 @@ impl EndpointResult { } impl From for EndpointResult { - fn from(ip: IpAddr) -> Self { EndpointResult::new(ip) } + fn from(ip: IpAddr) -> Self { + EndpointResult::new(ip) + } } #[cfg(test)] mod tests { use super::*; - use serde_json::{to_string_pretty, from_str}; + use serde_json::{from_str, to_string_pretty}; #[test] fn ports_roundtrip() { @@ -485,12 +537,18 @@ mod tests { }); let json = to_string_pretty(&ep).unwrap(); - + assert!(json.contains("\"ports\": [")); let back: EndpointResult = from_str(&json).unwrap(); assert_eq!(back.ports.len(), 2); - assert!(back.ports.contains_key(&Port::new(80, TransportProtocol::Tcp))); - assert!(back.ports.contains_key(&Port::new(443, TransportProtocol::Tcp))); + assert!( + back.ports + .contains_key(&Port::new(80, TransportProtocol::Tcp)) + ); + assert!( + back.ports + .contains_key(&Port::new(443, TransportProtocol::Tcp)) + ); } } diff --git a/src/interface.rs b/src/interface.rs index d185763..6db0a2e 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -169,4 +169,4 @@ pub fn get_gateway_macaddr(iface: &Interface) -> MacAddr { Some(gateway) => gateway.mac_addr.clone(), None => MacAddr::zero(), } -} \ No newline at end of file +} diff --git a/src/log.rs b/src/log.rs index ca37e20..ee48344 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,9 +1,9 @@ use anyhow::Result; -use tracing::level_filters::LevelFilter; use std::fs::File; +use tracing::level_filters::LevelFilter; use tracing_indicatif::IndicatifLayer; +use tracing_subscriber::{filter::Targets, fmt, prelude::*, registry}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; -use tracing_subscriber::{fmt, prelude::*, registry, filter::Targets}; use crate::cli::Cli; use crate::time::LocalTimeOnly; @@ -46,11 +46,16 @@ pub fn init_logger(cli_args: &Cli) -> Result<()> { } // Determine log file path - let log_file_path = cli_args.log_file_path.clone() + let log_file_path = cli_args + .log_file_path + .clone() .unwrap_or_else(|| crate::config::get_user_file_path("nrev.log").unwrap()); - + // Open log file in append mode - let file = File::options().create(true).append(true).open(&log_file_path)?; + let file = File::options() + .create(true) + .append(true) + .open(&log_file_path)?; // File-specific fmt layer let file_fmt = fmt::layer() @@ -66,8 +71,8 @@ pub fn init_logger(cli_args: &Cli) -> Result<()> { registry() .with(indicatif_layer) - .with(console_fmt.with_filter(console_filter)) - .with(file_fmt.with_filter(file_filter)) + .with(console_fmt.with_filter(console_filter)) + .with(file_fmt.with_filter(file_filter)) .init(); } @@ -76,9 +81,7 @@ pub fn init_logger(cli_args: &Cli) -> Result<()> { // release: no output to screen, ERROR only for file let file_filter = LevelFilter::ERROR; - registry() - .with(file_fmt.with_filter(file_filter)) - .init(); + registry().with(file_fmt.with_filter(file_filter)).init(); } Ok(()) diff --git a/src/main.rs b/src/main.rs index 1b930bd..59d823e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,24 +1,24 @@ +pub mod capture; pub mod cli; pub mod cmd; pub mod config; -pub mod endpoint; +pub mod db; pub mod dns; -pub mod scan; -pub mod output; -pub mod capture; +pub mod endpoint; pub mod interface; -pub mod packet; -pub mod time; pub mod log; -pub mod service; -pub mod db; +pub mod nei; pub mod os; +pub mod output; +pub mod packet; pub mod ping; -pub mod protocol; pub mod probe; -pub mod util; -pub mod nei; +pub mod protocol; +pub mod scan; +pub mod service; +pub mod time; pub mod trace; +pub mod util; use clap::Parser; use cli::{Cli, Command}; @@ -40,7 +40,7 @@ async fn main() { DbInitializer::with_all().init().await; let r = cmd::port::run(args, cli.no_stdout, cli.output).await; match r { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::error!("Port scan failed: {}", e), } } @@ -50,7 +50,7 @@ async fn main() { let r = cmd::host::run(args, cli.no_stdout, cli.output).await; match r { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::error!("Host scan failed: {}", e), } } @@ -60,7 +60,7 @@ async fn main() { let r = cmd::ping::run(args, cli.no_stdout, cli.output).await; match r { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::error!("Ping failed: {}", e), } } @@ -70,34 +70,38 @@ async fn main() { let r = cmd::trace::run(args, cli.no_stdout, cli.output).await; match r { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::error!("Trace failed: {}", e), } } Command::Nei(args) => { let db_ini = DbInitializer::new(); db_ini.with_oui_db().init().await; - + let r = cmd::nei::run(args, cli.no_stdout, cli.output).await; match r { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::error!("Neighbor discovery failed: {}", e), } } Command::Domain(args) => { let r = cmd::domain::run(args, cli.no_stdout, cli.output).await; match r { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::error!("Domain scan failed: {}", e), } } Command::Interface(args) => { let r = cmd::interface::show(&args); match r { - Ok(_) => {}, + Ok(_) => {} Err(e) => tracing::error!("Show interfaces failed: {}", e), } } } - tracing::info!("nrev v{} completed in {:?}", env!("CARGO_PKG_VERSION"), start_time.elapsed()); + tracing::info!( + "nrev v{} completed in {:?}", + env!("CARGO_PKG_VERSION"), + start_time.elapsed() + ); } diff --git a/src/nei/arp.rs b/src/nei/arp.rs index 43e3ab5..2de7136 100644 --- a/src/nei/arp.rs +++ b/src/nei/arp.rs @@ -1,17 +1,24 @@ +use crate::nei::NeighborDiscoveryResult; use anyhow::Result; +use futures::future::poll_fn; +use futures::stream::StreamExt; use netdev::Interface; +use nex::datalink::async_io::{AsyncChannel, async_channel}; use nex::packet::{ arp::ArpOperation, frame::{Frame, ParseOption}, }; -use std::{net::{IpAddr, Ipv4Addr}, time::{Duration, Instant}}; -use futures::stream::StreamExt; -use futures::future::poll_fn; -use nex::datalink::async_io::{async_channel, AsyncChannel}; -use crate::nei::NeighborDiscoveryResult; +use std::{ + net::{IpAddr, Ipv4Addr}, + time::{Duration, Instant}, +}; /// Send an ARP request to the specified IPv4 address on the given interface and wait for a reply. -pub async fn send_arp(ipv4_addr: Ipv4Addr, iface: &Interface, recv_timeout: Duration) -> Result { +pub async fn send_arp( + ipv4_addr: Ipv4Addr, + iface: &Interface, + recv_timeout: Duration, +) -> Result { let next_hop = crate::util::ip::next_hop_ip(iface, IpAddr::V4(ipv4_addr)) .ok_or_else(|| anyhow::anyhow!("No next hop found for {}", ipv4_addr))?; @@ -27,8 +34,7 @@ pub async fn send_arp(ipv4_addr: Ipv4Addr, iface: &Interface, recv_timeout: Dura promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&iface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&iface, config)? else { unreachable!(); }; @@ -37,8 +43,7 @@ pub async fn send_arp(ipv4_addr: Ipv4Addr, iface: &Interface, recv_timeout: Dura let start_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &arp_packet)).await { - Ok(_) => { - }, + Ok(_) => {} Err(e) => eprintln!("Failed to send packet: {}", e), } @@ -74,11 +79,11 @@ pub async fn send_arp(ipv4_addr: Ipv4Addr, iface: &Interface, recv_timeout: Dura Ok(Some(Err(e))) => { tracing::error!("Failed to receive packet: {}", e); anyhow::bail!("Failed to receive packet: {}", e); - }, + } Ok(None) => { tracing::error!("Channel closed"); anyhow::bail!("Channel closed"); - }, + } Err(_) => { tracing::error!("Request timeout"); anyhow::bail!("Request timeout"); diff --git a/src/nei/mod.rs b/src/nei/mod.rs index 8d7df2f..0d03755 100644 --- a/src/nei/mod.rs +++ b/src/nei/mod.rs @@ -1,9 +1,12 @@ pub mod arp; pub mod ndp; -use serde::{Deserialize, Serialize}; use netdev::MacAddr; -use std::{net::{IpAddr, Ipv4Addr}, time::Duration}; +use serde::{Deserialize, Serialize}; +use std::{ + net::{IpAddr, Ipv4Addr}, + time::Duration, +}; use crate::protocol::Protocol; diff --git a/src/nei/ndp.rs b/src/nei/ndp.rs index 5ea0b2f..f7495ee 100644 --- a/src/nei/ndp.rs +++ b/src/nei/ndp.rs @@ -1,17 +1,24 @@ +use crate::nei::NeighborDiscoveryResult; use anyhow::Result; +use futures::future::poll_fn; +use futures::stream::StreamExt; use netdev::Interface; +use nex::datalink::async_io::{AsyncChannel, async_channel}; use nex::packet::{ frame::{Frame, ParseOption}, icmpv6::Icmpv6Type, }; -use std::{net::{IpAddr, Ipv6Addr}, time::{Duration, Instant}}; -use futures::stream::StreamExt; -use futures::future::poll_fn; -use nex::datalink::async_io::{async_channel, AsyncChannel}; -use crate::nei::NeighborDiscoveryResult; +use std::{ + net::{IpAddr, Ipv6Addr}, + time::{Duration, Instant}, +}; /// Send an NDP (Neighbor Discovery Protocol) request to the specified IPv6 address on the given interface and wait for a reply. -pub async fn send_ndp(ipv6_addr: Ipv6Addr, iface: &Interface, recv_timeout: Duration) -> Result { +pub async fn send_ndp( + ipv6_addr: Ipv6Addr, + iface: &Interface, + recv_timeout: Duration, +) -> Result { let src_ip = iface .ipv6 .iter() @@ -33,18 +40,16 @@ pub async fn send_ndp(ipv6_addr: Ipv6Addr, iface: &Interface, recv_timeout: Dura promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&iface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&iface, config)? else { unreachable!(); }; let arp_packet = crate::packet::ndp::build_ndp_packet(iface, next_hop); let start_time = Instant::now(); - + match poll_fn(|cx| tx.poll_send(cx, &arp_packet)).await { - Ok(_) => { - }, + Ok(_) => {} Err(e) => eprintln!("Failed to send packet: {}", e), } @@ -81,7 +86,6 @@ pub async fn send_ndp(ipv6_addr: Ipv6Addr, iface: &Interface, recv_timeout: Dura if_index: iface.index, }; return Ok(ndp_result); - } else { eprintln!( "Received NDP reply from unexpected source: {}", @@ -100,11 +104,11 @@ pub async fn send_ndp(ipv6_addr: Ipv6Addr, iface: &Interface, recv_timeout: Dura Ok(Some(Err(e))) => { tracing::error!("Failed to receive packet: {}", e); anyhow::bail!("Failed to receive packet: {}", e); - }, + } Ok(None) => { tracing::error!("Channel closed"); anyhow::bail!("Channel closed"); - }, + } Err(_) => { tracing::error!("Request timeout"); anyhow::bail!("Request timeout"); diff --git a/src/os/mod.rs b/src/os/mod.rs index a03d1ed..4eb7c22 100644 --- a/src/os/mod.rs +++ b/src/os/mod.rs @@ -1,9 +1,12 @@ pub mod probe; -use nex::packet::{frame::Frame, tcp::{TcpHeader, TcpOptionKind}}; -use serde::{Deserialize, Serialize}; -use anyhow::Result; use crate::{output::port::OsProbeResult, probe::ProbeSetting}; +use anyhow::Result; +use nex::packet::{ + frame::Frame, + tcp::{TcpHeader, TcpOptionKind}, +}; +use serde::{Deserialize, Serialize}; /// OS Detector using TCP SYN packets pub struct OsDetector { diff --git a/src/os/probe/tcp.rs b/src/os/probe/tcp.rs index 5f93e9f..baf8776 100644 --- a/src/os/probe/tcp.rs +++ b/src/os/probe/tcp.rs @@ -1,19 +1,19 @@ -use futures::stream::StreamExt; -use futures::future::poll_fn; -use nex::datalink::async_io::{async_channel, AsyncChannel}; -use nex::packet::frame::{Frame, ParseOption}; -use tracing_indicatif::span_ext::IndicatifSpanExt; -use std::collections::BTreeMap; -use anyhow::Result; use crate::config::default::DEFAULT_LOCAL_TCP_PORT; -use crate::endpoint::{EndpointResult, OsGuess, Port, PortResult, PortState, ServiceInfo, TransportProtocol}; +use crate::endpoint::{ + EndpointResult, OsGuess, Port, PortResult, PortState, ServiceInfo, TransportProtocol, +}; use crate::output::port::OsProbeResult; use crate::probe::ProbeSetting; +use anyhow::Result; +use futures::future::poll_fn; +use futures::stream::StreamExt; +use nex::datalink::async_io::{AsyncChannel, async_channel}; +use nex::packet::frame::{Frame, ParseOption}; +use std::collections::BTreeMap; +use tracing_indicatif::span_ext::IndicatifSpanExt; /// Run OS detection probe using TCP SYN packets and return the results. -pub async fn run_os_probe( - setting: ProbeSetting, -) -> Result { +pub async fn run_os_probe(setting: ProbeSetting) -> Result { let mut result = OsProbeResult::new(); let interface = match crate::interface::get_interface_by_index(setting.if_index) { Some(interface) => interface, @@ -31,13 +31,14 @@ pub async fn run_os_probe( promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { let payload_offset = if interface.is_loopback() { 14 } else { 0 }; parse_option.from_ip_packet = true; parse_option.offset = payload_offset; @@ -105,9 +106,12 @@ pub async fn run_os_probe( } else { continue; } - tracing::debug!("Matching frame...: {:?}", frame.transport.as_ref().unwrap().tcp); + tracing::debug!( + "Matching frame...: {:?}", + frame.transport.as_ref().unwrap().tcp + ); match crate::os::match_tcpip_signatures(&frame) { - Some(os_match) => { + Some(os_match) => { let port_result = PortResult { port: Port::new(port.number, TransportProtocol::Tcp), state: PortState::Open, @@ -117,7 +121,10 @@ pub async fn run_os_probe( let endpoint_result = EndpointResult { ip: target.ip, hostname: target.hostname.clone(), - ports: BTreeMap::from([(port_result.port.clone(), port_result)]), + ports: BTreeMap::from([( + port_result.port.clone(), + port_result, + )]), mac_addr: target.mac_addr, vendor_name: None, os: OsGuess { @@ -133,11 +140,11 @@ pub async fn run_os_probe( detected = true; break; - } - None => { - tracing::debug!("No matching OS found"); - } - } + } + None => { + tracing::debug!("No matching OS found"); + } + } } Ok(Some(Err(e))) => { tracing::error!("Failed to receive packet: {}", e); diff --git a/src/output/domain.rs b/src/output/domain.rs index 7e9db2e..60d03e9 100644 --- a/src/output/domain.rs +++ b/src/output/domain.rs @@ -1,6 +1,9 @@ -use termtree::Tree; +use crate::{ + dns::{Domain, DomainScanResult}, + output::tree_label, +}; use std::net::IpAddr; -use crate::{dns::{Domain, DomainScanResult}, output::tree_label}; +use termtree::Tree; /// Print the domain scan results in a tree structure. pub fn print_domain_tree(base_domain: &Domain, res: &DomainScanResult) { @@ -73,7 +76,7 @@ pub fn print_domain_tree(base_domain: &Domain, res: &DomainScanResult) { base_node.push(node); } - + root.push(base_node); println!("Scan report(s)"); diff --git a/src/output/host.rs b/src/output/host.rs index 1a7e843..8c91588 100644 --- a/src/output/host.rs +++ b/src/output/host.rs @@ -1,5 +1,5 @@ +use crate::output::{ScanResult, tree_label}; use termtree::Tree; -use crate::output::{tree_label, ScanResult}; /// Print the scan report results in a tree structure. pub fn print_report_tree(result: &ScanResult) { @@ -52,7 +52,11 @@ pub fn print_report_tree(result: &ScanResult) { // Port information if !ep.ports.is_empty() { for (port, pr) in &ep.ports { - let mut pnode = Tree::new(tree_label(format!("{}/{}", port.number, port.transport.as_str().to_uppercase()))); + let mut pnode = Tree::new(tree_label(format!( + "{}/{}", + port.number, + port.transport.as_str().to_uppercase() + ))); pnode.push(Tree::new(tree_label(format!("state: {:?}", pr.state)))); if let Some(name) = &pr.service.name { pnode.push(Tree::new(tree_label(format!("service: {}", name)))); diff --git a/src/output/interface.rs b/src/output/interface.rs index 9238132..05fd41b 100644 --- a/src/output/interface.rs +++ b/src/output/interface.rs @@ -1,5 +1,5 @@ -use termtree::Tree; use netdev::Interface; +use termtree::Tree; use crate::output::tree_label; @@ -12,7 +12,7 @@ pub fn print_interface_tree(ifaces: &[Interface]) { iface.name, if iface.default { " (default)" } else { "" } )); - + node.push(Tree::new(format!("index: {}", iface.index))); if let Some(fn_name) = &iface.friendly_name { diff --git a/src/output/mod.rs b/src/output/mod.rs index 784b041..034022d 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -1,16 +1,16 @@ -use std::time::Duration; +use crate::endpoint::{Endpoint, EndpointResult}; use nex::packet::frame::Frame; use serde::{Deserialize, Serialize}; -use crate::endpoint::{Endpoint, EndpointResult}; +use std::time::Duration; -pub mod port; -pub mod progress; +pub mod domain; pub mod host; +pub mod interface; +pub mod nei; pub mod ping; +pub mod port; +pub mod progress; pub mod trace; -pub mod nei; -pub mod domain; -pub mod interface; /// Convert a string into a tree label. fn tree_label>(s: S) -> String { @@ -42,7 +42,10 @@ impl ScanResult { /// Get a list of active endpoints (those with active results). pub fn get_active_endpoints(&self) -> Vec { - self.endpoints.iter().filter_map(|e| e.active_endpoint()).collect() + self.endpoints + .iter() + .filter_map(|e| e.active_endpoint()) + .collect() } /// Sort the endpoints by their IP addresses. diff --git a/src/output/nei.rs b/src/output/nei.rs index e0fb348..a54bf1f 100644 --- a/src/output/nei.rs +++ b/src/output/nei.rs @@ -1,5 +1,5 @@ -use termtree::Tree; use crate::nei::NeighborDiscoveryResult; +use termtree::Tree; /// Print the neighbor discovery results in a tree structure. pub fn print_neighbor_tree(entries: &[NeighborDiscoveryResult]) { @@ -23,11 +23,20 @@ pub fn print_neighbor_tree(entries: &[NeighborDiscoveryResult]) { node.push(Tree::new(format!("Vendor: {}", vendor))); } - node.push(Tree::new(format!("Interface: {} (idx={})", e.if_name, e.if_index))); + node.push(Tree::new(format!( + "Interface: {} (idx={})", + e.if_name, e.if_index + ))); - node.push(Tree::new(format!("Protoco: {}", e.protocol.as_str().to_uppercase()))); + node.push(Tree::new(format!( + "Protoco: {}", + e.protocol.as_str().to_uppercase() + ))); - node.push(Tree::new(format!("RTT: {:.3}ms", e.rtt.as_secs_f64() * 1e3))); + node.push(Tree::new(format!( + "RTT: {:.3}ms", + e.rtt.as_secs_f64() * 1e3 + ))); root.push(node); } diff --git a/src/output/ping.rs b/src/output/ping.rs index d3dd00b..9da8efc 100644 --- a/src/output/ping.rs +++ b/src/output/ping.rs @@ -1,5 +1,5 @@ -use std::time::Duration; use netdev::MacAddr; +use std::time::Duration; use termtree::Tree; use crate::{ping::result::PingResult, probe::ProbeStatusKind, protocol::Protocol}; @@ -27,7 +27,11 @@ pub fn print_ping_tree(res: &PingResult) { // Packet loss rate let sent = s.transmitted_count as f64; let recv = s.received_count as f64; - let loss = if sent > 0.0 { ((sent - recv) / sent) * 100.0 } else { 0.0 }; + let loss = if sent > 0.0 { + ((sent - recv) / sent) * 100.0 + } else { + 0.0 + }; let mut root = Tree::new(title); @@ -43,7 +47,10 @@ pub fn print_ping_tree(res: &PingResult) { if let Some(hostname) = &res.hostname { summary.push(Tree::new(format!("Hostname: {}", hostname))); } - summary.push(Tree::new(format!("Protocol: {}", format!("{:?}", res.protocol).to_uppercase()))); + summary.push(Tree::new(format!( + "Protocol: {}", + format!("{:?}", res.protocol).to_uppercase() + ))); match res.protocol { Protocol::Icmp => {} Protocol::Tcp => { @@ -54,14 +61,21 @@ pub fn print_ping_tree(res: &PingResult) { Protocol::Udp => {} _ => {} } - summary.push(Tree::new(format!("Received/Sent: {}/{}", s.received_count, s.transmitted_count))); + summary.push(Tree::new(format!( + "Received/Sent: {}/{}", + s.received_count, s.transmitted_count + ))); summary.push(Tree::new(format!("Packet loss: {}", pct(loss)))); summary.push(Tree::new(format!("Elapsed: {:?}", res.elapsed_time))); if let Some(min) = &s.min { let mut rtt = Tree::new("RTT".to_string()); rtt.push(Tree::new(format!("MIN: {}", fmt_ms(min)))); - if let Some(avg) = &s.avg { rtt.push(Tree::new(format!("AVG: {}", fmt_ms(avg)))); } - if let Some(max) = &s.max { rtt.push(Tree::new(format!("MAX: {}", fmt_ms(max)))); } + if let Some(avg) = &s.avg { + rtt.push(Tree::new(format!("AVG: {}", fmt_ms(avg)))); + } + if let Some(max) = &s.max { + rtt.push(Tree::new(format!("MAX: {}", fmt_ms(max)))); + } summary.push(rtt); } root.push(summary); @@ -92,7 +106,7 @@ pub fn print_ping_tree(res: &PingResult) { node.push(Tree::new(format!("State: {}", state.as_str()))); } replies.push(node); - }, + } _ => { let err_head = format!( "#{} {}: {}", @@ -104,7 +118,7 @@ pub fn print_ping_tree(res: &PingResult) { .to_string(); let node = Tree::new(err_head); replies.push(node); - }, + } } } root.push(replies); diff --git a/src/output/port.rs b/src/output/port.rs index 64c4144..53a147e 100644 --- a/src/output/port.rs +++ b/src/output/port.rs @@ -1,9 +1,17 @@ -use std::{collections::BTreeMap, net::IpAddr, time::{Duration, SystemTime}}; +use std::{ + collections::BTreeMap, + net::IpAddr, + time::{Duration, SystemTime}, +}; +use crate::{ + endpoint::{EndpointResult, Port, PortResult, PortState, ServiceInfo, TransportProtocol}, + output::{ScanResult, tree_label}, + service::{ServiceDetectionResult, probe::ServiceProbe}, +}; use nex::packet::frame::Frame; use serde::{Deserialize, Serialize}; use termtree::Tree; -use crate::{endpoint::{EndpointResult, Port, PortResult, PortState, ServiceInfo, TransportProtocol}, output::{tree_label, ScanResult}, service::{probe::ServiceProbe, ServiceDetectionResult}}; /// Results of OS probing #[derive(Debug, Clone, Serialize, Deserialize)] @@ -38,8 +46,8 @@ pub struct ScanReport { /// Metadata about the scan report #[derive(Serialize, Deserialize, Debug)] pub struct ReportMeta { - pub tool: String, // "nrev" - pub version: String, // env!("CARGO_PKG_VERSION") + pub tool: String, // "nrev" + pub version: String, // env!("CARGO_PKG_VERSION") pub started_at: SystemTime, pub finished_at: Option, } @@ -61,9 +69,9 @@ pub struct ReportStats { pub hosts_total: usize, pub ports_scanned: usize, pub open_ports: usize, - pub duration_scan: Option, // PortScan - pub duration_service: Option, // ServiceDetect - pub duration_os: Option, // OS probe + pub duration_scan: Option, // PortScan + pub duration_service: Option, // ServiceDetect + pub duration_os: Option, // OS probe } /// An attempt to probe a service on a port @@ -74,7 +82,9 @@ pub struct PortProbeAttempt { } impl ScanReport { - pub fn new() -> Self { Self::default() } + pub fn new() -> Self { + Self::default() + } /// Apply port scan results: merge endpoints, update stats pub fn apply_port_scan(&mut self, ps: ScanResult) { @@ -96,7 +106,10 @@ impl ScanReport { continue; }; // Upsert port result - let port_key = Port { number: r.port, transport: r.transport }; + let port_key = Port { + number: r.port, + transport: r.transport, + }; let pr = ep.ports.entry(port_key).or_insert_with(|| PortResult { port: port_key, state: PortState::Open, @@ -135,10 +148,12 @@ impl ScanReport { if pr.service.name.is_none() { match port.transport { TransportProtocol::Tcp => { - pr.service.name = tcp_svc_db.get_name(port.number).map(|s| s.to_string()); + pr.service.name = + tcp_svc_db.get_name(port.number).map(|s| s.to_string()); } TransportProtocol::Udp | TransportProtocol::Quic => { - pr.service.name = udp_svc_db.get_name(port.number).map(|s| s.to_string()); + pr.service.name = + udp_svc_db.get_name(port.number).map(|s| s.to_string()); } } } @@ -155,7 +170,11 @@ impl ScanReport { let mut open_ports = 0usize; for ep in self.endpoints.values() { ports_scanned += ep.ports.len(); - open_ports += ep.ports.values().filter(|p| p.state == PortState::Open).count(); + open_ports += ep + .ports + .values() + .filter(|p| p.state == PortState::Open) + .count(); } self.stats.ports_scanned = ports_scanned; self.stats.open_ports = open_ports; @@ -172,8 +191,12 @@ fn select_better_service(cur: ServiceInfo, newv: ServiceInfo) -> ServiceInfo { /// Score a ServiceInfo based on the presence of certain fields. fn score_service(s: &ServiceInfo) -> usize { let mut sc = 0; - if s.name.is_some() { sc += 1; } - if s.product.is_some() { sc += 1; } + if s.name.is_some() { + sc += 1; + } + if s.product.is_some() { + sc += 1; + } if let Some(b) = &s.banner { sc += 1; // Check for HTTP 200 OK for additional points @@ -199,11 +222,17 @@ pub fn print_report_tree(rep: &ScanReport) { // OS if ep.os.family.is_some() || !ep.cpes.is_empty() { let mut os_node = Tree::new(tree_label("os")); - if let Some(f) = &ep.os.family { os_node.push(Tree::new(tree_label(format!("family: {}", f)))); } - if let Some(v) = &ep.os.ttl_observed { os_node.push(Tree::new(tree_label(format!("TTL: {}", v)))); } + if let Some(f) = &ep.os.family { + os_node.push(Tree::new(tree_label(format!("family: {}", f)))); + } + if let Some(v) = &ep.os.ttl_observed { + os_node.push(Tree::new(tree_label(format!("TTL: {}", v)))); + } if !ep.cpes.is_empty() { let mut cpe_node = Tree::new(tree_label("cpes")); - for c in &ep.cpes { cpe_node.push(Tree::new(c.clone())); } + for c in &ep.cpes { + cpe_node.push(Tree::new(c.clone())); + } os_node.push(cpe_node); } ep_root.push(os_node); @@ -214,14 +243,26 @@ pub fn print_report_tree(rep: &ScanReport) { if pr.state != PortState::Open { continue; } - let mut pnode = Tree::new(tree_label(format!("{}/{}", port.number, port.transport.as_str().to_uppercase()))); + let mut pnode = Tree::new(tree_label(format!( + "{}/{}", + port.number, + port.transport.as_str().to_uppercase() + ))); pnode.push(Tree::new(tree_label(format!("state: {:?}", pr.state)))); - if let Some(name) = &pr.service.name { pnode.push(Tree::new(tree_label(format!("service: {}", name)))); } - if let Some(b) = &pr.service.banner { pnode.push(Tree::new(tree_label(format!("banner: {}", b)))); } - if let Some(p) = &pr.service.product { pnode.push(Tree::new(tree_label(format!("product: {}", p)))); } + if let Some(name) = &pr.service.name { + pnode.push(Tree::new(tree_label(format!("service: {}", name)))); + } + if let Some(b) = &pr.service.banner { + pnode.push(Tree::new(tree_label(format!("banner: {}", b)))); + } + if let Some(p) = &pr.service.product { + pnode.push(Tree::new(tree_label(format!("product: {}", p)))); + } if !pr.service.cpes.is_empty() { let mut c = Tree::new(tree_label("cpes")); - for cp in &pr.service.cpes { c.push(Tree::new(cp.clone())); } + for cp in &pr.service.cpes { + c.push(Tree::new(cp.clone())); + } pnode.push(c); } ep_root.push(pnode); diff --git a/src/output/progress.rs b/src/output/progress.rs index 09aa433..9832f9e 100644 --- a/src/output/progress.rs +++ b/src/output/progress.rs @@ -13,10 +13,7 @@ pub fn get_progress_style() -> ProgressStyle { } /// Custom formatter for elapsed time with millisecond precision. -fn elapsed_precise_subsec( - state: &ProgressState, - writer: &mut dyn std::fmt::Write, -) { +fn elapsed_precise_subsec(state: &ProgressState, writer: &mut dyn std::fmt::Write) { let elapsed = state.elapsed(); let secs = elapsed.as_secs(); let sub_ms = elapsed.subsec_millis(); diff --git a/src/output/trace.rs b/src/output/trace.rs index c8c3a1d..93f64ef 100644 --- a/src/output/trace.rs +++ b/src/output/trace.rs @@ -1,17 +1,23 @@ -use std::net::IpAddr; use netdev::MacAddr; +use std::net::IpAddr; use termtree::Tree; use crate::endpoint::{Host, NodeType}; -use crate::trace::TraceResult; use crate::probe::ProbeStatusKind; +use crate::trace::TraceResult; /// Format a Duration as HH:MM:SS.mmm fn fmt_dur(d: std::time::Duration) -> String { // HH:MM:SS.mmm let s = d.as_secs(); let ms = d.subsec_millis(); - format!("{:02}:{:02}:{:02}.{:03}", s/3600, (s%3600)/60, s%60, ms) + format!( + "{:02}:{:02}:{:02}.{:03}", + s / 3600, + (s % 3600) / 60, + s % 60, + ms + ) } /// Format an IP address with an optional hostname. @@ -31,13 +37,24 @@ pub fn print_trace_tree(tr: &TraceResult, target: Host) { } // Check if the target was reached - let reached = tr.nodes.iter().any(|n| n.ip_addr == target.ip && matches!(n.probe_status.kind, ProbeStatusKind::Done)); + let reached = tr + .nodes + .iter() + .any(|n| n.ip_addr == target.ip && matches!(n.probe_status.kind, ProbeStatusKind::Done)); let mut root = if reached { - Tree::new(format!("Traceroute to {} - reached ({} hops, elapsed {})", - fmt_ip_host(target.ip, &target.hostname), tr.nodes.len(), fmt_dur(tr.elapsed_time))) + Tree::new(format!( + "Traceroute to {} - reached ({} hops, elapsed {})", + fmt_ip_host(target.ip, &target.hostname), + tr.nodes.len(), + fmt_dur(tr.elapsed_time) + )) } else { - Tree::new(format!("Traceroute to {} - not reached ({} hops, elapsed {})", - fmt_ip_host(target.ip, &target.hostname), tr.nodes.len(), fmt_dur(tr.elapsed_time))) + Tree::new(format!( + "Traceroute to {} - not reached ({} hops, elapsed {})", + fmt_ip_host(target.ip, &target.hostname), + tr.nodes.len(), + fmt_dur(tr.elapsed_time) + )) }; let mut nodes = tr.nodes.clone(); @@ -55,7 +72,10 @@ pub fn print_trace_tree(tr: &TraceResult, target: Host) { if n.mac_addr != MacAddr::zero() && n.node_type == NodeType::Gateway { hop_node.push(Tree::new(format!("MAC: {}", n.mac_addr))); } - hop_node.push(Tree::new(format!("RTT: {:.3}ms", n.rtt.as_secs_f64()*1000.0))); + hop_node.push(Tree::new(format!( + "RTT: {:.3}ms", + n.rtt.as_secs_f64() * 1000.0 + ))); hop_node.push(Tree::new(format!("TTL: {}", n.ttl))); hop_node.push(Tree::new(format!("HOP: {}", n.hop))); hop_node.push(Tree::new(format!("Type: {}", n.node_type.name()))); @@ -63,17 +83,11 @@ pub fn print_trace_tree(tr: &TraceResult, target: Host) { root.push(hop_node); } ProbeStatusKind::Timeout => { - let hop_node = Tree::new(format!( - "#{} timed out", - n.seq - )); + let hop_node = Tree::new(format!("#{} timed out", n.seq)); root.push(hop_node); } _ => { - let hop_node = Tree::new(format!( - "#{} unknown", - n.seq - )); + let hop_node = Tree::new(format!("#{} unknown", n.seq)); root.push(hop_node); } } diff --git a/src/packet/arp.rs b/src/packet/arp.rs index 207a705..5c4413e 100644 --- a/src/packet/arp.rs +++ b/src/packet/arp.rs @@ -16,16 +16,14 @@ pub fn build_arp_packet(interface: &Interface, dst_ip: IpAddr) -> Vec { crate::interface::get_interface_local_ipv6(interface).unwrap_or(Ipv6Addr::UNSPECIFIED); let src_ip: IpAddr = match dst_ip { - IpAddr::V4(_) => { - IpAddr::V4(src_ipv4) - }, + IpAddr::V4(_) => IpAddr::V4(src_ipv4), IpAddr::V6(_) => { if nex::net::ip::is_global_ip(&dst_ip) { IpAddr::V6(src_global_ipv6) } else { IpAddr::V6(src_local_ipv6) } - }, + } }; match src_ip { diff --git a/src/packet/icmp.rs b/src/packet/icmp.rs index 9b21f8d..91575c7 100644 --- a/src/packet/icmp.rs +++ b/src/packet/icmp.rs @@ -29,16 +29,14 @@ pub fn build_icmp_packet(interface: &Interface, dst_ip: IpAddr, is_ip_packet: bo crate::interface::get_interface_local_ipv6(interface).unwrap_or(Ipv6Addr::UNSPECIFIED); let src_ip: IpAddr = match dst_ip { - IpAddr::V4(_) => { - IpAddr::V4(src_ipv4) - }, + IpAddr::V4(_) => IpAddr::V4(src_ipv4), IpAddr::V6(_) => { if nex::net::ip::is_global_ip(&dst_ip) { IpAddr::V6(src_global_ipv6) } else { IpAddr::V6(src_local_ipv6) } - }, + } }; let icmp_packet: Bytes = match (src_ip, dst_ip) { diff --git a/src/packet/mod.rs b/src/packet/mod.rs index d0ff892..49603d8 100644 --- a/src/packet/mod.rs +++ b/src/packet/mod.rs @@ -1,5 +1,5 @@ -pub mod tcp; -pub mod udp; -pub mod icmp; pub mod arp; +pub mod icmp; pub mod ndp; +pub mod tcp; +pub mod udp; diff --git a/src/packet/ndp.rs b/src/packet/ndp.rs index 94574f1..3f27e42 100644 --- a/src/packet/ndp.rs +++ b/src/packet/ndp.rs @@ -31,16 +31,14 @@ pub fn build_ndp_packet(interface: &Interface, dst_ip: IpAddr) -> Vec { crate::interface::get_interface_local_ipv6(interface).unwrap_or(Ipv6Addr::UNSPECIFIED); let src_ip: IpAddr = match dst_ip { - IpAddr::V4(_) => { - IpAddr::V4(src_ipv4) - }, + IpAddr::V4(_) => IpAddr::V4(src_ipv4), IpAddr::V6(_) => { if nex::net::ip::is_global_ip(&dst_ip) { IpAddr::V6(src_global_ipv6) } else { IpAddr::V6(src_local_ipv6) } - }, + } }; // Build NDP packet //let ndp_payload_len = (NDP_SOL_PACKET_LEN + NDP_OPT_PACKET_LEN + MAC_ADDR_LEN) as u16; diff --git a/src/packet/tcp.rs b/src/packet/tcp.rs index 4b58613..42f1789 100644 --- a/src/packet/tcp.rs +++ b/src/packet/tcp.rs @@ -18,7 +18,7 @@ pub fn build_tcp_syn_packet( interface: &Interface, dst_ip: IpAddr, dst_port: u16, - is_ip_packet: bool + is_ip_packet: bool, ) -> Vec { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let dst_mac = match &interface.gateway { @@ -32,16 +32,14 @@ pub fn build_tcp_syn_packet( crate::interface::get_interface_local_ipv6(interface).unwrap_or(Ipv6Addr::UNSPECIFIED); let src_ip: IpAddr = match dst_ip { - IpAddr::V4(_) => { - IpAddr::V4(src_ipv4) - }, + IpAddr::V4(_) => IpAddr::V4(src_ipv4), IpAddr::V6(_) => { if nex::net::ip::is_global_ip(&dst_ip) { IpAddr::V6(src_global_ipv6) } else { IpAddr::V6(src_local_ipv6) } - }, + } }; // Packet builder for TCP SYN diff --git a/src/packet/udp.rs b/src/packet/udp.rs index 7f2ae13..3a6ab3d 100644 --- a/src/packet/udp.rs +++ b/src/packet/udp.rs @@ -14,7 +14,12 @@ use crate::config::default::DEFAULT_LOCAL_UDP_PORT; use crate::trace::TraceSetting; /// Build UDP packet -pub fn build_udp_packet(interface: &Interface, dst_ip: IpAddr, dst_port: u16, is_ip_packet: bool) -> Vec { +pub fn build_udp_packet( + interface: &Interface, + dst_ip: IpAddr, + dst_port: u16, + is_ip_packet: bool, +) -> Vec { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let dst_mac = match &interface.gateway { Some(gateway) => gateway.mac_addr, @@ -27,16 +32,14 @@ pub fn build_udp_packet(interface: &Interface, dst_ip: IpAddr, dst_port: u16, is crate::interface::get_interface_local_ipv6(interface).unwrap_or(Ipv6Addr::UNSPECIFIED); let src_ip: IpAddr = match dst_ip { - IpAddr::V4(_) => { - IpAddr::V4(src_ipv4) - }, + IpAddr::V4(_) => IpAddr::V4(src_ipv4), IpAddr::V6(_) => { if nex::net::ip::is_global_ip(&dst_ip) { IpAddr::V6(src_global_ipv6) } else { IpAddr::V6(src_local_ipv6) } - }, + } }; let udp_packet = UdpPacketBuilder::new(src_ip, dst_ip) @@ -91,7 +94,11 @@ pub fn build_udp_packet(interface: &Interface, dst_ip: IpAddr, dst_port: u16, is } /// Build UDP packet for traceroute with specific TTL -pub fn build_udp_trace_packet(interface: &Interface, trace_setting: &TraceSetting, seq_ttl: u8) -> Vec { +pub fn build_udp_trace_packet( + interface: &Interface, + trace_setting: &TraceSetting, + seq_ttl: u8, +) -> Vec { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let dst_mac = match &interface.gateway { Some(gateway) => gateway.mac_addr, @@ -104,16 +111,14 @@ pub fn build_udp_trace_packet(interface: &Interface, trace_setting: &TraceSettin crate::interface::get_interface_local_ipv6(interface).unwrap_or(Ipv6Addr::UNSPECIFIED); let src_ip: IpAddr = match trace_setting.dst_ip { - IpAddr::V4(_) => { - IpAddr::V4(src_ipv4) - }, + IpAddr::V4(_) => IpAddr::V4(src_ipv4), IpAddr::V6(_) => { if nex::net::ip::is_global_ip(&trace_setting.dst_ip) { IpAddr::V6(src_global_ipv6) } else { IpAddr::V6(src_local_ipv6) } - }, + } }; let dst_port = trace_setting.dst_port.unwrap_or(DEFAULT_LOCAL_UDP_PORT); diff --git a/src/ping/mod.rs b/src/ping/mod.rs index 1381f67..743c836 100644 --- a/src/ping/mod.rs +++ b/src/ping/mod.rs @@ -1,26 +1,33 @@ pub mod pinger; -pub mod setting; -pub mod result; pub mod probe; +pub mod result; +pub mod setting; use std::time::Duration; use anyhow::Result; use netdev::Interface; -use crate::{endpoint::Host, ping::{pinger::Pinger, setting::PingSetting}}; +use crate::{ + endpoint::Host, + ping::{pinger::Pinger, setting::PingSetting}, +}; // Check reachability of the target and measure latency before probing -pub async fn initial_ping(interface: &Interface, dst_host: &Host, port: Option) -> Result { +pub async fn initial_ping( + interface: &Interface, + dst_host: &Host, + port: Option, +) -> Result { match dst_host.hostname { Some(ref name) => { tracing::info!("Performing initial ping to {} ({})", name, dst_host.ip); - }, + } None => { tracing::info!("Performing initial ping to {}", dst_host.ip); } } - + // 1. Try ICMP ping let icmp_setting: PingSetting = PingSetting::icmp_ping(&interface, dst_host.clone(), 1)?; let pinger = Pinger::new(icmp_setting); @@ -29,12 +36,12 @@ pub async fn initial_ping(interface: &Interface, dst_host: &Host, port: Option { tracing::warn!("Initial ICMP ping failed: {}", e); } } - + // 2. Try UDP ping let udp_setting: PingSetting = PingSetting::udp_ping(&interface, dst_host.clone(), 1)?; let pinger = Pinger::new(udp_setting); @@ -43,7 +50,7 @@ pub async fn initial_ping(interface: &Interface, dst_host: &Host, port: Option { tracing::warn!("Initial UDP ping failed: {}", e); } @@ -51,19 +58,19 @@ pub async fn initial_ping(interface: &Interface, dst_host: &Host, port: Option { if let Some(first) = r.first_response() { return Ok(first.rtt); } - }, + } Err(e) => { tracing::warn!("Initial TCP ping failed: {}", e); } } anyhow::bail!("All initial ping methods failed"); - -} \ No newline at end of file +} diff --git a/src/ping/pinger.rs b/src/ping/pinger.rs index e641405..a349c58 100644 --- a/src/ping/pinger.rs +++ b/src/ping/pinger.rs @@ -1,4 +1,7 @@ -use crate::{ping::{result::PingResult, setting::PingSetting}, protocol::Protocol}; +use crate::{ + ping::{result::PingResult, setting::PingSetting}, + protocol::Protocol, +}; use anyhow::Result; /// Pinger structure. @@ -21,9 +24,7 @@ impl Pinger { Protocol::Icmp => super::probe::icmp::run_icmp_ping(&self.ping_setting).await, Protocol::Udp => super::probe::udp::run_udp_ping(&self.ping_setting).await, Protocol::Tcp => super::probe::tcp::run_tcp_ping(&self.ping_setting).await, - _ => { - Err(anyhow::anyhow!("Unsupported protocol")) - }, + _ => Err(anyhow::anyhow!("Unsupported protocol")), } } } diff --git a/src/ping/probe/icmp.rs b/src/ping/probe/icmp.rs index 46bb46c..e8efb43 100644 --- a/src/ping/probe/icmp.rs +++ b/src/ping/probe/icmp.rs @@ -1,18 +1,22 @@ -use std::collections::HashSet; -use std::net::IpAddr; -use std::time::{Duration, Instant}; -use futures::stream::StreamExt; +use crate::endpoint::NodeType; +use crate::ping::result::PingStat; +use crate::probe::{ProbeStatus, ProbeStatusKind}; +use crate::{ + ping::{result::PingResult, setting::PingSetting}, + probe::ProbeResult, + protocol::Protocol, +}; +use anyhow::Result; use futures::future::poll_fn; +use futures::stream::StreamExt; use netdev::MacAddr; +use nex::datalink::async_io::{AsyncChannel, async_channel}; use nex::packet::frame::{Frame, ParseOption}; use nex::packet::icmp::IcmpType; use nex::packet::icmpv6::Icmpv6Type; -use crate::endpoint::NodeType; -use crate::ping::result::PingStat; -use crate::probe::{ProbeStatus, ProbeStatusKind}; -use crate::{ping::{result::PingResult, setting::PingSetting}, probe::ProbeResult, protocol::Protocol}; -use anyhow::Result; -use nex::datalink::async_io::{async_channel, AsyncChannel}; +use std::collections::HashSet; +use std::net::IpAddr; +use std::time::{Duration, Instant}; use tracing_indicatif::span_ext::IndicatifSpanExt; /// Run ICMP Ping and return the results. @@ -37,15 +41,16 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; let mut responses: Vec = Vec::new(); let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { let payload_offset = if interface.is_loopback() { 14 } else { 0 }; parse_option.from_ip_packet = true; parse_option.offset = payload_offset; @@ -57,14 +62,13 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { header_span.pb_set_length(setting.count as u64); header_span.pb_set_position(0); header_span.pb_start(); - + let start_time = Instant::now(); let icmp_packet = crate::packet::icmp::build_icmp_packet(&interface, setting.dst_ip, false); for seq in 1..setting.count + 1 { let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &icmp_packet)).await { - Ok(_) => { - }, + Ok(_) => {} Err(e) => eprintln!("Failed to send packet: {}", e), } loop { @@ -111,7 +115,13 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { sent_packet_size: icmp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("Reply from {}, bytes={} RTT={:?} TTL={}", setting.dst_ip, packet.len(), rtt, ipv4_header.ttl); + tracing::info!( + "Reply from {}, bytes={} RTT={:?} TTL={}", + setting.dst_ip, + packet.len(), + rtt, + ipv4_header.ttl + ); responses.push(probe_result); header_span.pb_inc(1); break; @@ -135,8 +145,9 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { port_number: None, port_status: None, ttl: ipv6_header.hop_limit, - hop: crate::util::ip::initial_ttl(ipv6_header.hop_limit) - - ipv6_header.hop_limit, + hop: crate::util::ip::initial_ttl( + ipv6_header.hop_limit, + ) - ipv6_header.hop_limit, rtt: rtt, probe_status: ProbeStatus::new(), protocol: Protocol::Icmp, @@ -144,7 +155,13 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { sent_packet_size: icmp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("Reply from {}, bytes={} RTT={:?} TTL={}", setting.dst_ip, packet.len(), rtt, ipv6_header.hop_limit); + tracing::info!( + "Reply from {}, bytes={} RTT={:?} TTL={}", + setting.dst_ip, + packet.len(), + rtt, + ipv6_header.hop_limit + ); responses.push(probe_result); header_span.pb_inc(1); break; @@ -153,17 +170,17 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { } } } - }, + } Ok(Some(Err(e))) => { tracing::error!("Failed to receive packet: {}", e); header_span.pb_inc(1); break; - }, + } Ok(None) => { tracing::error!("Channel closed"); header_span.pb_inc(1); break; - }, + } Err(_) => { tracing::error!("Request timeout for seq {}", seq); let probe_result = ProbeResult::timeout( diff --git a/src/ping/probe/tcp.rs b/src/ping/probe/tcp.rs index 903b6c9..645c8dc 100644 --- a/src/ping/probe/tcp.rs +++ b/src/ping/probe/tcp.rs @@ -1,17 +1,21 @@ -use std::collections::HashSet; -use std::net::IpAddr; -use std::time::{Duration, Instant}; -use futures::stream::StreamExt; -use futures::future::poll_fn; -use netdev::MacAddr; -use nex::packet::frame::{Frame, ParseOption}; -use nex::packet::tcp::TcpFlags; use crate::endpoint::{NodeType, PortState}; use crate::ping::result::PingStat; use crate::probe::{ProbeStatus, ProbeStatusKind}; -use crate::{ping::{result::PingResult, setting::PingSetting}, probe::ProbeResult, protocol::Protocol}; +use crate::{ + ping::{result::PingResult, setting::PingSetting}, + probe::ProbeResult, + protocol::Protocol, +}; use anyhow::Result; -use nex::datalink::async_io::{async_channel, AsyncChannel}; +use futures::future::poll_fn; +use futures::stream::StreamExt; +use netdev::MacAddr; +use nex::datalink::async_io::{AsyncChannel, async_channel}; +use nex::packet::frame::{Frame, ParseOption}; +use nex::packet::tcp::TcpFlags; +use std::collections::HashSet; +use std::net::IpAddr; +use std::time::{Duration, Instant}; use tracing_indicatif::span_ext::IndicatifSpanExt; /// Run TCP Ping and return the results. @@ -37,15 +41,16 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; let mut responses: Vec = Vec::new(); let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { let payload_offset = if interface.is_loopback() { 14 } else { 0 }; parse_option.from_ip_packet = true; parse_option.offset = payload_offset; @@ -57,14 +62,14 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { header_span.pb_set_length(setting.count as u64); header_span.pb_set_position(0); header_span.pb_start(); - + let start_time = Instant::now(); - let tcp_packet = crate::packet::tcp::build_tcp_syn_packet(&interface, setting.dst_ip, dst_port, false); + let tcp_packet = + crate::packet::tcp::build_tcp_syn_packet(&interface, setting.dst_ip, dst_port, false); for seq in 1..setting.count + 1 { let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &tcp_packet)).await { - Ok(_) => { - }, + Ok(_) => {} Err(e) => eprintln!("Failed to send packet: {}", e), } loop { @@ -93,7 +98,10 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { { if let Some(transport) = &frame.transport { if let Some(tcp) = &transport.tcp { - let port_state: PortState = if (tcp.flags & (TcpFlags::SYN | TcpFlags::ACK)) == (TcpFlags::SYN | TcpFlags::ACK) { + let port_state: PortState = if (tcp.flags + & (TcpFlags::SYN | TcpFlags::ACK)) + == (TcpFlags::SYN | TcpFlags::ACK) + { PortState::Open } else if (tcp.flags & TcpFlags::RST) != 0 { PortState::Closed @@ -117,7 +125,15 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { sent_packet_size: tcp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("Reply from {}:{}, State={} bytes={} RTT={:?} TTL={}", setting.dst_ip, dst_port, port_state.as_str(), packet.len(), rtt, ipv4_header.ttl); + tracing::info!( + "Reply from {}:{}, State={} bytes={} RTT={:?} TTL={}", + setting.dst_ip, + dst_port, + port_state.as_str(), + packet.len(), + rtt, + ipv4_header.ttl + ); responses.push(probe_result); header_span.pb_inc(1); break; @@ -132,7 +148,10 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { { if let Some(transport) = &frame.transport { if let Some(tcp) = &transport.tcp { - let port_state: PortState = if (tcp.flags & (TcpFlags::SYN | TcpFlags::ACK)) == (TcpFlags::SYN | TcpFlags::ACK) { + let port_state: PortState = if (tcp.flags + & (TcpFlags::SYN | TcpFlags::ACK)) + == (TcpFlags::SYN | TcpFlags::ACK) + { PortState::Open } else if (tcp.flags & TcpFlags::RST) != 0 { PortState::Closed @@ -147,8 +166,9 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { port_number: Some(dst_port), port_status: Some(port_state), ttl: ipv6_header.hop_limit, - hop: crate::util::ip::initial_ttl(ipv6_header.hop_limit) - - ipv6_header.hop_limit, + hop: crate::util::ip::initial_ttl( + ipv6_header.hop_limit, + ) - ipv6_header.hop_limit, rtt: rtt, probe_status: ProbeStatus::new(), protocol: Protocol::Tcp, @@ -156,7 +176,15 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { sent_packet_size: tcp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("Reply from {}:{}, State={} bytes={} RTT={:?} TTL={}", setting.dst_ip, dst_port, port_state.as_str(), packet.len(), rtt, ipv6_header.hop_limit); + tracing::info!( + "Reply from {}:{}, State={} bytes={} RTT={:?} TTL={}", + setting.dst_ip, + dst_port, + port_state.as_str(), + packet.len(), + rtt, + ipv6_header.hop_limit + ); responses.push(probe_result); header_span.pb_inc(1); break; @@ -165,17 +193,17 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { } } } - }, + } Ok(Some(Err(e))) => { tracing::error!("Failed to receive packet: {}", e); header_span.pb_inc(1); break; - }, + } Ok(None) => { tracing::error!("Channel closed"); header_span.pb_inc(1); break; - }, + } Err(_) => { tracing::error!("Request timeout for seq {}", seq); let probe_result = ProbeResult::timeout( diff --git a/src/ping/probe/udp.rs b/src/ping/probe/udp.rs index dec7c5c..50f231f 100644 --- a/src/ping/probe/udp.rs +++ b/src/ping/probe/udp.rs @@ -1,19 +1,23 @@ -use std::collections::HashSet; -use std::net::IpAddr; -use std::time::{Duration, Instant}; -use futures::stream::StreamExt; -use futures::future::poll_fn; -use netdev::MacAddr; -use nex::packet::frame::{Frame, ParseOption}; -use nex::packet::icmp::IcmpType; -use nex::packet::icmpv6::Icmpv6Type; use crate::config::default::DEFAULT_BASE_TARGET_UDP_PORT; use crate::endpoint::NodeType; use crate::ping::result::PingStat; use crate::probe::{ProbeStatus, ProbeStatusKind}; -use crate::{ping::{result::PingResult, setting::PingSetting}, probe::ProbeResult, protocol::Protocol}; +use crate::{ + ping::{result::PingResult, setting::PingSetting}, + probe::ProbeResult, + protocol::Protocol, +}; use anyhow::Result; -use nex::datalink::async_io::{async_channel, AsyncChannel}; +use futures::future::poll_fn; +use futures::stream::StreamExt; +use netdev::MacAddr; +use nex::datalink::async_io::{AsyncChannel, async_channel}; +use nex::packet::frame::{Frame, ParseOption}; +use nex::packet::icmp::IcmpType; +use nex::packet::icmpv6::Icmpv6Type; +use std::collections::HashSet; +use std::net::IpAddr; +use std::time::{Duration, Instant}; use tracing_indicatif::span_ext::IndicatifSpanExt; /// Run UDP Ping and return the results. @@ -38,15 +42,16 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; let mut responses: Vec = Vec::new(); let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { let payload_offset = if interface.is_loopback() { 14 } else { 0 }; parse_option.from_ip_packet = true; parse_option.offset = payload_offset; @@ -58,14 +63,18 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { header_span.pb_set_length(setting.count as u64); header_span.pb_set_position(0); header_span.pb_start(); - + let start_time = Instant::now(); - let udp_packet = crate::packet::udp::build_udp_packet(&interface, setting.dst_ip, DEFAULT_BASE_TARGET_UDP_PORT, false); + let udp_packet = crate::packet::udp::build_udp_packet( + &interface, + setting.dst_ip, + DEFAULT_BASE_TARGET_UDP_PORT, + false, + ); for seq in 1..setting.count + 1 { let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &udp_packet)).await { - Ok(_) => { - }, + Ok(_) => {} Err(e) => eprintln!("Failed to send packet: {}", e), } loop { @@ -112,7 +121,13 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { sent_packet_size: udp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("Reply from {}, bytes={} RTT={:?} TTL={}", setting.dst_ip, packet.len(), rtt, ipv4_header.ttl); + tracing::info!( + "Reply from {}, bytes={} RTT={:?} TTL={}", + setting.dst_ip, + packet.len(), + rtt, + ipv4_header.ttl + ); responses.push(probe_result); header_span.pb_inc(1); break; @@ -127,7 +142,9 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { { // ICMPv6 if let Some(icmpv6_header) = &ip_layer.icmpv6 { - if icmpv6_header.icmpv6_type == Icmpv6Type::DestinationUnreachable { + if icmpv6_header.icmpv6_type + == Icmpv6Type::DestinationUnreachable + { let probe_result: ProbeResult = ProbeResult { seq: seq, mac_addr: mac_addr, @@ -136,8 +153,9 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { port_number: None, port_status: None, ttl: ipv6_header.hop_limit, - hop: crate::util::ip::initial_ttl(ipv6_header.hop_limit) - - ipv6_header.hop_limit, + hop: crate::util::ip::initial_ttl( + ipv6_header.hop_limit, + ) - ipv6_header.hop_limit, rtt: rtt, probe_status: ProbeStatus::new(), protocol: Protocol::Udp, @@ -145,7 +163,13 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { sent_packet_size: udp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("Reply from {}, bytes={} RTT={:?} TTL={}", setting.dst_ip, packet.len(), rtt, ipv6_header.hop_limit); + tracing::info!( + "Reply from {}, bytes={} RTT={:?} TTL={}", + setting.dst_ip, + packet.len(), + rtt, + ipv6_header.hop_limit + ); responses.push(probe_result); header_span.pb_inc(1); break; @@ -154,17 +178,17 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { } } } - }, + } Ok(Some(Err(e))) => { tracing::error!("Failed to receive packet: {}", e); header_span.pb_inc(1); break; - }, + } Ok(None) => { tracing::error!("Channel closed"); header_span.pb_inc(1); break; - }, + } Err(_) => { tracing::error!("Request timeout for seq {}", seq); let probe_result = ProbeResult::timeout( diff --git a/src/ping/result.rs b/src/ping/result.rs index 563154d..1508126 100644 --- a/src/ping/result.rs +++ b/src/ping/result.rs @@ -1,6 +1,12 @@ -use crate::{probe::{ProbeResult, ProbeStatus, ProbeStatusKind}, protocol::Protocol}; +use crate::{ + probe::{ProbeResult, ProbeStatus, ProbeStatusKind}, + protocol::Protocol, +}; use serde::{Deserialize, Serialize}; -use std::{net::{IpAddr, Ipv4Addr}, time::Duration}; +use std::{ + net::{IpAddr, Ipv4Addr}, + time::Duration, +}; /// Statistics of ping results #[derive(Serialize, Deserialize, Clone, Debug)] @@ -61,7 +67,10 @@ impl PingResult { } /// Return first successful response pub fn first_response(&self) -> Option<&ProbeResult> { - self.stat.responses.iter().find(|r| r.probe_status.kind == ProbeStatusKind::Done) + self.stat + .responses + .iter() + .find(|r| r.probe_status.kind == ProbeStatusKind::Done) } } diff --git a/src/ping/setting.rs b/src/ping/setting.rs index 25b0e70..042b881 100644 --- a/src/ping/setting.rs +++ b/src/ping/setting.rs @@ -1,11 +1,11 @@ -use std::net::Ipv4Addr; -use std::{net::IpAddr, time::Duration}; -use anyhow::Result; -use netdev::Interface; -use serde::{Deserialize, Serialize}; use crate::config::default::{DEFAULT_BASE_TARGET_UDP_PORT, DEFAULT_HOP_LIMIT, DEFAULT_PING_COUNT}; use crate::endpoint::Host; use crate::protocol::Protocol; +use anyhow::Result; +use netdev::Interface; +use serde::{Deserialize, Serialize}; +use std::net::Ipv4Addr; +use std::{net::IpAddr, time::Duration}; /// Settings for a ping operation #[derive(Deserialize, Serialize, Clone, Debug)] @@ -45,11 +45,7 @@ impl Default for PingSetting { impl PingSetting { /// Create a new ICMP ping setting - pub fn icmp_ping( - interface: &Interface, - dst_host: Host, - count: u32, - ) -> Result { + pub fn icmp_ping(interface: &Interface, dst_host: Host, count: u32) -> Result { let use_tun = interface.is_tun(); let loopback = interface.is_loopback(); @@ -96,11 +92,7 @@ impl PingSetting { Ok(setting) } /// Create a new UDP ping setting - pub fn udp_ping( - interface: &Interface, - dst_host: Host, - count: u32, - ) -> Result { + pub fn udp_ping(interface: &Interface, dst_host: Host, count: u32) -> Result { let use_tun = interface.is_tun(); let loopback = interface.is_loopback(); diff --git a/src/protocol.rs b/src/protocol.rs index 44fb3c5..62b272b 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -1,15 +1,15 @@ -use serde::{Deserialize, Serialize}; use clap::ValueEnum; +use serde::{Deserialize, Serialize}; /// Supported protocols for probing #[derive(Deserialize, Serialize, Copy, Clone, Debug, ValueEnum, Eq, PartialEq)] -pub enum Protocol { - Icmp, - Udp, +pub enum Protocol { + Icmp, + Udp, Tcp, Quic, Arp, - Ndp + Ndp, } impl Protocol { diff --git a/src/scan/mod.rs b/src/scan/mod.rs index ded2588..d096b2a 100644 --- a/src/scan/mod.rs +++ b/src/scan/mod.rs @@ -1,6 +1,11 @@ use anyhow::Result; -use crate::{cli::{HostScanProto, PortScanMethod}, endpoint::TransportProtocol, output::ScanResult, probe::ProbeSetting}; +use crate::{ + cli::{HostScanProto, PortScanMethod}, + endpoint::TransportProtocol, + output::ScanResult, + probe::ProbeSetting, +}; pub mod probe; @@ -13,7 +18,11 @@ pub struct PortScanner { impl PortScanner { /// Create a new PortScanner instance. - pub fn new(settings: ProbeSetting, transport: TransportProtocol, scan_method: PortScanMethod) -> Self { + pub fn new( + settings: ProbeSetting, + transport: TransportProtocol, + scan_method: PortScanMethod, + ) -> Self { Self { settings, scan_method, @@ -23,8 +32,12 @@ impl PortScanner { /// Run the port scan based on the specified transport protocol and method. pub async fn run(&self) -> Result { match self.transport { - TransportProtocol::Tcp => probe::tcp::run_port_scan(self.settings.clone(), self.scan_method).await, - TransportProtocol::Quic => probe::quic::run_port_scan(self.settings.clone(), self.scan_method).await, + TransportProtocol::Tcp => { + probe::tcp::run_port_scan(self.settings.clone(), self.scan_method).await + } + TransportProtocol::Quic => { + probe::quic::run_port_scan(self.settings.clone(), self.scan_method).await + } _ => anyhow::bail!("Unsupported transport protocol: {:?}", self.transport), } } diff --git a/src/scan/probe/icmp.rs b/src/scan/probe/icmp.rs index 3372e3b..026a174 100644 --- a/src/scan/probe/icmp.rs +++ b/src/scan/probe/icmp.rs @@ -1,16 +1,16 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use crate::capture::pcap::PacketCaptureOptions; +use crate::endpoint::{EndpointResult, OsGuess}; +use crate::{output::ScanResult, scan::ProbeSetting}; +use anyhow::Result; use futures::future::poll_fn; use netdev::{Interface, MacAddr}; -use nex::datalink::async_io::{async_channel, AsyncChannel, AsyncRawSender}; +use nex::datalink::async_io::{AsyncChannel, AsyncRawSender, async_channel}; use nex::packet::frame::Frame; use nex::packet::ip::IpNextProtocol; use tracing_indicatif::span_ext::IndicatifSpanExt; -use anyhow::Result; -use crate::{output::ScanResult, scan::ProbeSetting}; -use crate::capture::pcap::PacketCaptureOptions; -use crate::endpoint::{EndpointResult, OsGuess}; /// Send ICMP Echo Request packets to the specified target endpoints pub async fn send_hostscan_packets( @@ -33,7 +33,7 @@ pub async fn send_hostscan_packets( if !scan_setting.send_rate.is_zero() { tokio::time::sleep(scan_setting.send_rate).await; } - }, + } Err(e) => eprintln!("Failed to send packet: {}", e), } header_span.pb_inc(1); @@ -59,8 +59,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; @@ -86,18 +85,11 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { capture_options.ip_protocols.insert(IpNextProtocol::Icmp); capture_options.ip_protocols.insert(IpNextProtocol::Icmpv6); - let (ready_tx, ready_rx) = tokio::sync::oneshot::channel(); let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel(); let capture_handle: tokio::task::JoinHandle<_> = tokio::spawn(async move { - crate::capture::pcap::start_capture( - &mut rx, - capture_options, - ready_tx, - &mut stop_rx, - ) - .await + crate::capture::pcap::start_capture(&mut rx, capture_options, ready_tx, &mut stop_rx).await }); // Wait for listener to start @@ -154,7 +146,7 @@ fn parse_hostscan_result( if if_ipv4_set.contains(&ipv4_packet.source) { mac_addr = iface.mac_addr.unwrap_or(MacAddr::zero()); ttl = crate::util::ip::initial_ttl(ipv4_packet.ttl); - }else{ + } else { ttl = ipv4_packet.ttl; } ip_addr = IpAddr::V4(ipv4_packet.source); @@ -162,7 +154,7 @@ fn parse_hostscan_result( if if_ipv6_set.contains(&ipv6_packet.source) { mac_addr = iface.mac_addr.unwrap_or(MacAddr::zero()); ttl = crate::util::ip::initial_ttl(ipv6_packet.hop_limit); - }else { + } else { ttl = ipv6_packet.hop_limit; } ip_addr = IpAddr::V6(ipv6_packet.source); @@ -184,21 +176,18 @@ fn parse_hostscan_result( vendor_name_opt = None; } - endpoint_map - .entry(ip_addr) - .or_insert(EndpointResult { - ip: ip_addr, - hostname: dns_map.get(&ip_addr).cloned(), - ports: BTreeMap::new(), - mac_addr: Some(mac_addr), - vendor_name: vendor_name_opt, - os: OsGuess::default().with_ttl_observed(ttl), - tags: Vec::new(), - cpes: Vec::new(), - }); + endpoint_map.entry(ip_addr).or_insert(EndpointResult { + ip: ip_addr, + hostname: dns_map.get(&ip_addr).cloned(), + ports: BTreeMap::new(), + mac_addr: Some(mac_addr), + vendor_name: vendor_name_opt, + os: OsGuess::default().with_ttl_observed(ttl), + tags: Vec::new(), + cpes: Vec::new(), + }); result.fingerprints.push(p.clone()); - } for (_ip, endpoint) in endpoint_map { result.endpoints.push(endpoint); diff --git a/src/scan/probe/mod.rs b/src/scan/probe/mod.rs index 5a350cd..5cd9fd2 100644 --- a/src/scan/probe/mod.rs +++ b/src/scan/probe/mod.rs @@ -1,4 +1,4 @@ +pub mod icmp; +pub mod quic; pub mod tcp; pub mod udp; -pub mod quic; -pub mod icmp; diff --git a/src/scan/probe/quic.rs b/src/scan/probe/quic.rs index 5a00471..a2cd98a 100644 --- a/src/scan/probe/quic.rs +++ b/src/scan/probe/quic.rs @@ -1,10 +1,18 @@ use std::{collections::BTreeMap, time::Duration}; +use crate::{ + cli::PortScanMethod, + endpoint::{ + EndpointResult, OsGuess, Port, PortResult, PortState, ServiceInfo, TransportProtocol, + }, + output::ScanResult, + scan::ProbeSetting, + service::probe::quic::quic_client_config, +}; use anyhow::Result; use futures::StreamExt; use tokio::sync::mpsc; use tracing_indicatif::span_ext::IndicatifSpanExt; -use crate::{cli::PortScanMethod, endpoint::{EndpointResult, OsGuess, Port, PortResult, PortState, ServiceInfo, TransportProtocol}, output::ScanResult, scan::ProbeSetting, service::probe::quic::quic_client_config}; /// Try to connect to the given ports on the target endpoint using QUIC protocol. /// Concurrency specifies the number of concurrent connection attempts. @@ -14,10 +22,15 @@ pub async fn try_connect_ports( timeout: Duration, ) -> Result { let alpn: [&[u8]; 8] = [ - b"h3".as_slice(), - b"h3-34".as_slice(), b"h3-33".as_slice(), b"h3-32".as_slice(), b"h3-31".as_slice(), b"h3-30".as_slice(), b"h3-29".as_slice(), - b"hq-29".as_slice(), - ]; + b"h3".as_slice(), + b"h3-34".as_slice(), + b"h3-33".as_slice(), + b"h3-32".as_slice(), + b"h3-31".as_slice(), + b"h3-30".as_slice(), + b"h3-29".as_slice(), + b"hq-29".as_slice(), + ]; let (ch_tx, mut ch_rx) = mpsc::unbounded_channel::(); let header_span = tracing::info_span!("quic_connect_scan"); header_span.pb_set_style(&crate::output::progress::get_progress_style()); @@ -37,65 +50,75 @@ pub async fn try_connect_ports( open_ports }); - let hostname = target.hostname.clone().unwrap_or_else(|| target.ip.to_string()); - let prod = futures::stream::iter(target.socket_addrs(TransportProtocol::Quic)).for_each_concurrent(concurrency, move |socket_addr| { - let ch_tx = ch_tx.clone(); - let hostname = hostname.clone(); - let client_cfg = quic_client_config(true, &alpn).unwrap(); + let hostname = target + .hostname + .clone() + .unwrap_or_else(|| target.ip.to_string()); + let prod = futures::stream::iter(target.socket_addrs(TransportProtocol::Quic)) + .for_each_concurrent(concurrency, move |socket_addr| { + let ch_tx = ch_tx.clone(); + let hostname = hostname.clone(); + let client_cfg = quic_client_config(true, &alpn).unwrap(); - async move { - let mut endpoint = match quinn::Endpoint::client((if target.ip.is_ipv6() { "[::]:0" } else { "0.0.0.0:0" }).parse().unwrap()) { - Ok(ep) => ep, - Err(_) => return, - }; - endpoint.set_default_client_config(client_cfg.clone()); - let connect_fut = match endpoint.connect(socket_addr, hostname.as_str()) { - Ok(connecting) => { - connecting - } - Err(e) => { - tracing::error!("Failed to connect to {}: {}", socket_addr, e); - return; - } - }; - let mut port_result = PortResult { - port: Port::new(socket_addr.port(), TransportProtocol::Udp), - state: PortState::Closed, - service: ServiceInfo::default(), - rtt_ms: None, - }; - match tokio::time::timeout(timeout, connect_fut).await { - Ok(quinn_conn) => { - match quinn_conn { - Ok(conn) => { - // Connection succeeded - port_result.state = PortState::Open; - conn.close(0u32.into(), b"Connection closed by client"); - } - Err(e) => { - match e { - quinn::ConnectionError::VersionMismatch - | quinn::ConnectionError::TransportError(_) - | quinn::ConnectionError::ConnectionClosed(_) - | quinn::ConnectionError::ApplicationClosed(_) - | quinn::ConnectionError::Reset => { - // Error, but QUIC service is still running - // So we classify it as open - port_result.state = PortState::Open; - }, - _ => {}, + async move { + let mut endpoint = match quinn::Endpoint::client( + (if target.ip.is_ipv6() { + "[::]:0" + } else { + "0.0.0.0:0" + }) + .parse() + .unwrap(), + ) { + Ok(ep) => ep, + Err(_) => return, + }; + endpoint.set_default_client_config(client_cfg.clone()); + let connect_fut = match endpoint.connect(socket_addr, hostname.as_str()) { + Ok(connecting) => connecting, + Err(e) => { + tracing::error!("Failed to connect to {}: {}", socket_addr, e); + return; + } + }; + let mut port_result = PortResult { + port: Port::new(socket_addr.port(), TransportProtocol::Udp), + state: PortState::Closed, + service: ServiceInfo::default(), + rtt_ms: None, + }; + match tokio::time::timeout(timeout, connect_fut).await { + Ok(quinn_conn) => { + match quinn_conn { + Ok(conn) => { + // Connection succeeded + port_result.state = PortState::Open; + conn.close(0u32.into(), b"Connection closed by client"); + } + Err(e) => { + match e { + quinn::ConnectionError::VersionMismatch + | quinn::ConnectionError::TransportError(_) + | quinn::ConnectionError::ConnectionClosed(_) + | quinn::ConnectionError::ApplicationClosed(_) + | quinn::ConnectionError::Reset => { + // Error, but QUIC service is still running + // So we classify it as open + port_result.state = PortState::Open; + } + _ => {} + } } } } + Err(e) => { + // Timeout + tracing::error!("Failed to connect to {}: {}", socket_addr, e); + } } - Err(e) => { - // Timeout - tracing::error!("Failed to connect to {}: {}", socket_addr, e); - } + let _ = ch_tx.send(port_result); } - let _ = ch_tx.send(port_result); - } - }); + }); let prod_task = tokio::spawn(prod); let (results_res, _prod_res) = tokio::join!(recv_task, prod_task); @@ -117,33 +140,25 @@ pub async fn try_connect_ports( } /// Run a QUIC connect scan based on the provided probe settings. -pub async fn run_connect_scan( - setting: ProbeSetting, -) -> Result { +pub async fn run_connect_scan(setting: ProbeSetting) -> Result { let start_time = std::time::Instant::now(); let mut tasks = vec![]; for target in setting.target_endpoints { tasks.push(tokio::spawn(async move { - let host = try_connect_ports( - target, - setting.port_concurrency, - setting.connect_timeout, - ) - .await; + let host = + try_connect_ports(target, setting.port_concurrency, setting.connect_timeout).await; host })); } let mut endpoints: Vec = vec![]; for task in tasks { match task.await { - Ok(endpoint) => { - match endpoint { - Ok(ep) => endpoints.push(ep), - Err(e) => { - tracing::error!("Failed to connect to endpoint: {}", e); - } + Ok(endpoint) => match endpoint { + Ok(ep) => endpoints.push(ep), + Err(e) => { + tracing::error!("Failed to connect to endpoint: {}", e); } - } + }, Err(e) => { tracing::error!("Failed to join task: {}", e); } @@ -157,9 +172,6 @@ pub async fn run_connect_scan( } /// Run a QUIC port scan using the specified probe settings and method. -pub async fn run_port_scan( - setting: ProbeSetting, - _method: PortScanMethod, -) -> Result { +pub async fn run_port_scan(setting: ProbeSetting, _method: PortScanMethod) -> Result { run_connect_scan(setting).await } diff --git a/src/scan/probe/tcp.rs b/src/scan/probe/tcp.rs index f50085d..338f330 100644 --- a/src/scan/probe/tcp.rs +++ b/src/scan/probe/tcp.rs @@ -1,22 +1,24 @@ -use futures::stream::{self, StreamExt}; +use anyhow::Result; use futures::future::poll_fn; +use futures::stream::{self, StreamExt}; use netdev::{Interface, MacAddr}; -use nex::datalink::async_io::{async_channel, AsyncChannel, AsyncRawSender}; +use nex::datalink::async_io::{AsyncChannel, AsyncRawSender, async_channel}; use nex::packet::frame::Frame; use nex::packet::ip::IpNextProtocol; use nex::packet::tcp::TcpFlags; use nex::socket::tcp::{AsyncTcpSocket, TcpConfig}; -use tracing_indicatif::span_ext::IndicatifSpanExt; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; use std::time::Duration; use tokio::io::AsyncWriteExt; -use std::collections::{BTreeMap, HashMap, HashSet}; -use anyhow::Result; use tokio::sync::mpsc; +use tracing_indicatif::span_ext::IndicatifSpanExt; use crate::capture::pcap::PacketCaptureOptions; -use crate::cli::{PortScanMethod}; -use crate::endpoint::{Endpoint, EndpointResult, OsGuess, Port, PortResult, PortState, ServiceInfo, TransportProtocol}; +use crate::cli::PortScanMethod; +use crate::endpoint::{ + Endpoint, EndpointResult, OsGuess, Port, PortResult, PortState, ServiceInfo, TransportProtocol, +}; use crate::output::ScanResult; use crate::probe::ProbeSetting; @@ -46,34 +48,37 @@ pub async fn try_connect_ports( open_ports }); - let prod = stream::iter(target.socket_addrs(TransportProtocol::Tcp)).for_each_concurrent(concurrency, move |socket_addr| { - let ch_tx = ch_tx.clone(); - async move { - let cfg = if socket_addr.is_ipv4() { - TcpConfig::v4_stream() - } else { - TcpConfig::v6_stream() - }; - let socket = AsyncTcpSocket::from_config(&cfg).unwrap(); - let mut port_result = PortResult { - port: Port::new(socket_addr.port(), TransportProtocol::Tcp), - state: PortState::Closed, - service: ServiceInfo::default(), - rtt_ms: None, - }; - match socket.connect_timeout(socket_addr, timeout).await { - Ok(mut stream) => { - port_result.state = PortState::Open; - match stream.shutdown().await { - Ok(_) => {} - Err(_) => {} + let prod = stream::iter(target.socket_addrs(TransportProtocol::Tcp)).for_each_concurrent( + concurrency, + move |socket_addr| { + let ch_tx = ch_tx.clone(); + async move { + let cfg = if socket_addr.is_ipv4() { + TcpConfig::v4_stream() + } else { + TcpConfig::v6_stream() + }; + let socket = AsyncTcpSocket::from_config(&cfg).unwrap(); + let mut port_result = PortResult { + port: Port::new(socket_addr.port(), TransportProtocol::Tcp), + state: PortState::Closed, + service: ServiceInfo::default(), + rtt_ms: None, + }; + match socket.connect_timeout(socket_addr, timeout).await { + Ok(mut stream) => { + port_result.state = PortState::Open; + match stream.shutdown().await { + Ok(_) => {} + Err(_) => {} + } } + Err(_) => {} } - Err(_) => {} + let _ = ch_tx.send(port_result); } - let _ = ch_tx.send(port_result); - } - }); + }, + ); let prod_task = tokio::spawn(prod); let (results_res, _prod_res) = tokio::join!(recv_task, prod_task); let open_ports = results_res?; @@ -92,35 +97,27 @@ pub async fn try_connect_ports( } /// Run a TCP connect scan based on the provided probe settings. -pub async fn run_connect_scan( - setting: ProbeSetting, -) -> Result { +pub async fn run_connect_scan(setting: ProbeSetting) -> Result { let start_time = std::time::Instant::now(); let mut tasks = vec![]; for target in setting.target_endpoints { tasks.push(tokio::spawn(async move { - let host = try_connect_ports( - target, - setting.port_concurrency, - setting.connect_timeout, - ) - .await; + let host = + try_connect_ports(target, setting.port_concurrency, setting.connect_timeout).await; host })); } let mut endpoints: Vec = vec![]; for task in tasks { match task.await { - Ok(endpoint_result) => { - match endpoint_result { - Ok(endpoint) => { - endpoints.push(endpoint); - } - Err(e) => { - tracing::error!("error: {}", e); - } + Ok(endpoint_result) => match endpoint_result { + Ok(endpoint) => { + endpoints.push(endpoint); } - } + Err(e) => { + tracing::error!("error: {}", e); + } + }, Err(e) => { tracing::error!("error: {}", e); } @@ -147,7 +144,7 @@ pub async fn send_portscan_packets( header_span.pb_set_length(target.ports.len() as u64); header_span.pb_set_position(0); header_span.pb_start(); - + for port in &target.ports { let packet = crate::packet::tcp::build_tcp_syn_packet(&interface, target.ip, port.number, false); @@ -180,7 +177,7 @@ pub async fn send_hostscan_packets( header_span.pb_set_length(scan_setting.target_endpoints.len() as u64); header_span.pb_set_position(0); header_span.pb_start(); - + for target in &scan_setting.target_endpoints { for port in &target.ports { let packet = @@ -202,9 +199,7 @@ pub async fn send_hostscan_packets( } /// Run a TCP SYN scan based on the provided probe settings. -pub async fn run_syn_scan( - setting: ProbeSetting, -) -> Result { +pub async fn run_syn_scan(setting: ProbeSetting) -> Result { let interface = match crate::interface::get_interface_by_index(setting.if_index) { Some(interface) => interface, None => return Err(anyhow::anyhow!("Interface not found")), @@ -221,8 +216,7 @@ pub async fn run_syn_scan( promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; @@ -244,7 +238,9 @@ pub async fn run_syn_scan( }; for endpoint in setting.target_endpoints.clone() { capture_options.src_ips.insert(endpoint.ip); - capture_options.src_ports.extend(endpoint.ports.iter().map(|p| p.number)); + capture_options + .src_ports + .extend(endpoint.ports.iter().map(|p| p.number)); } capture_options.ip_protocols.insert(IpNextProtocol::Tcp); @@ -252,13 +248,7 @@ pub async fn run_syn_scan( let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel(); let capture_handle: tokio::task::JoinHandle<_> = tokio::spawn(async move { - crate::capture::pcap::start_capture( - &mut rx, - capture_options, - ready_tx, - &mut stop_rx, - ) - .await + crate::capture::pcap::start_capture(&mut rx, capture_options, ready_tx, &mut stop_rx).await }); // Wait for listener to start @@ -277,10 +267,7 @@ pub async fn run_syn_scan( } /// Run a TCP port scan using the specified probe settings and method. -pub async fn run_port_scan( - setting: ProbeSetting, - method: PortScanMethod -) -> Result { +pub async fn run_port_scan(setting: ProbeSetting, method: PortScanMethod) -> Result { match method { PortScanMethod::Connect => { return run_connect_scan(setting).await; @@ -309,8 +296,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; @@ -332,7 +318,9 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { }; for endpoint in &setting.target_endpoints { capture_options.src_ips.insert(endpoint.ip); - capture_options.src_ports.extend(endpoint.ports.iter().map(|p| p.number)); + capture_options + .src_ports + .extend(endpoint.ports.iter().map(|p| p.number)); } capture_options.ip_protocols.insert(IpNextProtocol::Tcp); @@ -340,13 +328,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel(); let capture_handle: tokio::task::JoinHandle<_> = tokio::spawn(async move { - crate::capture::pcap::start_capture( - &mut rx, - capture_options, - ready_tx, - &mut stop_rx, - ) - .await + crate::capture::pcap::start_capture(&mut rx, capture_options, ready_tx, &mut stop_rx).await }); // Wait for listener to start @@ -408,7 +390,7 @@ fn parse_portscan_result( } if let Some(transport) = &p.transport { if let Some(tcp_packet) = &transport.tcp { - if socket_set.contains(&SocketAddr::new(ip_addr,tcp_packet.source)) { + if socket_set.contains(&SocketAddr::new(ip_addr, tcp_packet.source)) { continue; } let f = tcp_packet.flags; @@ -452,7 +434,6 @@ fn parse_portscan_result( result.fingerprints.push(p.clone()); socket_set.insert(SocketAddr::new(ip_addr, port.port.number)); - } for (ip, endpoint) in endpoint_map { let mut ep = EndpointResult::new(ip); @@ -506,7 +487,7 @@ fn parse_hostscan_result( if if_ipv4_set.contains(&ipv4_packet.source) { mac_addr = iface.mac_addr.unwrap_or(MacAddr::zero()); ttl = crate::util::ip::initial_ttl(ipv4_packet.ttl); - }else{ + } else { ttl = ipv4_packet.ttl; } ip_addr = IpAddr::V4(ipv4_packet.source); @@ -514,7 +495,7 @@ fn parse_hostscan_result( if if_ipv6_set.contains(&ipv6_packet.source) { mac_addr = iface.mac_addr.unwrap_or(MacAddr::zero()); ttl = crate::util::ip::initial_ttl(ipv6_packet.hop_limit); - }else { + } else { ttl = ipv6_packet.hop_limit; } ip_addr = IpAddr::V6(ipv6_packet.source); @@ -526,7 +507,7 @@ fn parse_hostscan_result( } if let Some(transport) = &p.transport { if let Some(tcp_packet) = &transport.tcp { - if socket_set.contains(&SocketAddr::new(ip_addr,tcp_packet.source)) { + if socket_set.contains(&SocketAddr::new(ip_addr, tcp_packet.source)) { continue; } let f = tcp_packet.flags; @@ -590,7 +571,6 @@ fn parse_hostscan_result( result.fingerprints.push(p.clone()); socket_set.insert(SocketAddr::new(ip_addr, port.port.number)); - } for (ip, endpoint) in endpoint_map { let mut ep = EndpointResult::new(ip); diff --git a/src/scan/probe/udp.rs b/src/scan/probe/udp.rs index 4235f1f..7651c04 100644 --- a/src/scan/probe/udp.rs +++ b/src/scan/probe/udp.rs @@ -1,16 +1,18 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; +use crate::capture::pcap::PacketCaptureOptions; +use crate::endpoint::{EndpointResult, OsGuess}; +use crate::{ + config::default::DEFAULT_BASE_TARGET_UDP_PORT, output::ScanResult, probe::ProbeSetting, +}; +use anyhow::Result; use futures::future::poll_fn; use netdev::{Interface, MacAddr}; -use nex::datalink::async_io::{async_channel, AsyncChannel, AsyncRawSender}; +use nex::datalink::async_io::{AsyncChannel, AsyncRawSender, async_channel}; use nex::packet::frame::Frame; use nex::packet::ip::IpNextProtocol; use tracing_indicatif::span_ext::IndicatifSpanExt; -use anyhow::Result; -use crate::{config::default::DEFAULT_BASE_TARGET_UDP_PORT, output::ScanResult, probe::ProbeSetting}; -use crate::capture::pcap::PacketCaptureOptions; -use crate::endpoint::{EndpointResult, OsGuess}; /// Send UDP packets for host scanning. pub async fn send_hostscan_packets( @@ -26,14 +28,19 @@ pub async fn send_hostscan_packets( header_span.pb_start(); for target in &scan_setting.target_endpoints { - let packet = crate::packet::udp::build_udp_packet(&interface, target.ip, DEFAULT_BASE_TARGET_UDP_PORT, false); + let packet = crate::packet::udp::build_udp_packet( + &interface, + target.ip, + DEFAULT_BASE_TARGET_UDP_PORT, + false, + ); // Send a packet using poll_fn. match poll_fn(|cx| tx.poll_send(cx, &packet)).await { Ok(_) => { if !scan_setting.send_rate.is_zero() { tokio::time::sleep(scan_setting.send_rate).await; } - }, + } Err(e) => eprintln!("Failed to send packet: {}", e), } header_span.pb_inc(1); @@ -59,8 +66,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; @@ -91,13 +97,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel(); let capture_handle: tokio::task::JoinHandle<_> = tokio::spawn(async move { - crate::capture::pcap::start_capture( - &mut rx, - capture_options, - ready_tx, - &mut stop_rx, - ) - .await + crate::capture::pcap::start_capture(&mut rx, capture_options, ready_tx, &mut stop_rx).await }); // Wait for listener to start @@ -154,7 +154,7 @@ fn parse_hostscan_result( if if_ipv4_set.contains(&ipv4_packet.source) { mac_addr = iface.mac_addr.unwrap_or(MacAddr::zero()); ttl = crate::util::ip::initial_ttl(ipv4_packet.ttl); - }else{ + } else { ttl = ipv4_packet.ttl; } ip_addr = IpAddr::V4(ipv4_packet.source); @@ -162,7 +162,7 @@ fn parse_hostscan_result( if if_ipv6_set.contains(&ipv6_packet.source) { mac_addr = iface.mac_addr.unwrap_or(MacAddr::zero()); ttl = crate::util::ip::initial_ttl(ipv6_packet.hop_limit); - }else { + } else { ttl = ipv6_packet.hop_limit; } ip_addr = IpAddr::V6(ipv6_packet.source); @@ -184,21 +184,18 @@ fn parse_hostscan_result( vendor_name_opt = None; } - endpoint_map - .entry(ip_addr) - .or_insert(EndpointResult { - ip: ip_addr, - hostname: dns_map.get(&ip_addr).cloned(), - ports: BTreeMap::new(), - mac_addr: Some(mac_addr), - vendor_name: vendor_name_opt, - os: OsGuess::default().with_ttl_observed(ttl), - tags: Vec::new(), - cpes: Vec::new(), - }); + endpoint_map.entry(ip_addr).or_insert(EndpointResult { + ip: ip_addr, + hostname: dns_map.get(&ip_addr).cloned(), + ports: BTreeMap::new(), + mac_addr: Some(mac_addr), + vendor_name: vendor_name_opt, + os: OsGuess::default().with_ttl_observed(ttl), + tags: Vec::new(), + cpes: Vec::new(), + }); result.fingerprints.push(p.clone()); - } for (_ip, endpoint) in endpoint_map { result.endpoints.push(endpoint); diff --git a/src/service/mod.rs b/src/service/mod.rs index 82c4dea..704f9bc 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -1,18 +1,25 @@ -use std::time::Duration; -use regex::{Regex, RegexBuilder}; use anyhow::{Result, bail}; use futures::stream::{self, StreamExt}; -use tokio::{io::{AsyncRead, AsyncReadExt}, net::TcpStream, time::{timeout, Instant}}; +use regex::{Regex, RegexBuilder}; +use std::time::Duration; use tokio::sync::mpsc; +use tokio::{ + io::{AsyncRead, AsyncReadExt}, + net::TcpStream, + time::{Instant, timeout}, +}; use tracing_indicatif::span_ext::IndicatifSpanExt; -use crate::{endpoint::Endpoint, service::probe::{PortProbe, PortProbeResult, ProbeContext, ServiceProbe}}; +use crate::{ + endpoint::Endpoint, + service::probe::{PortProbe, PortProbeResult, ProbeContext, ServiceProbe}, +}; -pub mod probe; mod payload; +pub mod probe; /// Configuration for service probing -#[derive(Clone,Debug)] +#[derive(Clone, Debug)] pub struct ServiceProbeConfig { pub timeout: Duration, pub max_concurrency: usize, @@ -35,16 +42,17 @@ pub struct ServiceDetector { impl ServiceDetector { /// Create a new ServiceDetector with the given configuration pub fn new(config: ServiceProbeConfig) -> Self { - ServiceDetector { - config - } + ServiceDetector { config } } /// Detect services on the given endpoint using configured probes - pub async fn detect_services(config: ServiceProbeConfig, endpoint: Endpoint) -> Result> { + pub async fn detect_services( + config: ServiceProbeConfig, + endpoint: Endpoint, + ) -> Result> { let port_probe_db = crate::db::service::port_probe_db(); - let service_probe_db = crate::db::service::service_probe_db(); + let service_probe_db = crate::db::service::service_probe_db(); let (ch_tx, mut ch_rx) = mpsc::unbounded_channel::>>(); - + let header_span = tracing::info_span!("detect_services"); header_span.pb_set_style(&crate::output::progress::get_progress_style()); header_span.pb_set_message(&format!("Service Probe ({})", endpoint.ip)); @@ -84,7 +92,8 @@ impl ServiceDetector { let probe_payload = match service_probe_db.get(&probe) { Some(payload) => payload, None => { - results.push(Err(anyhow::anyhow!("No payload for probe {:?}", probe))); + results + .push(Err(anyhow::anyhow!("No payload for probe {:?}", probe))); continue; } }; @@ -107,24 +116,21 @@ impl ServiceDetector { }; let r = match probe { - ServiceProbe::TcpHTTPGet | ServiceProbe::TcpHTTPSGet | ServiceProbe::TcpHTTPOptions => { + ServiceProbe::TcpHTTPGet + | ServiceProbe::TcpHTTPSGet + | ServiceProbe::TcpHTTPOptions => { probe::http::HttpProbe::run(ctx).await - }, - ServiceProbe::TcpTlsSession => { - probe::tls::TlsProbe::run(ctx).await - }, + } + ServiceProbe::TcpTlsSession => probe::tls::TlsProbe::run(ctx).await, ServiceProbe::TcpGenericLines | ServiceProbe::TcpHelp => { probe::generic::GenericProbe::run(ctx).await - }, - ServiceProbe::UdpDNSVersionBindReq | ServiceProbe::TcpDNSVersionBindReq => { + } + ServiceProbe::UdpDNSVersionBindReq + | ServiceProbe::TcpDNSVersionBindReq => { probe::dns::DnsProbe::run(ctx).await - }, - ServiceProbe::UdpQuic => { - probe::quic::QuicProbe::run(ctx).await - }, - _ => { - probe::null::NullProbe::run(ctx).await } + ServiceProbe::UdpQuic => probe::quic::QuicProbe::run(ctx).await, + _ => probe::null::NullProbe::run(ctx).await, }; results.push(r); } @@ -152,20 +158,19 @@ impl ServiceDetector { Ok(results) } - pub async fn run_service_detection(&self, targets: Vec) -> Result { + pub async fn run_service_detection( + &self, + targets: Vec, + ) -> Result { let start_time = Instant::now(); let mut tasks = vec![]; for endpoint in targets { let endpoint = endpoint.clone(); let conf = self.config.clone(); tasks.push(tokio::spawn(async move { - let probe_results = Self::detect_services( - conf, - endpoint - ) - .await; + let probe_results = Self::detect_services(conf, endpoint).await; probe_results - })); + })); } let mut results: Vec = Vec::new(); for task in tasks { @@ -174,7 +179,7 @@ impl ServiceDetector { Ok(mut result) => { // Merge results results.append(&mut result); - }, + } Err(e) => { tracing::error!("Service detection failed: {}", e); } @@ -226,7 +231,11 @@ where // Data read Ok(Ok(n)) => { if out.len() > max_bytes { - bail!("response exceeded max_bytes ({} > {})", out.len(), max_bytes); + bail!( + "response exceeded max_bytes ({} > {})", + out.len(), + max_bytes + ); } out.extend_from_slice(&buf[..n]); @@ -248,7 +257,8 @@ where // Build a regex with given pattern and flags fn build_regex(pat: &str, flags: &str) -> anyhow::Result { let mut b = RegexBuilder::new(pat); - b.case_insensitive(flags.contains('i')).dot_matches_new_line(flags.contains('s')); + b.case_insensitive(flags.contains('i')) + .dot_matches_new_line(flags.contains('s')); //b.multi_line(true); Ok(b.build()?) } @@ -256,8 +266,8 @@ fn build_regex(pat: &str, flags: &str) -> anyhow::Result { /// Build a regex for HTTP headers (multi-line, case-insensitive, dot matches new line) fn build_http_regex(pat: &str) -> anyhow::Result { Ok(RegexBuilder::new(pat) - .multi_line(true) - .case_insensitive(true) + .multi_line(true) + .case_insensitive(true) .dot_matches_new_line(true) .build()?) } diff --git a/src/service/payload.rs b/src/service/payload.rs index 39e139a..f4990c5 100644 --- a/src/service/payload.rs +++ b/src/service/payload.rs @@ -1,6 +1,6 @@ -use anyhow::Result; -use base64::{engine::general_purpose, Engine as _}; use crate::service::probe::{PayloadEncoding, PortProbe}; +use anyhow::Result; +use base64::{Engine as _, engine::general_purpose}; /// Context for building payloads #[derive(Default, Clone)] @@ -27,7 +27,10 @@ impl PayloadBuilder { if s.contains("$HOST") { let host = ctx.hostname.ok_or_else(|| { - anyhow::anyhow!("probe {} requires hostname (found $HOST in payload)", self.probe.probe_id.as_str()) + anyhow::anyhow!( + "probe {} requires hostname (found $HOST in payload)", + self.probe.probe_id.as_str() + ) })?; s = s.replace("$HOST", host); } @@ -38,11 +41,15 @@ impl PayloadBuilder { Ok(s.into_bytes()) } - PayloadEncoding::Base64 => { - general_purpose::STANDARD - .decode(&self.probe.payload) - .map_err(|e| anyhow::anyhow!("base64 decode failed for {}: {}", self.probe.probe_id.as_str(), e)) - } + PayloadEncoding::Base64 => general_purpose::STANDARD + .decode(&self.probe.payload) + .map_err(|e| { + anyhow::anyhow!( + "base64 decode failed for {}: {}", + self.probe.probe_id.as_str(), + e + ) + }), } } } diff --git a/src/service/probe/dns.rs b/src/service/probe/dns.rs index a987cab..5a464b6 100644 --- a/src/service/probe/dns.rs +++ b/src/service/probe/dns.rs @@ -1,14 +1,23 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; +use crate::{ + endpoint::ServiceInfo, + service::{ + build_regex, expand_cpe_templates, + probe::{PortProbeResult, ProbeContext, ServiceProbe}, + }, +}; use anyhow::Result; -use crate::{endpoint::ServiceInfo, service::{build_regex, expand_cpe_templates, probe::{PortProbeResult, ProbeContext, ServiceProbe}}}; use hickory_proto::{ op::{Message, MessageType, OpCode, Query}, rr::{DNSClass, Name, RecordType}, serialize::binary::{BinEncodable, BinEncoder}, }; -use tokio::{io::{AsyncReadExt, AsyncWriteExt}, net::{TcpStream, UdpSocket}}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpStream, UdpSocket}, +}; /// Build a DNS query message for "version.bind" TXT record in CHAOS class. fn build_version_bind_query() -> anyhow::Result> { @@ -31,8 +40,12 @@ fn build_version_bind_query() -> anyhow::Result> { } /// Perform a DNS version.bind query over UDP. -async fn run_dns_version_bind_udp(addr: std::net::SocketAddr, idle: std::time::Duration, _total: std::time::Duration, max_bytes: usize) --> anyhow::Result<(String, bool)> { +async fn run_dns_version_bind_udp( + addr: std::net::SocketAddr, + idle: std::time::Duration, + _total: std::time::Duration, + max_bytes: usize, +) -> anyhow::Result<(String, bool)> { let q = build_version_bind_query()?; let local = if addr.is_ipv6() { SocketAddr::new(IpAddr::V6(Ipv6Addr::UNSPECIFIED), 0) @@ -54,11 +67,17 @@ async fn run_dns_version_bind_udp(addr: std::net::SocketAddr, idle: std::time::D // Extract TXT record let mut txt = String::new(); for ans in msg.answers() { - if ans.record_type() == RecordType::TXT && ans.name().to_ascii().eq_ignore_ascii_case("version.bind.") { + if ans.record_type() == RecordType::TXT + && ans.name().to_ascii().eq_ignore_ascii_case("version.bind.") + { if ans.dns_class() == DNSClass::CH { if let hickory_proto::rr::RData::TXT(t) = ans.data() { - let joined = t.txt_data().iter().map(|b| String::from_utf8_lossy(b).to_string()) - .collect::>().join(""); + let joined = t + .txt_data() + .iter() + .map(|b| String::from_utf8_lossy(b).to_string()) + .collect::>() + .join(""); txt = joined; break; } @@ -73,8 +92,12 @@ async fn run_dns_version_bind_udp(addr: std::net::SocketAddr, idle: std::time::D } /// Perform a DNS version.bind query over TCP. -async fn run_dns_version_bind_tcp(addr: std::net::SocketAddr, idle: std::time::Duration, total: std::time::Duration, max_bytes: usize) --> anyhow::Result { +async fn run_dns_version_bind_tcp( + addr: std::net::SocketAddr, + idle: std::time::Duration, + total: std::time::Duration, + max_bytes: usize, +) -> anyhow::Result { let mut stream = tokio::time::timeout(total, TcpStream::connect(addr)).await??; let q = build_version_bind_query()?; @@ -88,7 +111,9 @@ async fn run_dns_version_bind_tcp(addr: std::net::SocketAddr, idle: std::time::D let mut lenbuf = [0u8; 2]; tokio::time::timeout(idle, stream.read_exact(&mut lenbuf)).await??; let want = u16::from_be_bytes(lenbuf) as usize; - if want > max_bytes { anyhow::bail!("dns/tcp response exceeds max_bytes"); } + if want > max_bytes { + anyhow::bail!("dns/tcp response exceeds max_bytes"); + } let mut buf = vec![0u8; want]; tokio::time::timeout(idle, stream.read_exact(&mut buf)).await??; @@ -96,12 +121,20 @@ async fn run_dns_version_bind_tcp(addr: std::net::SocketAddr, idle: std::time::D let msg = hickory_proto::op::Message::from_vec(&buf)?; // Extract TXT record for ans in msg.answers() { - if ans.record_type() == RecordType::TXT && ans.name().to_ascii().eq_ignore_ascii_case("version.bind.") { + if ans.record_type() == RecordType::TXT + && ans.name().to_ascii().eq_ignore_ascii_case("version.bind.") + { if ans.dns_class() == DNSClass::CH { if let hickory_proto::rr::RData::TXT(t) = ans.data() { - let joined = t.txt_data().iter().map(|b| String::from_utf8_lossy(b).to_string()) - .collect::>().join(""); - if !joined.is_empty() { return Ok(joined); } + let joined = t + .txt_data() + .iter() + .map(|b| String::from_utf8_lossy(b).to_string()) + .collect::>() + .join(""); + if !joined.is_empty() { + return Ok(joined); + } } } } @@ -153,9 +186,17 @@ impl DnsProbe { pub async fn run(ctx: ProbeContext) -> Result { let addr = std::net::SocketAddr::new(ctx.ip, ctx.probe.port); // Try UDP first - if matches!(ctx.probe.probe_id, ServiceProbe::UdpDNSVersionBindReq | ServiceProbe::TcpDNSVersionBindReq) { - tracing::debug!("DNS Version Bind Probe (UDP): {}:{}", ctx.ip, ctx.probe.port); - match run_dns_version_bind_udp(addr, ctx.timeout, ctx.timeout, ctx.max_read_size).await { + if matches!( + ctx.probe.probe_id, + ServiceProbe::UdpDNSVersionBindReq | ServiceProbe::TcpDNSVersionBindReq + ) { + tracing::debug!( + "DNS Version Bind Probe (UDP): {}:{}", + ctx.ip, + ctx.probe.port + ); + match run_dns_version_bind_udp(addr, ctx.timeout, ctx.timeout, ctx.max_read_size).await + { Ok((txt, truncated)) => { let mut svc = ServiceInfo::default(); let udp_svc_db = crate::db::service::udp_service_db(); @@ -163,20 +204,24 @@ impl DnsProbe { svc.banner = Some(txt.clone()); svc.raw = Some(txt.clone()); // Match (using UDP-side) - let hits = match_response_signatures( - "udp:dns_version_bind_req", &txt - )?; + let hits = match_response_signatures("udp:dns_version_bind_req", &txt)?; if let Some((best_service, cpes)) = hits { svc.name = Some(best_service); svc.cpes = cpes; } // If truncated, try TCP as well if truncated { - if let Ok(txt2) = run_dns_version_bind_tcp(addr, ctx.timeout, ctx.timeout, ctx.max_read_size).await { + if let Ok(txt2) = run_dns_version_bind_tcp( + addr, + ctx.timeout, + ctx.timeout, + ctx.max_read_size, + ) + .await + { svc.raw = Some(txt2.clone()); - let hits2 = match_response_signatures( - "tcp:dns_version_bind_req", &txt2 - )?; + let hits2 = + match_response_signatures("tcp:dns_version_bind_req", &txt2)?; if let Some((best_service, cpes)) = hits2 { svc.name = Some(best_service); svc.cpes = cpes; @@ -195,17 +240,22 @@ impl DnsProbe { } Err(e) => { tracing::debug!("DNS Version Bind Probe (UDP) failed: {}", e); - tracing::debug!("Attempting DNS Version Bind Probe (TCP): {}:{}", ctx.ip, ctx.probe.port); + tracing::debug!( + "Attempting DNS Version Bind Probe (TCP): {}:{}", + ctx.ip, + ctx.probe.port + ); let mut svc = ServiceInfo::default(); let tcp_svc_db = crate::db::service::tcp_service_db(); svc.name = tcp_svc_db.get_name(ctx.probe.port).map(|s| s.to_string()); // If UDP failed, try TCP - if let Ok(txt) = run_dns_version_bind_tcp(addr, ctx.timeout, ctx.timeout, ctx.max_read_size).await { + if let Ok(txt) = + run_dns_version_bind_tcp(addr, ctx.timeout, ctx.timeout, ctx.max_read_size) + .await + { svc.banner = Some(txt.clone()); svc.raw = Some(txt.clone()); - let hits = match_response_signatures( - "tcp:dns_version_bind_req", &txt - )?; + let hits = match_response_signatures("tcp:dns_version_bind_req", &txt)?; if let Some((best_service, cpes)) = hits { svc.name = Some(best_service); svc.cpes = cpes; @@ -226,8 +276,13 @@ impl DnsProbe { // If UDP not selected or failed, and TCP is selected if matches!(ctx.probe.probe_id, ServiceProbe::TcpDNSVersionBindReq) { - tracing::debug!("DNS Version Bind Probe (TCP): {}:{}", ctx.ip, ctx.probe.port); - let txt = run_dns_version_bind_tcp(addr, ctx.timeout, ctx.timeout, ctx.max_read_size).await?; + tracing::debug!( + "DNS Version Bind Probe (TCP): {}:{}", + ctx.ip, + ctx.probe.port + ); + let txt = + run_dns_version_bind_tcp(addr, ctx.timeout, ctx.timeout, ctx.max_read_size).await?; let mut svc = ServiceInfo::default(); let tcp_svc_db = crate::db::service::tcp_service_db(); svc.name = tcp_svc_db.get_name(ctx.probe.port).map(|s| s.to_string()); diff --git a/src/service/probe/generic.rs b/src/service/probe/generic.rs index 221169f..f66b06d 100644 --- a/src/service/probe/generic.rs +++ b/src/service/probe/generic.rs @@ -1,11 +1,14 @@ -use std::net::SocketAddr; use anyhow::Result; -use tokio::{io::{AsyncWriteExt}, net::TcpStream, time::timeout}; +use std::net::SocketAddr; +use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout}; use crate::{ endpoint::ServiceInfo, service::{ - build_regex, expand_cpe_templates, payload::{PayloadBuilder, PayloadContext}, probe::{PortProbeResult, ProbeContext}, read_timeout + build_regex, expand_cpe_templates, + payload::{PayloadBuilder, PayloadContext}, + probe::{PortProbeResult, ProbeContext}, + read_timeout, }, }; @@ -24,7 +27,12 @@ fn parse_banner(bytes: &[u8], max_preview: usize) -> BannerLite { } else { raw.to_string() }; - let first = out.raw_text.split(|c| c == '\n').next().unwrap_or("").trim_end_matches('\r'); + let first = out + .raw_text + .split(|c| c == '\n') + .next() + .unwrap_or("") + .trim_end_matches('\r'); if !first.is_empty() { out.first_line = Some(first.to_string()); } @@ -33,10 +41,7 @@ fn parse_banner(bytes: &[u8], max_preview: usize) -> BannerLite { /// Match response text against known service signatures. /// (service, cpes) -fn match_signatures( - probe_id: &str, - text: &str, -) -> anyhow::Result)>> { +fn match_signatures(probe_id: &str, text: &str) -> anyhow::Result)>> { let sigdb = crate::db::service::response_signatures_db(); let mut best_service: String = String::new(); let mut cpes: Vec = Vec::new(); @@ -77,7 +82,7 @@ impl GenericProbe { // If payload is present, send it let payload = PayloadBuilder::new(ctx.probe.clone()) - .payload(PayloadContext::default()) + .payload(PayloadContext::default()) .unwrap_or_default(); if !payload.is_empty() { timeout(ctx.timeout, stream.write_all(&payload)).await??; @@ -87,13 +92,23 @@ impl GenericProbe { // Apply idle/total timeout + max byte limit for reading let idle = ctx.timeout; let total = ctx.timeout; - tracing::debug!("Generic Probe: {}:{} - Reading response(timeout: {})", ctx.ip, ctx.probe.port, total.as_millis()); + tracing::debug!( + "Generic Probe: {}:{} - Reading response(timeout: {})", + ctx.ip, + ctx.probe.port, + total.as_millis() + ); let bytes = read_timeout(&mut stream, idle, total, ctx.max_read_size).await?; // Extract banner let banner = parse_banner(&bytes, 64 * 1024); - tracing::debug!("Generic Probe: {}:{} - Banner: {:?}", ctx.ip, ctx.probe.port, banner.first_line); + tracing::debug!( + "Generic Probe: {}:{} - Banner: {:?}", + ctx.ip, + ctx.probe.port, + banner.first_line + ); // Match signatures let hit = match_signatures(ctx.probe.probe_id.as_str(), &banner.raw_text)?; diff --git a/src/service/probe/http.rs b/src/service/probe/http.rs index 361289a..f257243 100644 --- a/src/service/probe/http.rs +++ b/src/service/probe/http.rs @@ -2,12 +2,23 @@ use std::{collections::HashMap, net::SocketAddr}; use anyhow::Result; use rustls_pki_types::ServerName; -use tokio::{io::{AsyncWriteExt}, net::TcpStream, time::timeout}; -use tokio_rustls::{TlsConnector, rustls::{ClientConfig, RootCertStore}}; use std::sync::Arc; +use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout}; +use tokio_rustls::{ + TlsConnector, + rustls::{ClientConfig, RootCertStore}, +}; -use crate::{endpoint::ServiceInfo, service::{build_http_regex, expand_cpe_templates, payload::{PayloadBuilder, PayloadContext}, probe::{PortProbeResult, ProbeContext, ServiceProbe}, read_timeout}}; use super::tls::SkipServerVerification; +use crate::{ + endpoint::ServiceInfo, + service::{ + build_http_regex, expand_cpe_templates, + payload::{PayloadBuilder, PayloadContext}, + probe::{PortProbeResult, ProbeContext, ServiceProbe}, + read_timeout, + }, +}; /// A lightweight representation of an HTTP response for analysis. #[derive(Debug, Default, Clone)] @@ -34,7 +45,11 @@ fn parse_http_response(bytes: &[u8], body_limit: usize) -> HttpResponseLite { .unwrap_or_else(|| bytes.len().min(HDR_MAX)); let header_bytes = &bytes[..hdr_end.min(bytes.len())]; - let body_bytes = if hdr_end < bytes.len() { &bytes[hdr_end..] } else { &[][..] }; + let body_bytes = if hdr_end < bytes.len() { + &bytes[hdr_end..] + } else { + &[][..] + }; // Convert header bytes to a lossy UTF-8 string let header_text = String::from_utf8_lossy(header_bytes); @@ -49,13 +64,18 @@ fn parse_http_response(bytes: &[u8], body_limit: usize) -> HttpResponseLite { if let Some(first) = lines.next() { let line = first.trim().to_string(); res.status_line = Some(line.clone()); - if let Some(code) = line.split_whitespace().nth(1).and_then(|s| s.parse::().ok()) { + if let Some(code) = line + .split_whitespace() + .nth(1) + .and_then(|s| s.parse::().ok()) + { res.status_code = Some(code); } } for line in lines { if let Some((k, v)) = line.split_once(':') { - res.headers.insert(k.trim().to_ascii_lowercase(), v.trim().to_string()); + res.headers + .insert(k.trim().to_ascii_lowercase(), v.trim().to_string()); } } @@ -84,10 +104,13 @@ fn match_http_signatures( let mut hits = Vec::new(); 'outer: for sig in sigdb { - if !service_keys.iter().any(|k| sig.service.eq_ignore_ascii_case(k)) { + if !service_keys + .iter() + .any(|k| sig.service.eq_ignore_ascii_case(k)) + { continue; } - + /* if !sig.probe_id.is_empty() && !sig.probe_id.eq_ignore_ascii_case(probe_id) { continue; } */ @@ -118,27 +141,40 @@ impl HttpProbe { let tcp_svc_db = crate::db::service::tcp_service_db(); match ctx.probe.probe_id { ServiceProbe::TcpHTTPGet => { - tracing::debug!("HTTP Probe: {}:{} - Sending HTTP GET", ctx.ip, ctx.probe.port); + tracing::debug!( + "HTTP Probe: {}:{} - Sending HTTP GET", + ctx.ip, + ctx.probe.port + ); let payload: Vec = payload_builder.payload(PayloadContext::default())?; timeout(ctx.timeout, tcp_stream.write_all(&payload)).await??; tcp_stream.flush().await?; - let res: Vec = read_timeout(&mut tcp_stream, ctx.timeout, ctx.timeout, ctx.max_read_size).await?; + let res: Vec = + read_timeout(&mut tcp_stream, ctx.timeout, ctx.timeout, ctx.max_read_size) + .await?; let http_res = parse_http_response(&res, 64 * 1024); - tracing::debug!("HTTP Probe: {}:{} - Header: {:?}", ctx.ip, ctx.probe.port, http_res.header_text); + tracing::debug!( + "HTTP Probe: {}:{} - Header: {:?}", + ctx.ip, + ctx.probe.port, + http_res.header_text + ); let mut svc = ServiceInfo::default(); svc.name = tcp_svc_db.get_name(ctx.probe.port).map(|s| s.to_string()); svc.banner = http_res.status_line.clone(); svc.product = http_res.headers.get("server").cloned(); svc.raw = Some(http_res.raw_text.clone()); - tracing::debug!("HTTP Probe: {}:{} - Banner: {:?}, Server {:?}", ctx.ip, ctx.probe.port, svc.banner, svc.product); + tracing::debug!( + "HTTP Probe: {}:{} - Banner: {:?}, Server {:?}", + ctx.ip, + ctx.probe.port, + svc.banner, + svc.product + ); // Match signatures - let cpes = match_http_signatures( - &["http"], - "tcp:http_get", - &http_res, - )?; + let cpes = match_http_signatures(&["http"], "tcp:http_get", &http_res)?; if !cpes.is_empty() { svc.cpes = cpes; } @@ -152,9 +188,13 @@ impl HttpProbe { }; tracing::debug!("HTTP Probe Result: {:?}", probe_result); return Ok(probe_result); - }, + } ServiceProbe::TcpHTTPSGet => { - tracing::debug!("HTTP Probe: {}:{} - Sending HTTPS GET", ctx.ip, ctx.probe.port); + tracing::debug!( + "HTTP Probe: {}:{} - Sending HTTPS GET", + ctx.ip, + ctx.probe.port + ); let payload_ctx = PayloadContext { hostname: ctx.hostname.as_deref(), path: Some("/".into()), @@ -163,7 +203,9 @@ impl HttpProbe { // rustls config let mut roots = RootCertStore::empty(); - for cert in rustls_native_certs::load_native_certs()? { let _ = roots.add(cert); } + for cert in rustls_native_certs::load_native_certs()? { + let _ = roots.add(cert); + } let mut config = ClientConfig::builder() .with_root_certificates(roots) .with_no_client_auth(); @@ -173,7 +215,9 @@ impl HttpProbe { config.alpn_protocols = vec!["http/1.1".into()]; if ctx.skip_cert_verify { - config.dangerous().set_certificate_verifier(SkipServerVerification::new()); + config + .dangerous() + .set_certificate_verifier(SkipServerVerification::new()); } let connector = TlsConnector::from(Arc::new(config)); @@ -183,7 +227,8 @@ impl HttpProbe { ServerName::try_from("localhost")? }; - let mut tls_stream = timeout(ctx.timeout, connector.connect(sni_name, tcp_stream)).await??; + let mut tls_stream = + timeout(ctx.timeout, connector.connect(sni_name, tcp_stream)).await??; // server connection let conn = tls_stream.get_ref().1; @@ -194,22 +239,31 @@ impl HttpProbe { tls_stream.write_all(&payload).await?; tls_stream.flush().await?; - let res: Vec = read_timeout(&mut tls_stream, ctx.timeout, ctx.timeout, ctx.max_read_size).await?; + let res: Vec = + read_timeout(&mut tls_stream, ctx.timeout, ctx.timeout, ctx.max_read_size) + .await?; let http_res = parse_http_response(&res, 64 * 1024); - tracing::debug!("HTTP Probe: {}:{} - Header: {:?}", ctx.ip, ctx.probe.port, http_res.header_text); + tracing::debug!( + "HTTP Probe: {}:{} - Header: {:?}", + ctx.ip, + ctx.probe.port, + http_res.header_text + ); svc.banner = http_res.status_line.clone(); svc.product = http_res.headers.get("server").cloned(); svc.raw = Some(http_res.raw_text.clone()); - tracing::debug!("HTTPS Probe: {}:{} - Banner: {:?}, Server {:?}", ctx.ip, ctx.probe.port, svc.banner, svc.product); + tracing::debug!( + "HTTPS Probe: {}:{} - Banner: {:?}, Server {:?}", + ctx.ip, + ctx.probe.port, + svc.banner, + svc.product + ); tracing::debug!("RAW: {:?}", svc.raw); // Match signatures - let cpes = match_http_signatures( - &["http"], - "tcp:https_get", - &http_res, - )?; + let cpes = match_http_signatures(&["http"], "tcp:https_get", &http_res)?; if !cpes.is_empty() { svc.cpes = cpes; } @@ -223,29 +277,42 @@ impl HttpProbe { }; tracing::debug!("HTTP Probe Result: {:?}", probe_result); return Ok(probe_result); - }, + } ServiceProbe::TcpHTTPOptions => { - tracing::debug!("HTTP Probe: {}:{} - Sending HTTP OPTIONS", ctx.ip, ctx.probe.port); + tracing::debug!( + "HTTP Probe: {}:{} - Sending HTTP OPTIONS", + ctx.ip, + ctx.probe.port + ); let payload: Vec = payload_builder.payload(PayloadContext::default())?; timeout(ctx.timeout, tcp_stream.write_all(&payload)).await??; tcp_stream.flush().await?; - let res: Vec = read_timeout(&mut tcp_stream, ctx.timeout, ctx.timeout, ctx.max_read_size).await?; + let res: Vec = + read_timeout(&mut tcp_stream, ctx.timeout, ctx.timeout, ctx.max_read_size) + .await?; let http_res = parse_http_response(&res, 64 * 1024); - tracing::debug!("HTTP Probe: {}:{} - Header: {:?}", ctx.ip, ctx.probe.port, http_res.header_text); + tracing::debug!( + "HTTP Probe: {}:{} - Header: {:?}", + ctx.ip, + ctx.probe.port, + http_res.header_text + ); let mut svc = ServiceInfo::default(); svc.name = tcp_svc_db.get_name(ctx.probe.port).map(|s| s.to_string()); svc.banner = http_res.status_line.clone(); svc.product = http_res.headers.get("server").cloned(); svc.raw = Some(http_res.raw_text.clone()); - tracing::debug!("HTTP Probe: {}:{} - Banner: {:?}, Server {:?}", ctx.ip, ctx.probe.port, svc.banner, svc.product); + tracing::debug!( + "HTTP Probe: {}:{} - Banner: {:?}, Server {:?}", + ctx.ip, + ctx.probe.port, + svc.banner, + svc.product + ); // Match signatures - let cpes = match_http_signatures( - &["http"], - "tcp:http_options", - &http_res, - )?; + let cpes = match_http_signatures(&["http"], "tcp:http_options", &http_res)?; if !cpes.is_empty() { svc.cpes = cpes; } @@ -260,7 +327,7 @@ impl HttpProbe { tracing::debug!("HTTP Probe Result: {:?}", probe_result); return Ok(probe_result); } - _ => {}, + _ => {} } Err(anyhow::anyhow!("Failed to probe HTTP service at {}", addr)) } diff --git a/src/service/probe/mod.rs b/src/service/probe/mod.rs index 05e75af..5adcbea 100644 --- a/src/service/probe/mod.rs +++ b/src/service/probe/mod.rs @@ -1,15 +1,15 @@ -pub mod null; +pub mod dns; pub mod generic; pub mod http; -pub mod tls; -pub mod dns; +pub mod null; pub mod quic; +pub mod tls; use std::{collections::BTreeMap, net::IpAddr, time::Duration}; use serde::{Deserialize, Serialize}; -use crate::{endpoint::{ServiceInfo, TransportProtocol}}; +use crate::endpoint::{ServiceInfo, TransportProtocol}; /// Metadata for the database #[derive(Serialize, Deserialize, Clone, Debug)] @@ -68,9 +68,13 @@ impl ServiceProbe { /// Get the transport protocol associated with the ServiceProbe. pub fn transport(&self) -> TransportProtocol { match self { - ServiceProbe::TcpNull | ServiceProbe::TcpGenericLines | ServiceProbe::TcpHTTPGet - | ServiceProbe::TcpHTTPSGet | ServiceProbe::TcpHTTPOptions - | ServiceProbe::TcpDNSVersionBindReq | ServiceProbe::TcpHelp + ServiceProbe::TcpNull + | ServiceProbe::TcpGenericLines + | ServiceProbe::TcpHTTPGet + | ServiceProbe::TcpHTTPSGet + | ServiceProbe::TcpHTTPOptions + | ServiceProbe::TcpDNSVersionBindReq + | ServiceProbe::TcpHelp | ServiceProbe::TcpTlsSession => TransportProtocol::Tcp, ServiceProbe::UdpDNSVersionBindReq | ServiceProbe::UdpQuic => TransportProtocol::Udp, } @@ -90,7 +94,7 @@ pub enum PayloadEncoding { pub struct PortProbeDb { pub meta: Meta, // port -> probe_id[] - pub map: BTreeMap>, + pub map: BTreeMap>, } impl PortProbeDb { diff --git a/src/service/probe/null.rs b/src/service/probe/null.rs index 8b5aaae..e01e5ec 100644 --- a/src/service/probe/null.rs +++ b/src/service/probe/null.rs @@ -1,11 +1,14 @@ +use anyhow::{Result, bail}; use std::net::SocketAddr; -use anyhow::{bail, Result}; -use tokio::{io::{AsyncWriteExt}, net::TcpStream, time::timeout}; +use tokio::{io::AsyncWriteExt, net::TcpStream, time::timeout}; use crate::{ endpoint::ServiceInfo, service::{ - build_regex, expand_cpe_templates, payload::{PayloadBuilder, PayloadContext}, probe::{PortProbeResult, ProbeContext, ServiceProbe}, read_timeout + build_regex, expand_cpe_templates, + payload::{PayloadBuilder, PayloadContext}, + probe::{PortProbeResult, ProbeContext, ServiceProbe}, + read_timeout, }, }; @@ -23,7 +26,10 @@ fn looks_like_text_line(s: &str) -> bool { return false; } // ASCII printable characters and whitespace ratio - let printable = t.chars().filter(|&c| c.is_ascii_graphic() || c.is_ascii_whitespace()).count(); + let printable = t + .chars() + .filter(|&c| c.is_ascii_graphic() || c.is_ascii_whitespace()) + .count(); let ratio = printable as f32 / t.len() as f32; ratio >= 0.6 } @@ -42,7 +48,10 @@ fn parse_banner(bytes: &[u8], max_preview: usize) -> BannerLite { raw.to_string() }; - let mut lines = out.raw_text.split(|c| c == '\n').map(|l| l.trim_end_matches('\r')); + let mut lines = out + .raw_text + .split(|c| c == '\n') + .map(|l| l.trim_end_matches('\r')); let first = lines.next().unwrap_or(""); let second = lines.next().unwrap_or(""); @@ -87,7 +96,10 @@ impl NullProbe { pub async fn run(ctx: ProbeContext) -> Result { // Pre-check if ctx.probe.probe_id != ServiceProbe::TcpNull { - bail!("NullProbe invoked with non-tcp:null probe_id: {:?}", ctx.probe.probe_id); + bail!( + "NullProbe invoked with non-tcp:null probe_id: {:?}", + ctx.probe.probe_id + ); } tracing::debug!("Null Probe: {}:{} - Connecting", ctx.ip, ctx.probe.port); @@ -98,7 +110,7 @@ impl NullProbe { // if payload is present, send it (should not happen for tcp:null, but just in case) let payload = PayloadBuilder::new(ctx.probe.clone()) - .payload(PayloadContext::default()) + .payload(PayloadContext::default()) .unwrap_or_default(); if !payload.is_empty() { timeout(ctx.timeout, stream.write_all(&payload)).await??; @@ -108,16 +120,26 @@ impl NullProbe { // Apply idle/total timeout and max byte limit for reading let idle = ctx.timeout; let total = ctx.timeout; - tracing::debug!("Null Probe: {}:{} - Reading response(timeout: {})", ctx.ip, ctx.probe.port, total.as_millis()); + tracing::debug!( + "Null Probe: {}:{} - Reading response(timeout: {})", + ctx.ip, + ctx.probe.port, + total.as_millis() + ); let bytes = read_timeout(&mut stream, idle, total, ctx.max_read_size).await?; // Parse banner from response let banner = parse_banner(&bytes, 64 * 1024); - tracing::debug!("TCP NULL Probe: {}:{} - Banner: {:?}", ctx.ip, ctx.probe.port, banner.first_line); + tracing::debug!( + "TCP NULL Probe: {}:{} - Banner: {:?}", + ctx.ip, + ctx.probe.port, + banner.first_line + ); // Match signatures (tcp:NULL) - let hit = match_null_signatures( "tcp:NULL", &banner.raw_text)?; + let hit = match_null_signatures("tcp:NULL", &banner.raw_text)?; // Construct service info let mut svc = ServiceInfo::default(); diff --git a/src/service/probe/quic.rs b/src/service/probe/quic.rs index 2eca02a..c39691f 100644 --- a/src/service/probe/quic.rs +++ b/src/service/probe/quic.rs @@ -1,16 +1,14 @@ use anyhow::Result; use bytes::{Buf, BytesMut}; -use x509_parser::prelude::FromDer; -use std::{net::SocketAddr, sync::Arc}; +use http::{Method, Request}; use quinn::{ClientConfig, Endpoint}; use rustls::{ClientConfig as RustlsClientConfig, RootCertStore}; -use http::{Request, Method}; +use std::{net::SocketAddr, sync::Arc}; +use x509_parser::prelude::FromDer; use crate::{ endpoint::{ServiceInfo, TlsInfo}, - service::{ - probe::{ProbeContext, PortProbeResult, tls::SkipServerVerification}, - }, + service::probe::{PortProbeResult, ProbeContext, tls::SkipServerVerification}, }; /// Create a QUIC client configuration with optional certificate verification skipping and ALPN protocols. @@ -23,7 +21,8 @@ pub fn quic_client_config(skip_verify: bool, alpn: &[&[u8]]) -> Result { - match data.downcast::() { - Ok(hd) => { - match hd.protocol { - Some(ref p) => Some(String::from_utf8_lossy(p).to_string()), - None => None, - } - }, - Err(_) => None, - } + Some(data) => match data.downcast::() { + Ok(hd) => match hd.protocol { + Some(ref p) => Some(String::from_utf8_lossy(p).to_string()), + None => None, + }, + Err(_) => None, }, None => None, }; let cert_der_bytes: Option> = quinn_conn .peer_identity() - .and_then(|any| any.downcast_ref::>().cloned()) + .and_then(|any| { + any.downcast_ref::>() + .cloned() + }) .and_then(|vec_der| vec_der.into_iter().next()) .map(|der| der.to_vec()); @@ -83,19 +98,23 @@ impl QuicProbe { tls_info.version = Some("TLSv1_3".into()); if let Some(bytes) = cert_der_bytes.as_deref() { if let Ok((_, x509)) = x509_parser::prelude::X509Certificate::from_der(bytes) { - tls_info.subject = x509.subject() + tls_info.subject = x509 + .subject() .iter_common_name() .next() .and_then(|cn| cn.as_str().ok()) .map(|s| s.to_string()); - tls_info.issuer = x509.issuer() + tls_info.issuer = x509 + .issuer() .iter_common_name() .next() .and_then(|cn| cn.as_str().ok()) .map(|s| s.to_string()); let mut sans = Vec::new(); for ext in x509.extensions() { - if let x509_parser::extensions::ParsedExtension::SubjectAlternativeName(san) = ext.parsed_extension() { + if let x509_parser::extensions::ParsedExtension::SubjectAlternativeName(san) = + ext.parsed_extension() + { for name in san.general_names.iter() { sans.push(name.to_string()); } @@ -103,7 +122,7 @@ impl QuicProbe { } tls_info.san_list = sans; tls_info.not_before = Some(x509.validity().not_before.to_string()); - tls_info.not_after = Some(x509.validity().not_after.to_string()); + tls_info.not_after = Some(x509.validity().not_after.to_string()); tls_info.serial_hex = Some(x509.raw_serial_as_string()); tls_info.sig_algorithm = Some(crate::db::tls::oid_sig_name( x509.signature_algorithm.oid().to_id_string().as_str(), @@ -122,7 +141,9 @@ impl QuicProbe { let h3_quinn_conn = h3_quinn::Connection::new(quinn_conn); let (mut driver, mut send_request) = h3::client::new(h3_quinn_conn).await?; let drive = async move { - return Err::<(), h3::error::ConnectionError>(futures::future::poll_fn(|cx| driver.poll_close(cx)).await); + return Err::<(), h3::error::ConnectionError>( + futures::future::poll_fn(|cx| driver.poll_close(cx)).await, + ); }; let request = async move { @@ -136,14 +157,22 @@ impl QuicProbe { .body(()) .unwrap(); - tracing::debug!("HTTP/3 Probe: {}:{} - Sending request", ctx.ip, ctx.probe.port); + tracing::debug!( + "HTTP/3 Probe: {}:{} - Sending request", + ctx.ip, + ctx.probe.port + ); // Send request let mut stream = send_request.send_request(req).await?; //let mut stream = tokio::time::timeout(ctx.timeout, send_request.send_request(req)).await??; stream.finish().await?; // Receive response (headers) - tracing::debug!("HTTP/3 Probe: {}:{} - Receiving response", ctx.ip, ctx.probe.port); + tracing::debug!( + "HTTP/3 Probe: {}:{} - Receiving response", + ctx.ip, + ctx.probe.port + ); let res = stream.recv_response().await?; // Extract status and Server headers let udp_svc_db = crate::db::service::udp_service_db(); @@ -151,8 +180,8 @@ impl QuicProbe { svc.banner = Some(format!("HTTP/3 {}", res.status())); svc.quic_version = Some("1".into()); if let Some(val) = res.headers().get("server") { - if let Ok(s) = val.to_str() { - svc.product = Some(s.to_string()); + if let Ok(s) = val.to_str() { + svc.product = Some(s.to_string()); } } @@ -164,7 +193,9 @@ impl QuicProbe { let bytes: &[u8] = chunk.chunk(); body_bytes.extend_from_slice(bytes); read += bytes.len(); - if read >= max_body { break; } + if read >= max_body { + break; + } } svc.raw = Some(format!("alpn=h3; status={}", res.status())); @@ -175,7 +206,11 @@ impl QuicProbe { let (req_res, _drive_res) = tokio::join!(request, drive); match req_res { Ok(mut svc) => { - tracing::debug!("HTTP/3 Probe: {}:{} - Request succeeded", ctx.ip, ctx.probe.port); + tracing::debug!( + "HTTP/3 Probe: {}:{} - Request succeeded", + ctx.ip, + ctx.probe.port + ); tracing::debug!("HTTP/3 Probe Result: {:?}", svc); svc.tls_info = Some(tls_info.clone()); let probe_result = PortProbeResult { @@ -187,9 +222,14 @@ impl QuicProbe { service_info: svc, }; return Ok(probe_result); - }, + } Err(e) => { - tracing::error!("HTTP/3 Probe: {}:{} - Request failed: {}", ctx.ip, ctx.probe.port, e); + tracing::error!( + "HTTP/3 Probe: {}:{} - Request failed: {}", + ctx.ip, + ctx.probe.port, + e + ); } } } diff --git a/src/service/probe/tls.rs b/src/service/probe/tls.rs index 850b4ff..ece732e 100644 --- a/src/service/probe/tls.rs +++ b/src/service/probe/tls.rs @@ -1,15 +1,18 @@ +use crate::endpoint::ServiceInfo; +use crate::endpoint::TlsInfo; +use crate::service::probe::{PortProbeResult, ProbeContext}; use anyhow::Result; +use rustls::ClientConnection; use rustls::client::danger::ServerCertVerifier; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; -use rustls::ClientConnection; -use tokio::{net::TcpStream, time::timeout}; -use tokio_rustls::{TlsConnector, rustls::{ClientConfig, RootCertStore}}; +use std::net::SocketAddr; use std::sync::Arc; +use tokio::{net::TcpStream, time::timeout}; +use tokio_rustls::{ + TlsConnector, + rustls::{ClientConfig, RootCertStore}, +}; use x509_parser::prelude::{FromDer, ParsedExtension, X509Certificate}; -use std::net::SocketAddr; -use crate::endpoint::TlsInfo; -use crate::{endpoint::ServiceInfo}; -use crate::service::probe::{PortProbeResult, ProbeContext}; /// Dummy certificate verifier that treats any certificate as valid. /// NOTE, such verification is vulnerable to MITM attacks, but convenient for testing. @@ -68,7 +71,10 @@ impl ServerCertVerifier for SkipServerVerification { } /// Extract TLS info from a ClientConnection -pub fn extract_tls_info(probe_ctx: &ProbeContext, client_conn: &ClientConnection) -> Option { +pub fn extract_tls_info( + probe_ctx: &ProbeContext, + client_conn: &ClientConnection, +) -> Option { let mut tls_info = TlsInfo::default(); if let Some(version) = client_conn.protocol_version() { if let Some(version) = version.as_str() { @@ -83,19 +89,27 @@ pub fn extract_tls_info(probe_ctx: &ProbeContext, client_conn: &ClientConnection if let Some(alpn) = client_conn.alpn_protocol() { tls_info.alpn = Some(String::from_utf8_lossy(alpn).to_string()); } - - if let Some(cert) = client_conn.peer_certificates().and_then(|v| v.first()).cloned() { + + if let Some(cert) = client_conn + .peer_certificates() + .and_then(|v| v.first()) + .cloned() + { tls_info.sni = probe_ctx.hostname.clone(); match X509Certificate::from_der(&cert) { Ok((_, x509)) => { // Subject - let subject = x509.subject().iter_common_name() + let subject = x509 + .subject() + .iter_common_name() .next() .and_then(|cn| cn.as_str().ok()) .map(|s| s.to_string()); // Issuer - let issuer = x509.issuer().iter_common_name() + let issuer = x509 + .issuer() + .iter_common_name() .next() .and_then(|cn| cn.as_str().ok()) .map(|s| s.to_string()); @@ -116,11 +130,14 @@ pub fn extract_tls_info(probe_ctx: &ProbeContext, client_conn: &ClientConnection tls_info.not_before = Some(x509.validity().not_before.to_string()); tls_info.not_after = Some(x509.validity().not_after.to_string()); tls_info.serial_hex = Some(x509.raw_serial_as_string()); - let sig_alg_name = crate::db::tls::oid_sig_name(x509.signature_algorithm.oid().to_id_string().as_str()); + let sig_alg_name = crate::db::tls::oid_sig_name( + x509.signature_algorithm.oid().to_id_string().as_str(), + ); tls_info.sig_algorithm = Some(sig_alg_name); - let pubkey_alg_name = crate::db::tls::oid_pubkey_name(x509.public_key().algorithm.oid().to_id_string().as_str()); + let pubkey_alg_name = crate::db::tls::oid_pubkey_name( + x509.public_key().algorithm.oid().to_id_string().as_str(), + ); tls_info.pubkey_algorithm = Some(pubkey_alg_name); - } Err(e) => { tracing::warn!("Failed to parse certificate: {}", e); @@ -142,13 +159,17 @@ impl TlsProbe { // rustls config let mut roots = RootCertStore::empty(); - for cert in rustls_native_certs::load_native_certs()? { let _ = roots.add(cert); } + for cert in rustls_native_certs::load_native_certs()? { + let _ = roots.add(cert); + } let mut config = ClientConfig::builder() .with_root_certificates(roots) .with_no_client_auth(); if ctx.skip_cert_verify { - config.dangerous().set_certificate_verifier(SkipServerVerification::new()); + config + .dangerous() + .set_certificate_verifier(SkipServerVerification::new()); } let connector = TlsConnector::from(Arc::new(config)); diff --git a/src/time.rs b/src/time.rs index 13e258b..0dc3430 100644 --- a/src/time.rs +++ b/src/time.rs @@ -1,6 +1,6 @@ -use tracing_subscriber::fmt::time::FormatTime; -use std::fmt; use chrono::Local; +use std::fmt; +use tracing_subscriber::fmt::time::FormatTime; /// DateTime format for logging that includes date, time, and timezone (YYYY-MM-DD HH:MM:SS.mmmmmm+00:00) /// Same as `ChronoLocal::rfc_3339()` but with a custom format diff --git a/src/trace/mod.rs b/src/trace/mod.rs index 8d5bd96..289a001 100644 --- a/src/trace/mod.rs +++ b/src/trace/mod.rs @@ -3,9 +3,9 @@ pub mod probe; use std::net::Ipv4Addr; use std::{net::IpAddr, time::Duration}; +use anyhow::Result; use netdev::Interface; use serde::{Deserialize, Serialize}; -use anyhow::Result; use crate::config::default::{DEFAULT_BASE_TARGET_UDP_PORT, DEFAULT_HOP_LIMIT}; use crate::endpoint::Host; @@ -108,9 +108,7 @@ impl Tracer { pub async fn run(&self) -> Result { match self.setting.protocol { Protocol::Udp => probe::udp::run_udp_trace(&self.setting).await, - _ => { - Err(anyhow::anyhow!("Unsupported protocol")) - }, + _ => Err(anyhow::anyhow!("Unsupported protocol")), } } } diff --git a/src/trace/probe/udp.rs b/src/trace/probe/udp.rs index ce4909c..ca702e5 100644 --- a/src/trace/probe/udp.rs +++ b/src/trace/probe/udp.rs @@ -1,17 +1,17 @@ -use anyhow::Result; +use crate::endpoint::NodeType; +use crate::probe::ProbeStatus; use crate::trace::{TraceResult, TraceSetting}; -use std::net::IpAddr; -use std::time::{Duration, Instant}; -use futures::stream::StreamExt; +use crate::{probe::ProbeResult, protocol::Protocol}; +use anyhow::Result; use futures::future::poll_fn; +use futures::stream::StreamExt; use netdev::MacAddr; +use nex::datalink::async_io::{AsyncChannel, async_channel}; use nex::packet::frame::{Frame, ParseOption}; use nex::packet::icmp::IcmpType; use nex::packet::icmpv6::Icmpv6Type; -use crate::endpoint::NodeType; -use crate::probe::ProbeStatus; -use crate::{probe::ProbeResult, protocol::Protocol}; -use nex::datalink::async_io::{async_channel, AsyncChannel}; +use std::net::IpAddr; +use std::time::{Duration, Instant}; use tracing_indicatif::span_ext::IndicatifSpanExt; /// Run a UDP traceroute based on the provided trace settings. @@ -35,15 +35,16 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { promiscuous: false, }; - let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? - else { + let AsyncChannel::Ethernet(mut tx, mut rx) = async_channel(&interface, config)? else { unreachable!(); }; let mut responses: Vec = Vec::new(); let mut parse_option: ParseOption = ParseOption::default(); - if interface.is_tun() || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) { + if interface.is_tun() + || (cfg!(any(target_os = "macos", target_os = "ios")) && interface.is_loopback()) + { let payload_offset = if interface.is_loopback() { 14 } else { 0 }; parse_option.from_ip_packet = true; parse_option.offset = payload_offset; @@ -62,8 +63,7 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { let udp_packet = crate::packet::udp::build_udp_trace_packet(&interface, &setting, seq_ttl); let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &udp_packet)).await { - Ok(_) => { - }, + Ok(_) => {} Err(e) => eprintln!("Failed to send packet: {}", e), } loop { @@ -112,7 +112,14 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { sent_packet_size: udp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("#{} Reply from {}, RTT={:?} TTL={} Type={}", seq_ttl, ipv4_header.source, rtt, ipv4_header.ttl, probe_result.node_type.as_str()); + tracing::info!( + "#{} Reply from {}, RTT={:?} TTL={} Type={}", + seq_ttl, + ipv4_header.source, + rtt, + ipv4_header.ttl, + probe_result.node_type.as_str() + ); responses.push(probe_result); header_span.pb_inc(1); break; @@ -136,7 +143,14 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { sent_packet_size: udp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("#{} Reply from {}, RTT={:?} TTL={} Type={}", seq_ttl, ipv4_header.source, rtt, ipv4_header.ttl, probe_result.node_type.as_str()); + tracing::info!( + "#{} Reply from {}, RTT={:?} TTL={} Type={}", + seq_ttl, + ipv4_header.source, + rtt, + ipv4_header.ttl, + probe_result.node_type.as_str() + ); responses.push(probe_result); header_span.pb_inc(1); dst_reached = true; @@ -161,8 +175,9 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { port_number: None, port_status: None, ttl: ipv6_header.hop_limit, - hop: crate::util::ip::initial_ttl(ipv6_header.hop_limit) - - ipv6_header.hop_limit, + hop: crate::util::ip::initial_ttl( + ipv6_header.hop_limit, + ) - ipv6_header.hop_limit, rtt: rtt, probe_status: ProbeStatus::new(), protocol: Protocol::Udp, @@ -174,11 +189,18 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { sent_packet_size: udp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("#{} Reply from {}, RTT={:?} TTL={} Type={}", seq_ttl, ipv6_header.source, rtt, ipv6_header.hop_limit, probe_result.node_type.as_str()); + tracing::info!( + "#{} Reply from {}, RTT={:?} TTL={} Type={}", + seq_ttl, + ipv6_header.source, + rtt, + ipv6_header.hop_limit, + probe_result.node_type.as_str() + ); responses.push(probe_result); header_span.pb_inc(1); break; - }, + } Icmpv6Type::DestinationUnreachable => { if IpAddr::V6(ipv6_header.source) == setting.dst_ip { let probe_result: ProbeResult = ProbeResult { @@ -189,8 +211,9 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { port_number: None, port_status: None, ttl: ipv6_header.hop_limit, - hop: crate::util::ip::initial_ttl(ipv6_header.hop_limit) - - ipv6_header.hop_limit, + hop: crate::util::ip::initial_ttl( + ipv6_header.hop_limit, + ) - ipv6_header.hop_limit, rtt: rtt, probe_status: ProbeStatus::new(), protocol: Protocol::Udp, @@ -198,7 +221,14 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { sent_packet_size: udp_packet.len(), received_packet_size: packet.len(), }; - tracing::info!("#{} Reply from {}, RTT={:?} TTL={} Type={}", seq_ttl, ipv6_header.source, rtt, ipv6_header.hop_limit, probe_result.node_type.as_str()); + tracing::info!( + "#{} Reply from {}, RTT={:?} TTL={} Type={}", + seq_ttl, + ipv6_header.source, + rtt, + ipv6_header.hop_limit, + probe_result.node_type.as_str() + ); responses.push(probe_result); header_span.pb_inc(1); dst_reached = true; @@ -210,17 +240,17 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { } } } - }, + } Ok(Some(Err(e))) => { tracing::error!("Failed to receive packet: {}", e); header_span.pb_inc(1); break; - }, + } Ok(None) => { tracing::error!("Channel closed"); header_span.pb_inc(1); break; - }, + } Err(_) => { tracing::error!("Request timeout for seq {}", seq_ttl as u32); let probe_result = ProbeResult::timeout( diff --git a/src/util/json.rs b/src/util/json.rs index 47de179..5d980f6 100644 --- a/src/util/json.rs +++ b/src/util/json.rs @@ -1,8 +1,8 @@ +use anyhow::Result; +use serde::Serialize; use std::fs::File; use std::io::Write; use std::path::Path; -use anyhow::Result; -use serde::Serialize; /// JSON output style pub enum JsonStyle { From 715238cc31bae434f51362741baf109b8dfa0616 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 00:19:30 +0900 Subject: [PATCH 02/21] chore: upgrade deps --- Cargo.lock | 205 ++++++++++++++++++++++++++++++++--------- Cargo.toml | 10 +- src/db/oui.rs | 9 ++ src/nei/mod.rs | 10 +- src/scan/probe/icmp.rs | 12 +-- src/scan/probe/tcp.rs | 12 +-- src/scan/probe/udp.rs | 12 +-- 7 files changed, 178 insertions(+), 92 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 69e41dc..bc03b96 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -206,6 +206,15 @@ version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.19.0" @@ -463,6 +472,18 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dispatch2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec" +dependencies = [ + "bitflags 2.9.4", + "block2", + "libc", + "objc2", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -705,6 +726,12 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" @@ -914,6 +941,16 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "indicatif" version = "0.18.0" @@ -1065,12 +1102,30 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mac-addr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3d25b0e0b648a86960ac23b7ad4abb9717601dec6f66c165f5b037f3f03065f" +dependencies = [ + "serde", +] + [[package]] name = "memchr" version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1129,32 +1184,32 @@ dependencies = [ [[package]] name = "ndb-core" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64f689236631fba991a5b369d9a1ff6d8b80beffe41f63b3c2799a59b2d83c5" +checksum = "108c0d5054ede174dce131d107ecca1bf1d802065ea9a561c62d3ebed3ce7b1c" dependencies = [ "serde", ] [[package]] name = "ndb-oui" -version = "0.3.2" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "787e9dfc6693c40148f5123e1f53709eda311f1c2de07b87212bc9ce751db8f4" +checksum = "0fc97af0c84b8b5e392c808d5c557446561fa74f4da21edbcdaddd858da107d9" dependencies = [ "anyhow", "bincode", "csv", - "netdev", + "mac-addr", "rangemap", "serde", ] [[package]] name = "ndb-tcp-service" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a6c17a7056c70fbf51642882574f4f956d4cf23513774ee2302b024ddf9adc" +checksum = "4ee136c79a7f3791a91760ca237d4baec4de732d140e5d4da2b283493796e45d" dependencies = [ "anyhow", "bincode", @@ -1165,9 +1220,9 @@ dependencies = [ [[package]] name = "ndb-udp-service" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2419c0c85f94fa317d29bcf4052bb6b884e199749a2c662020e02b7d5be143c4" +checksum = "144e1644cbd997fb7382100c5534ec311d3fc2fe33cca045edcec7beaf99d46e" dependencies = [ "anyhow", "bincode", @@ -1178,19 +1233,24 @@ dependencies = [ [[package]] name = "netdev" -version = "0.38.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c1c4a111cf1ba52aa040e77e55fd0e2066d03a3d6ea7d8f383725166a60820" +checksum = "dc9815643a243856e7bd84524e1ff739e901e846cfb06ad9627cd2b6d59bd737" dependencies = [ + "block2", + "dispatch2", "dlopen2", "ipnet", "libc", + "mac-addr", "netlink-packet-core", "netlink-packet-route", "netlink-sys", + "objc2-core-foundation", + "objc2-system-configuration", "once_cell", + "plist", "serde", - "system-configuration", "windows-sys 0.59.0", ] @@ -1237,9 +1297,9 @@ dependencies = [ [[package]] name = "nex" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b9259ac90d76abdcc6b94ac0c0e4192426aac7f99ae41a0a7dafd7008dd598" +checksum = "d54423ee704c3a4df300c209ef1be5dd42dc17683ae78b920d0863eded6658c4" dependencies = [ "nex-core", "nex-datalink", @@ -1249,9 +1309,9 @@ dependencies = [ [[package]] name = "nex-core" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eab4e51a2f7d20a50921aaace9f7989a5a1da737f383f2a0b4156e90f7dbb384" +checksum = "f9e8055dfcf3622e5986181c870d545dadfe195a684b3caf1a67aa2caf5a65f3" dependencies = [ "netdev", "serde", @@ -1259,9 +1319,9 @@ dependencies = [ [[package]] name = "nex-datalink" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8ede3ead0cf82254e0c7591aaf7db2443698d5ded5c5fb322e38c0c5d5e123" +checksum = "938606aa1e6187e3c07ec7db051acf21c32e322e1b29cfa8afe4faadc24eda18" dependencies = [ "bytes", "futures-core", @@ -1275,9 +1335,9 @@ dependencies = [ [[package]] name = "nex-packet" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75b48c882ea0e576780c6f48b32f205e9104767347d9da0946750d2e74b84d24" +checksum = "b825e8bea9b5b32c8d00ee8ba32102c674fbb98dd3e6e2d7696fd6975e1519bc" dependencies = [ "bytes", "nex-core", @@ -1287,9 +1347,9 @@ dependencies = [ [[package]] name = "nex-socket" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "255e645ceedc1db4b859442a4ad4f21a2ee6830aa7c4819c064154fec1b82bb4" +checksum = "4514c5ae210acf26e1f7fa2c6299f1cc5b20ef63d7156496a86faa617a38ac71" dependencies = [ "libc", "nex-core", @@ -1302,9 +1362,9 @@ dependencies = [ [[package]] name = "nex-sys" -version = "0.23.2" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613fe5446f03f18d8f976ab9e7fe335becd1f766bc83a09c50d33079877b6070" +checksum = "79a9bff7e7d28a00ad4bfa07d2ee23d48071db085cb850eddd56aacac1c58b01" dependencies = [ "libc", "windows-sys 0.59.0", @@ -1320,6 +1380,7 @@ dependencies = [ "cfg-if", "cfg_aliases", "libc", + "memoffset", ] [[package]] @@ -1430,6 +1491,59 @@ dependencies = [ "libc", ] +[[package]] +name = "objc2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c2599ce0ec54857b29ce62166b0ed9b4f6f1a70ccc9a71165b6154caca8c05" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.9.4", + "block2", + "dispatch2", + "libc", + "objc2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-security" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe137109bd1e8b5a99390f77a7d8b2961dafc1a1c5db8f2e60329ad6d895a" +dependencies = [ + "bitflags 2.9.4", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-system-configuration" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" +dependencies = [ + "bitflags 2.9.4", + "dispatch2", + "libc", + "objc2", + "objc2-core-foundation", + "objc2-security", +] + [[package]] name = "object" version = "0.37.3" @@ -1517,6 +1631,19 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml", + "serde", + "time", +] + [[package]] name = "portable-atomic" version = "1.11.1" @@ -1556,6 +1683,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.38.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -2128,27 +2264,6 @@ dependencies = [ "syn", ] -[[package]] -name = "system-configuration" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tagptr" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 4644445..797e297 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,8 +19,8 @@ tracing-subscriber = { version = "0.3", features = ["time", "chrono"] } serde = { version = "1", features = ["derive"] } serde_json = "1" bytes = "1" -netdev = { version = "0.38", features = ["serde"] } -nex = { version = "0.23", features = ["serde"] } +netdev = { version = "0.40", features = ["serde"] } +nex = { version = "0.25", features = ["serde"] } futures = {version = "0.3", features = ["executor", "thread-pool"]} rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } rustls-native-certs = "0.7" @@ -41,9 +41,9 @@ inquire = "0.6" ipnet = "2.11" num_cpus = "1.17" termtree = "0.5" -ndb-oui = { version = "0.3", features = ["bundled"] } -ndb-tcp-service = { version = "0.3", features = ["bundled"] } -ndb-udp-service = { version = "0.3", features = ["bundled"] } +ndb-oui = { version = "0.4", features = ["bundled"] } +ndb-tcp-service = { version = "0.4", features = ["bundled"] } +ndb-udp-service = { version = "0.4", features = ["bundled"] } home = "0.5" base64 = "0.22" regex = "1.11" diff --git a/src/db/oui.rs b/src/db/oui.rs index a47a3a7..65be108 100644 --- a/src/db/oui.rs +++ b/src/db/oui.rs @@ -17,3 +17,12 @@ pub fn init_oui_db() -> Result<()> { pub fn oui_db() -> &'static OuiDb { OUI_DB.get().expect("OUI_DB not initialized") } + +/// Lookup vendor name from MAC address. +pub fn lookup_vendor_name(mac_addr: &netdev::MacAddr) -> Option { + oui_db().lookup_mac(mac_addr).map(|oui| { + oui.vendor_detail + .clone() + .unwrap_or_else(|| oui.vendor.clone()) + }) +} diff --git a/src/nei/mod.rs b/src/nei/mod.rs index 0d03755..86b836b 100644 --- a/src/nei/mod.rs +++ b/src/nei/mod.rs @@ -45,13 +45,5 @@ impl NeighborDiscoveryResult { /// Lookup the vendor name for a given MAC address using the OUI database. pub fn lookup_vendor(mac: &MacAddr) -> Option { - let oui_db = crate::db::oui::oui_db(); - if let Some(oui) = oui_db.lookup_mac(mac) { - if let Some(vendor_detail) = &oui.vendor_detail { - return Some(vendor_detail.clone()); - } else { - return Some(oui.vendor.clone()); - } - } - None + crate::db::oui::lookup_vendor_name(mac) } diff --git a/src/scan/probe/icmp.rs b/src/scan/probe/icmp.rs index 026a174..afdf990 100644 --- a/src/scan/probe/icmp.rs +++ b/src/scan/probe/icmp.rs @@ -113,7 +113,6 @@ fn parse_hostscan_result( iface: &Interface, dns_map: &HashMap, ) -> ScanResult { - let oui_db = crate::db::oui::oui_db(); let if_ipv4_set: HashSet = iface.ipv4_addrs().into_iter().collect(); let if_ipv6_set: HashSet = iface.ipv6_addrs().into_iter().collect(); let mut result: ScanResult = ScanResult::new(); @@ -165,16 +164,7 @@ fn parse_hostscan_result( continue; } - let vendor_name_opt: Option; - if let Some(oui) = oui_db.lookup_mac(&mac_addr) { - if let Some(vendor_detail) = &oui.vendor_detail { - vendor_name_opt = Some(vendor_detail.clone()); - } else { - vendor_name_opt = Some(oui.vendor.clone()); - } - } else { - vendor_name_opt = None; - } + let vendor_name_opt = crate::db::oui::lookup_vendor_name(&mac_addr); endpoint_map.entry(ip_addr).or_insert(EndpointResult { ip: ip_addr, diff --git a/src/scan/probe/tcp.rs b/src/scan/probe/tcp.rs index 338f330..e318171 100644 --- a/src/scan/probe/tcp.rs +++ b/src/scan/probe/tcp.rs @@ -456,7 +456,6 @@ fn parse_hostscan_result( iface: &Interface, dns_map: &HashMap, ) -> ScanResult { - let oui_db = crate::db::oui::oui_db(); let if_ipv4_set: HashSet = iface.ipv4_addrs().into_iter().collect(); let if_ipv6_set: HashSet = iface.ipv6_addrs().into_iter().collect(); let mut result: ScanResult = ScanResult::new(); @@ -528,16 +527,7 @@ fn parse_hostscan_result( continue; } - let vendor_name_opt: Option; - if let Some(oui) = oui_db.lookup_mac(&mac_addr) { - if let Some(vendor_detail) = &oui.vendor_detail { - vendor_name_opt = Some(vendor_detail.clone()); - } else { - vendor_name_opt = Some(oui.vendor.clone()); - } - } else { - vendor_name_opt = None; - } + let vendor_name_opt = crate::db::oui::lookup_vendor_name(&mac_addr); let mut os_guess = OsGuess::default().with_ttl_observed(ttl); let mut cpes: Vec = Vec::new(); diff --git a/src/scan/probe/udp.rs b/src/scan/probe/udp.rs index 7651c04..c2e9472 100644 --- a/src/scan/probe/udp.rs +++ b/src/scan/probe/udp.rs @@ -121,7 +121,6 @@ fn parse_hostscan_result( iface: &Interface, dns_map: &HashMap, ) -> ScanResult { - let oui_db = crate::db::oui::oui_db(); let if_ipv4_set: HashSet = iface.ipv4_addrs().into_iter().collect(); let if_ipv6_set: HashSet = iface.ipv6_addrs().into_iter().collect(); let mut result: ScanResult = ScanResult::new(); @@ -173,16 +172,7 @@ fn parse_hostscan_result( continue; } - let vendor_name_opt: Option; - if let Some(oui) = oui_db.lookup_mac(&mac_addr) { - if let Some(vendor_detail) = &oui.vendor_detail { - vendor_name_opt = Some(vendor_detail.clone()); - } else { - vendor_name_opt = Some(oui.vendor.clone()); - } - } else { - vendor_name_opt = None; - } + let vendor_name_opt = crate::db::oui::lookup_vendor_name(&mac_addr); endpoint_map.entry(ip_addr).or_insert(EndpointResult { ip: ip_addr, From 4c640e9fa8b42e68df2fdca64d91721f410d43d5 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 00:36:21 +0900 Subject: [PATCH 03/21] refactor: remove panic-prone paths --- src/nei/arp.rs | 8 +++++--- src/nei/ndp.rs | 8 ++++---- src/os/probe/tcp.rs | 8 ++++++-- src/output/domain.rs | 2 +- src/packet/arp.rs | 10 +++++----- src/packet/icmp.rs | 16 ++++++++++++---- src/packet/ndp.rs | 9 +++++---- src/packet/tcp.rs | 9 ++++++--- src/packet/udp.rs | 21 +++++++++++++-------- src/ping/probe/icmp.rs | 9 +++------ src/ping/probe/tcp.rs | 9 +++------ src/ping/probe/udp.rs | 9 +++------ src/scan/probe/icmp.rs | 7 ++++--- src/scan/probe/tcp.rs | 14 ++++++++------ src/scan/probe/udp.rs | 9 +++++---- src/trace/probe/udp.rs | 9 +++------ 16 files changed, 86 insertions(+), 71 deletions(-) diff --git a/src/nei/arp.rs b/src/nei/arp.rs index 2de7136..6939167 100644 --- a/src/nei/arp.rs +++ b/src/nei/arp.rs @@ -38,19 +38,21 @@ pub async fn send_arp( unreachable!(); }; - let arp_packet = crate::packet::arp::build_arp_packet(iface, next_hop); + let arp_packet = crate::packet::arp::build_arp_packet(iface, next_hop)?; let start_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &arp_packet)).await { Ok(_) => {} - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } loop { match tokio::time::timeout(recv_timeout, rx.next()).await { Ok(Some(Ok(packet))) => { - let frame = Frame::from_buf(&packet, ParseOption::default()).unwrap(); + let Some(frame) = Frame::from_buf(&packet, ParseOption::default()) else { + continue; + }; match &frame.datalink { Some(dlink) => { if let Some(arp) = &dlink.arp { diff --git a/src/nei/ndp.rs b/src/nei/ndp.rs index f7495ee..64dacf7 100644 --- a/src/nei/ndp.rs +++ b/src/nei/ndp.rs @@ -44,13 +44,13 @@ pub async fn send_ndp( unreachable!(); }; - let arp_packet = crate::packet::ndp::build_ndp_packet(iface, next_hop); + let ndp_packet = crate::packet::ndp::build_ndp_packet(iface, next_hop)?; let start_time = Instant::now(); - match poll_fn(|cx| tx.poll_send(cx, &arp_packet)).await { + match poll_fn(|cx| tx.poll_send(cx, &ndp_packet)).await { Ok(_) => {} - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } loop { @@ -87,7 +87,7 @@ pub async fn send_ndp( }; return Ok(ndp_result); } else { - eprintln!( + tracing::debug!( "Received NDP reply from unexpected source: {}", ipv6_hdr.source ); diff --git a/src/os/probe/tcp.rs b/src/os/probe/tcp.rs index baf8776..17162ea 100644 --- a/src/os/probe/tcp.rs +++ b/src/os/probe/tcp.rs @@ -54,8 +54,12 @@ pub async fn run_os_probe(setting: ProbeSetting) -> Result { let mut detected: bool = false; for port in &target.ports { - let packet = - crate::packet::tcp::build_tcp_syn_packet(&interface, target.ip, port.number, false); + let packet = crate::packet::tcp::build_tcp_syn_packet( + &interface, + target.ip, + port.number, + false, + )?; // Send a packet using poll_fn. match poll_fn(|cx| tx.poll_send(cx, &packet)).await { diff --git a/src/output/domain.rs b/src/output/domain.rs index 60d03e9..945c8fb 100644 --- a/src/output/domain.rs +++ b/src/output/domain.rs @@ -9,7 +9,7 @@ use termtree::Tree; pub fn print_domain_tree(base_domain: &Domain, res: &DomainScanResult) { // Create the root of the tree let mut root = Tree::new(format!( - "Subdomains of {} — found: {} (elapsed: {:?})", + "Subdomains of {} - found: {} (elapsed: {:?})", base_domain.name, res.domains.len(), res.scan_time diff --git a/src/packet/arp.rs b/src/packet/arp.rs index 5c4413e..db854e5 100644 --- a/src/packet/arp.rs +++ b/src/packet/arp.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use netdev::Interface; use nex::net::mac::MacAddr; use nex::packet::builder::arp::ArpPacketBuilder; @@ -7,7 +8,7 @@ use nex::packet::packet::Packet; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; /// Build ARP packet -pub fn build_arp_packet(interface: &Interface, dst_ip: IpAddr) -> Vec { +pub fn build_arp_packet(interface: &Interface, dst_ip: IpAddr) -> Result> { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let src_ipv4 = crate::interface::get_interface_ipv4(interface).unwrap_or(Ipv4Addr::UNSPECIFIED); let src_global_ipv6 = @@ -41,16 +42,15 @@ pub fn build_arp_packet(interface: &Interface, dst_ip: IpAddr) -> Vec { let packet = eth_builder.payload(arp_builder.build().to_bytes()).build(); - return packet.to_bytes().to_vec(); + return Ok(packet.to_bytes().to_vec()); } IpAddr::V6(_) => { - // ARP is not used with IPv6, return empty vector - return Vec::new(); + anyhow::bail!("ARP is not used with IPv6"); } } } IpAddr::V6(_) => { - return Vec::new(); // ARP is not used with IPv6 + anyhow::bail!("ARP is not used with IPv6"); } } } diff --git a/src/packet/icmp.rs b/src/packet/icmp.rs index 91575c7..b1a23ab 100644 --- a/src/packet/icmp.rs +++ b/src/packet/icmp.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use bytes::Bytes; use netdev::{Interface, MacAddr}; use nex::packet::builder::ethernet::EthernetPacketBuilder; @@ -16,7 +17,11 @@ use nex::packet::packet::Packet; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; /// Build ICMP packet. Supports both ICMPv4 and ICMPv6 -pub fn build_icmp_packet(interface: &Interface, dst_ip: IpAddr, is_ip_packet: bool) -> Vec { +pub fn build_icmp_packet( + interface: &Interface, + dst_ip: IpAddr, + is_ip_packet: bool, +) -> Result> { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let dst_mac = match &interface.gateway { Some(gateway) => gateway.mac_addr, @@ -54,7 +59,7 @@ pub fn build_icmp_packet(interface: &Interface, dst_ip: IpAddr, is_ip_packet: bo .payload(Bytes::from_static(b"hello")) .build() .to_bytes(), - _ => panic!("Source and destination IP version mismatch"), + _ => anyhow::bail!("source and destination IP version mismatch"), }; let ip_packet = match (src_ip, dst_ip) { @@ -95,8 +100,11 @@ pub fn build_icmp_packet(interface: &Interface, dst_ip: IpAddr, is_ip_packet: bo .build(); if is_ip_packet { - ethernet_packet.ip_packet().unwrap().to_vec() + Ok(ethernet_packet + .ip_packet() + .ok_or_else(|| anyhow::anyhow!("failed to extract IP packet payload"))? + .to_vec()) } else { - ethernet_packet.to_bytes().to_vec() + Ok(ethernet_packet.to_bytes().to_vec()) } } diff --git a/src/packet/ndp.rs b/src/packet/ndp.rs index 3f27e42..fa86183 100644 --- a/src/packet/ndp.rs +++ b/src/packet/ndp.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use netdev::Interface; use nex::net::mac::MacAddr; use nex::packet::builder::ethernet::EthernetPacketBuilder; @@ -22,7 +23,7 @@ fn ipv6_multicast_mac(ipv6: &Ipv6Addr) -> MacAddr { } /// Build NDP packet -pub fn build_ndp_packet(interface: &Interface, dst_ip: IpAddr) -> Vec { +pub fn build_ndp_packet(interface: &Interface, dst_ip: IpAddr) -> Result> { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let src_ipv4 = crate::interface::get_interface_ipv4(interface).unwrap_or(Ipv4Addr::UNSPECIFIED); let src_global_ipv6 = @@ -44,7 +45,7 @@ pub fn build_ndp_packet(interface: &Interface, dst_ip: IpAddr) -> Vec { //let ndp_payload_len = (NDP_SOL_PACKET_LEN + NDP_OPT_PACKET_LEN + MAC_ADDR_LEN) as u16; match (src_ip, dst_ip) { (IpAddr::V4(_), IpAddr::V4(_)) => { - panic!("NDP is not used with IPv4 addresses"); + anyhow::bail!("NDP is not used with IPv4 addresses"); } (IpAddr::V6(src), IpAddr::V6(dst)) => { let ipv6 = Ipv6PacketBuilder::new() @@ -64,8 +65,8 @@ pub fn build_ndp_packet(interface: &Interface, dst_ip: IpAddr) -> Vec { .payload(ipv6.payload(ndp.build().to_bytes()).build().to_bytes()); let packet = ethernet.build().to_bytes(); - packet.to_vec() + Ok(packet.to_vec()) } - _ => panic!("Source and destination IP versions must match"), + _ => anyhow::bail!("source and destination IP versions must match"), } } diff --git a/src/packet/tcp.rs b/src/packet/tcp.rs index 42f1789..e8a2237 100644 --- a/src/packet/tcp.rs +++ b/src/packet/tcp.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use bytes::Bytes; use netdev::{Interface, MacAddr}; use nex::packet::builder::{ @@ -19,7 +20,7 @@ pub fn build_tcp_syn_packet( dst_ip: IpAddr, dst_port: u16, is_ip_packet: bool, -) -> Vec { +) -> Result> { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let dst_mac = match &interface.gateway { Some(gateway) => gateway.mac_addr, @@ -97,9 +98,11 @@ pub fn build_tcp_syn_packet( .build(); let packet: Bytes = if is_ip_packet { - ethernet_packet.ip_packet().unwrap() + ethernet_packet + .ip_packet() + .ok_or_else(|| anyhow::anyhow!("failed to extract IP packet payload"))? } else { ethernet_packet.to_bytes() }; - packet.to_vec() + Ok(packet.to_vec()) } diff --git a/src/packet/udp.rs b/src/packet/udp.rs index 3a6ab3d..c8babfc 100644 --- a/src/packet/udp.rs +++ b/src/packet/udp.rs @@ -1,3 +1,4 @@ +use anyhow::Result; use bytes::Bytes; use netdev::{Interface, MacAddr}; use nex::packet::builder::{ @@ -19,7 +20,7 @@ pub fn build_udp_packet( dst_ip: IpAddr, dst_port: u16, is_ip_packet: bool, -) -> Vec { +) -> Result> { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let dst_mac = match &interface.gateway { Some(gateway) => gateway.mac_addr, @@ -63,7 +64,7 @@ pub fn build_udp_packet( .payload(udp_packet.to_bytes()) .build() .to_bytes(), - _ => panic!("Source and destination IP version mismatch"), + _ => anyhow::bail!("source and destination IP version mismatch"), }; let ethernet_packet = EthernetPacketBuilder::new() @@ -85,12 +86,14 @@ pub fn build_udp_packet( .build(); let packet: Bytes = if is_ip_packet { - ethernet_packet.ip_packet().unwrap() + ethernet_packet + .ip_packet() + .ok_or_else(|| anyhow::anyhow!("failed to extract IP packet payload"))? } else { ethernet_packet.to_bytes() }; - packet.to_vec() + Ok(packet.to_vec()) } /// Build UDP packet for traceroute with specific TTL @@ -98,7 +101,7 @@ pub fn build_udp_trace_packet( interface: &Interface, trace_setting: &TraceSetting, seq_ttl: u8, -) -> Vec { +) -> Result> { let src_mac = interface.mac_addr.unwrap_or(MacAddr::zero()); let dst_mac = match &interface.gateway { Some(gateway) => gateway.mac_addr, @@ -148,7 +151,7 @@ pub fn build_udp_trace_packet( .payload(udp_packet.to_bytes()) .build() .to_bytes(), - _ => panic!("Source and destination IP version mismatch"), + _ => anyhow::bail!("source and destination IP version mismatch"), }; let ethernet_packet = EthernetPacketBuilder::new() @@ -170,10 +173,12 @@ pub fn build_udp_trace_packet( .build(); let packet: Bytes = if is_ip_packet { - ethernet_packet.ip_packet().unwrap() + ethernet_packet + .ip_packet() + .ok_or_else(|| anyhow::anyhow!("failed to extract IP packet payload"))? } else { ethernet_packet.to_bytes() }; - packet.to_vec() + Ok(packet.to_vec()) } diff --git a/src/ping/probe/icmp.rs b/src/ping/probe/icmp.rs index e8efb43..8605279 100644 --- a/src/ping/probe/icmp.rs +++ b/src/ping/probe/icmp.rs @@ -64,12 +64,12 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { header_span.pb_start(); let start_time = Instant::now(); - let icmp_packet = crate::packet::icmp::build_icmp_packet(&interface, setting.dst_ip, false); + let icmp_packet = crate::packet::icmp::build_icmp_packet(&interface, setting.dst_ip, false)?; for seq in 1..setting.count + 1 { let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &icmp_packet)).await { Ok(_) => {} - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } loop { match tokio::time::timeout(setting.receive_timeout, rx.next()).await { @@ -77,10 +77,7 @@ pub async fn run_icmp_ping(setting: &PingSetting) -> Result { let rtt = send_time.elapsed(); let frame = match Frame::from_buf(&packet, parse_option.clone()) { Some(frame) => frame, - None => { - eprintln!("Failed to parse packet: {:?}", packet); - continue; - } + None => continue, }; let mut mac_addr: MacAddr = MacAddr::zero(); if let Some(datalink_layer) = &frame.datalink { diff --git a/src/ping/probe/tcp.rs b/src/ping/probe/tcp.rs index 645c8dc..b954bff 100644 --- a/src/ping/probe/tcp.rs +++ b/src/ping/probe/tcp.rs @@ -65,12 +65,12 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { let start_time = Instant::now(); let tcp_packet = - crate::packet::tcp::build_tcp_syn_packet(&interface, setting.dst_ip, dst_port, false); + crate::packet::tcp::build_tcp_syn_packet(&interface, setting.dst_ip, dst_port, false)?; for seq in 1..setting.count + 1 { let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &tcp_packet)).await { Ok(_) => {} - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } loop { match tokio::time::timeout(setting.receive_timeout, rx.next()).await { @@ -78,10 +78,7 @@ pub async fn run_tcp_ping(setting: &PingSetting) -> Result { let rtt = send_time.elapsed(); let frame = match Frame::from_buf(&packet, parse_option.clone()) { Some(frame) => frame, - None => { - eprintln!("Failed to parse packet: {:?}", packet); - continue; - } + None => continue, }; let mut mac_addr: MacAddr = MacAddr::zero(); if let Some(datalink_layer) = &frame.datalink { diff --git a/src/ping/probe/udp.rs b/src/ping/probe/udp.rs index 50f231f..6e8ac91 100644 --- a/src/ping/probe/udp.rs +++ b/src/ping/probe/udp.rs @@ -70,12 +70,12 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { setting.dst_ip, DEFAULT_BASE_TARGET_UDP_PORT, false, - ); + )?; for seq in 1..setting.count + 1 { let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &udp_packet)).await { Ok(_) => {} - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } loop { match tokio::time::timeout(setting.receive_timeout, rx.next()).await { @@ -83,10 +83,7 @@ pub async fn run_udp_ping(setting: &PingSetting) -> Result { let rtt = send_time.elapsed(); let frame = match Frame::from_buf(&packet, parse_option.clone()) { Some(frame) => frame, - None => { - eprintln!("Failed to parse packet: {:?}", packet); - continue; - } + None => continue, }; let mut mac_addr: MacAddr = MacAddr::zero(); if let Some(datalink_layer) = &frame.datalink { diff --git a/src/scan/probe/icmp.rs b/src/scan/probe/icmp.rs index afdf990..b7bce1d 100644 --- a/src/scan/probe/icmp.rs +++ b/src/scan/probe/icmp.rs @@ -17,7 +17,7 @@ pub async fn send_hostscan_packets( tx: &mut Box, interface: &Interface, scan_setting: &ProbeSetting, -) { +) -> Result<()> { let header_span = tracing::info_span!("icmp_host_scan"); header_span.pb_set_style(&crate::output::progress::get_progress_style()); header_span.pb_set_message("HostScan"); @@ -26,7 +26,7 @@ pub async fn send_hostscan_packets( header_span.pb_start(); for target in &scan_setting.target_endpoints { - let packet = crate::packet::icmp::build_icmp_packet(&interface, target.ip, false); + let packet = crate::packet::icmp::build_icmp_packet(interface, target.ip, false)?; // Send a packet using poll_fn. match poll_fn(|cx| tx.poll_send(cx, &packet)).await { Ok(_) => { @@ -39,6 +39,7 @@ pub async fn send_hostscan_packets( header_span.pb_inc(1); } drop(header_span); + Ok(()) } /// Run host scan using ICMP Echo Request packets and return the results. @@ -96,7 +97,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { let _ = ready_rx; let start_time = std::time::Instant::now(); // Send probe packets - send_hostscan_packets(&mut tx, &interface, &setting).await; + send_hostscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); diff --git a/src/scan/probe/tcp.rs b/src/scan/probe/tcp.rs index e318171..c9327e9 100644 --- a/src/scan/probe/tcp.rs +++ b/src/scan/probe/tcp.rs @@ -135,7 +135,7 @@ pub async fn send_portscan_packets( tx: &mut Box, interface: &Interface, scan_setting: &ProbeSetting, -) { +) -> Result<()> { let mut sent: usize = 0; for target in &scan_setting.target_endpoints { let header_span = tracing::info_span!("tcp_syn_scan"); @@ -147,7 +147,7 @@ pub async fn send_portscan_packets( for port in &target.ports { let packet = - crate::packet::tcp::build_tcp_syn_packet(&interface, target.ip, port.number, false); + crate::packet::tcp::build_tcp_syn_packet(interface, target.ip, port.number, false)?; // Send a packet using poll_fn. match poll_fn(|cx| tx.poll_send(cx, &packet)).await { @@ -163,6 +163,7 @@ pub async fn send_portscan_packets( } drop(header_span); } + Ok(()) } /// Send TCP SYN packets for host scanning. @@ -170,7 +171,7 @@ pub async fn send_hostscan_packets( tx: &mut Box, interface: &Interface, scan_setting: &ProbeSetting, -) { +) -> Result<()> { let header_span = tracing::info_span!("tcp_syn_host_scan"); header_span.pb_set_style(&crate::output::progress::get_progress_style()); header_span.pb_set_message("HostScan"); @@ -181,7 +182,7 @@ pub async fn send_hostscan_packets( for target in &scan_setting.target_endpoints { for port in &target.ports { let packet = - crate::packet::tcp::build_tcp_syn_packet(&interface, target.ip, port.number, false); + crate::packet::tcp::build_tcp_syn_packet(interface, target.ip, port.number, false)?; // Send a packet using poll_fn. match poll_fn(|cx| tx.poll_send(cx, &packet)).await { @@ -196,6 +197,7 @@ pub async fn send_hostscan_packets( header_span.pb_inc(1); } drop(header_span); + Ok(()) } /// Run a TCP SYN scan based on the provided probe settings. @@ -255,7 +257,7 @@ pub async fn run_syn_scan(setting: ProbeSetting) -> Result { let _ = ready_rx; let start_time = std::time::Instant::now(); // Send probe packets - send_portscan_packets(&mut tx, &interface, &setting).await; + send_portscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); @@ -335,7 +337,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { let _ = ready_rx; let start_time = std::time::Instant::now(); // Send probe packets - send_hostscan_packets(&mut tx, &interface, &setting).await; + send_hostscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); diff --git a/src/scan/probe/udp.rs b/src/scan/probe/udp.rs index c2e9472..f214132 100644 --- a/src/scan/probe/udp.rs +++ b/src/scan/probe/udp.rs @@ -19,7 +19,7 @@ pub async fn send_hostscan_packets( tx: &mut Box, interface: &Interface, scan_setting: &ProbeSetting, -) { +) -> Result<()> { let header_span = tracing::info_span!("udp_host_scan"); header_span.pb_set_style(&crate::output::progress::get_progress_style()); header_span.pb_set_message("HostScan"); @@ -29,11 +29,11 @@ pub async fn send_hostscan_packets( for target in &scan_setting.target_endpoints { let packet = crate::packet::udp::build_udp_packet( - &interface, + interface, target.ip, DEFAULT_BASE_TARGET_UDP_PORT, false, - ); + )?; // Send a packet using poll_fn. match poll_fn(|cx| tx.poll_send(cx, &packet)).await { Ok(_) => { @@ -46,6 +46,7 @@ pub async fn send_hostscan_packets( header_span.pb_inc(1); } drop(header_span); + Ok(()) } /// Run a UDP host scan based on the provided probe settings. @@ -104,7 +105,7 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { let _ = ready_rx; let start_time = std::time::Instant::now(); // Send probe packets - send_hostscan_packets(&mut tx, &interface, &setting).await; + send_hostscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); diff --git a/src/trace/probe/udp.rs b/src/trace/probe/udp.rs index ca702e5..2579fdf 100644 --- a/src/trace/probe/udp.rs +++ b/src/trace/probe/udp.rs @@ -60,11 +60,11 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { let mut dst_reached: bool = false; let start_time = Instant::now(); for seq_ttl in 1..setting.hop_limit { - let udp_packet = crate::packet::udp::build_udp_trace_packet(&interface, &setting, seq_ttl); + let udp_packet = crate::packet::udp::build_udp_trace_packet(&interface, setting, seq_ttl)?; let send_time = Instant::now(); match poll_fn(|cx| tx.poll_send(cx, &udp_packet)).await { Ok(_) => {} - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } loop { match tokio::time::timeout(setting.receive_timeout, rx.next()).await { @@ -72,10 +72,7 @@ pub async fn run_udp_trace(setting: &TraceSetting) -> Result { let rtt = send_time.elapsed(); let frame = match Frame::from_buf(&packet, parse_option.clone()) { Some(frame) => frame, - None => { - eprintln!("Failed to parse packet: {:?}", packet); - continue; - } + None => continue, }; let mut mac_addr: MacAddr = MacAddr::zero(); if let Some(datalink_layer) = &frame.datalink { From 5fbe2e3b271dbf89c91136b2d4aa82a6eda7c75a Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 00:42:22 +0900 Subject: [PATCH 04/21] refactor: improve error handling and remove unwraps --- src/os/probe/tcp.rs | 32 ++++++++++++-------------------- src/scan/probe/icmp.rs | 8 +++++--- src/scan/probe/tcp.rs | 35 +++++++++++++++++++++++------------ src/scan/probe/udp.rs | 8 +++++--- 4 files changed, 45 insertions(+), 38 deletions(-) diff --git a/src/os/probe/tcp.rs b/src/os/probe/tcp.rs index 17162ea..f9de7eb 100644 --- a/src/os/probe/tcp.rs +++ b/src/os/probe/tcp.rs @@ -73,16 +73,15 @@ pub async fn run_os_probe(setting: ProbeSetting) -> Result { Ok(Some(Ok(packet))) => { let frame = match Frame::from_buf(&packet, parse_option.clone()) { Some(frame) => frame, - None => { - eprintln!("Failed to parse packet: {:?}", packet); - continue; - } + None => continue, }; - if frame.ip.is_none() || frame.transport.is_none() { + let Some(ip) = &frame.ip else { continue; - } + }; + let Some(transport) = &frame.transport else { + continue; + }; let ttl: u8; - let ip = frame.ip.as_ref().unwrap(); if let Some(ipv4) = &ip.ipv4 { if ipv4.source != target.ip { continue; @@ -96,24 +95,17 @@ pub async fn run_os_probe(setting: ProbeSetting) -> Result { } else { continue; } - if let Some(transport) = &frame.transport { - if let Some(tcp) = &transport.tcp { - if tcp.destination != DEFAULT_LOCAL_TCP_PORT { - continue; - } - if tcp.options.len() == 0 { - continue; - } - } else { + if let Some(tcp) = &transport.tcp { + if tcp.destination != DEFAULT_LOCAL_TCP_PORT { + continue; + } + if tcp.options.is_empty() { continue; } } else { continue; } - tracing::debug!( - "Matching frame...: {:?}", - frame.transport.as_ref().unwrap().tcp - ); + tracing::debug!("Matching frame...: {:?}", transport.tcp); match crate::os::match_tcpip_signatures(&frame) { Some(os_match) => { let port_result = PortResult { diff --git a/src/scan/probe/icmp.rs b/src/scan/probe/icmp.rs index b7bce1d..a02f2c2 100644 --- a/src/scan/probe/icmp.rs +++ b/src/scan/probe/icmp.rs @@ -34,7 +34,7 @@ pub async fn send_hostscan_packets( tokio::time::sleep(scan_setting.send_rate).await; } } - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } header_span.pb_inc(1); } @@ -94,14 +94,16 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { }); // Wait for listener to start - let _ = ready_rx; + let _ = ready_rx.await; let start_time = std::time::Instant::now(); // Send probe packets send_hostscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); - let frames = capture_handle.await.unwrap(); + let frames = capture_handle + .await + .map_err(|e| anyhow::anyhow!("capture task join error: {}", e))?; let dns_map = setting.get_dns_map(); let mut result = parse_hostscan_result(frames, &interface, &dns_map); result.scan_time = start_time.elapsed(); diff --git a/src/scan/probe/tcp.rs b/src/scan/probe/tcp.rs index c9327e9..2640eaa 100644 --- a/src/scan/probe/tcp.rs +++ b/src/scan/probe/tcp.rs @@ -53,18 +53,25 @@ pub async fn try_connect_ports( move |socket_addr| { let ch_tx = ch_tx.clone(); async move { - let cfg = if socket_addr.is_ipv4() { - TcpConfig::v4_stream() - } else { - TcpConfig::v6_stream() - }; - let socket = AsyncTcpSocket::from_config(&cfg).unwrap(); let mut port_result = PortResult { port: Port::new(socket_addr.port(), TransportProtocol::Tcp), state: PortState::Closed, service: ServiceInfo::default(), rtt_ms: None, }; + let cfg = if socket_addr.is_ipv4() { + TcpConfig::v4_stream() + } else { + TcpConfig::v6_stream() + }; + let socket = match AsyncTcpSocket::from_config(&cfg) { + Ok(socket) => socket, + Err(e) => { + tracing::error!("failed to create TCP socket: {}", e); + let _ = ch_tx.send(port_result); + return; + } + }; match socket.connect_timeout(socket_addr, timeout).await { Ok(mut stream) => { port_result.state = PortState::Open; @@ -157,7 +164,7 @@ pub async fn send_portscan_packets( } sent += 1; } - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } header_span.pb_inc(1); } @@ -191,7 +198,7 @@ pub async fn send_hostscan_packets( tokio::time::sleep(scan_setting.send_rate).await; } } - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } } header_span.pb_inc(1); @@ -254,14 +261,16 @@ pub async fn run_syn_scan(setting: ProbeSetting) -> Result { }); // Wait for listener to start - let _ = ready_rx; + let _ = ready_rx.await; let start_time = std::time::Instant::now(); // Send probe packets send_portscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); - let frames = capture_handle.await.unwrap(); + let frames = capture_handle + .await + .map_err(|e| anyhow::anyhow!("capture task join error: {}", e))?; let dns_map = setting.get_dns_map(); let mut result = parse_portscan_result(frames, &interface, &dns_map); result.scan_time = start_time.elapsed(); @@ -334,14 +343,16 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { }); // Wait for listener to start - let _ = ready_rx; + let _ = ready_rx.await; let start_time = std::time::Instant::now(); // Send probe packets send_hostscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); - let frames = capture_handle.await.unwrap(); + let frames = capture_handle + .await + .map_err(|e| anyhow::anyhow!("capture task join error: {}", e))?; let dns_map = setting.get_dns_map(); let mut result = parse_hostscan_result(frames, &interface, &dns_map); result.scan_time = start_time.elapsed(); diff --git a/src/scan/probe/udp.rs b/src/scan/probe/udp.rs index f214132..6bdf0bb 100644 --- a/src/scan/probe/udp.rs +++ b/src/scan/probe/udp.rs @@ -41,7 +41,7 @@ pub async fn send_hostscan_packets( tokio::time::sleep(scan_setting.send_rate).await; } } - Err(e) => eprintln!("Failed to send packet: {}", e), + Err(e) => tracing::error!("Failed to send packet: {}", e), } header_span.pb_inc(1); } @@ -102,14 +102,16 @@ pub async fn run_host_scan(setting: ProbeSetting) -> Result { }); // Wait for listener to start - let _ = ready_rx; + let _ = ready_rx.await; let start_time = std::time::Instant::now(); // Send probe packets send_hostscan_packets(&mut tx, &interface, &setting).await?; tokio::time::sleep(setting.wait_time).await; // Stop pcap let _ = stop_tx.send(()); - let frames = capture_handle.await.unwrap(); + let frames = capture_handle + .await + .map_err(|e| anyhow::anyhow!("capture task join error: {}", e))?; let dns_map = setting.get_dns_map(); let mut result = parse_hostscan_result(frames, &interface, &dns_map); result.scan_time = start_time.elapsed(); From 57aa2758029d0a344fe502655cc396efb71d19a5 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 01:03:17 +0900 Subject: [PATCH 05/21] refactor: remove panic-prone unwraps --- src/capture/pcap.rs | 33 +++++++++++++++++++++++++-------- src/interface.rs | 35 ++++++++++++++++++++++------------- src/output/progress.rs | 16 ++++++++-------- src/service/probe/quic.rs | 21 ++++++++++----------- 4 files changed, 65 insertions(+), 40 deletions(-) diff --git a/src/capture/pcap.rs b/src/capture/pcap.rs index 15ad5b1..7ab5e47 100644 --- a/src/capture/pcap.rs +++ b/src/capture/pcap.rs @@ -88,10 +88,27 @@ impl PacketCaptureOptions { Some(options) } pub fn from_interface_name(if_name: String) -> PacketCaptureOptions { - let iface = interface::get_interface_by_name(if_name).unwrap(); + let iface = interface::get_interface_by_name(if_name.clone()).or_else(|| { + tracing::warn!( + "interface '{}' not found, falling back to default interface", + if_name + ); + netdev::get_default_interface().ok() + }); + let (interface_index, interface_name, tunnel, loopback) = if let Some(iface) = iface { + ( + iface.index, + iface.name.clone(), + iface.is_tun(), + iface.is_loopback(), + ) + } else { + tracing::warn!("no usable interface found, using index=0 placeholder capture options"); + (0, if_name, false, false) + }; let options = PacketCaptureOptions { - interface_index: iface.index, - interface_name: iface.name.clone(), + interface_index, + interface_name, src_ips: HashSet::new(), dst_ips: HashSet::new(), src_ports: HashSet::new(), @@ -102,8 +119,8 @@ impl PacketCaptureOptions { read_timeout: Duration::from_millis(200), promiscuous: false, receive_undefined: true, - tunnel: iface.is_tun(), - loopback: iface.is_loopback(), + tunnel, + loopback, }; options } @@ -137,7 +154,7 @@ pub async fn start_capture( ) -> Vec { let mut frames = Vec::new(); let start_time = Instant::now(); - ready_tx.send(()).unwrap(); + let _ = ready_tx.send(()); loop { tokio::select! { _ = &mut *stop_rx => break, @@ -157,11 +174,11 @@ pub async fn start_capture( frames.push(frame); } } else { - eprintln!("Error parsing packet"); + tracing::debug!("Error parsing packet"); } } Some(Err(e)) => { - eprintln!("Error reading packet: {}", e); + tracing::error!("Error reading packet: {}", e); break; } None => {} diff --git a/src/interface.rs b/src/interface.rs index 6db0a2e..7f3fe67 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -84,13 +84,16 @@ pub fn get_interface_ips(iface: &Interface) -> Vec { /// Get all local IP addresses on the specified interface. pub fn get_local_ips(if_index: u32) -> HashSet { - let interface = get_interface_by_index(if_index).unwrap(); let mut ips: HashSet = HashSet::new(); - for ip in interface.ipv4.clone() { - ips.insert(IpAddr::V4(ip.addr())); - } - for ip in interface.ipv6.clone() { - ips.insert(IpAddr::V6(ip.addr())); + if let Some(interface) = get_interface_by_index(if_index) { + for ip in interface.ipv4.clone() { + ips.insert(IpAddr::V4(ip.addr())); + } + for ip in interface.ipv6.clone() { + ips.insert(IpAddr::V6(ip.addr())); + } + } else { + tracing::warn!("interface not found for index {}", if_index); } // localhost IP addresses ips.insert(IpAddr::V4(Ipv4Addr::LOCALHOST)); @@ -100,14 +103,20 @@ pub fn get_local_ips(if_index: u32) -> HashSet { /// Get all local IP addresses on the default interface. pub fn get_default_local_ips() -> HashSet { - // Default interface IP addresses - let default_interface = netdev::get_default_interface().unwrap(); let mut ips: HashSet = HashSet::new(); - for ip in default_interface.ipv4.clone() { - ips.insert(IpAddr::V4(ip.addr())); - } - for ip in default_interface.ipv6.clone() { - ips.insert(IpAddr::V6(ip.addr())); + // Default interface IP addresses + match netdev::get_default_interface() { + Ok(default_interface) => { + for ip in default_interface.ipv4.clone() { + ips.insert(IpAddr::V4(ip.addr())); + } + for ip in default_interface.ipv6.clone() { + ips.insert(IpAddr::V6(ip.addr())); + } + } + Err(e) => { + tracing::warn!("failed to get default interface: {}", e); + } } // localhost IP addresses ips.insert(IpAddr::V4(Ipv4Addr::LOCALHOST)); diff --git a/src/output/progress.rs b/src/output/progress.rs index 9832f9e..e262d5e 100644 --- a/src/output/progress.rs +++ b/src/output/progress.rs @@ -2,14 +2,14 @@ use indicatif::{ProgressState, ProgressStyle}; /// Get a progress bar style with a custom elapsed time formatter. pub fn get_progress_style() -> ProgressStyle { - ProgressStyle::default_bar() - .template( - "{spinner:.green} {msg} [{elapsed_precise_subsec}] [{bar:40.cyan/blue}] {pos}/{len}", - ) - .unwrap() - .with_key("elapsed_precise_subsec", elapsed_precise_subsec) - .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", "✓"]) - .progress_chars("#>-") + let tpl = "{spinner:.green} {msg} [{elapsed_precise_subsec}] [{bar:40.cyan/blue}] {pos}/{len}"; + match ProgressStyle::default_bar().template(tpl) { + Ok(style) => style, + Err(_) => ProgressStyle::default_bar(), + } + .with_key("elapsed_precise_subsec", elapsed_precise_subsec) + .tick_strings(&["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏", "✓"]) + .progress_chars("#>-") } /// Custom formatter for elapsed time with millisecond precision. diff --git a/src/service/probe/quic.rs b/src/service/probe/quic.rs index c39691f..48ab2f0 100644 --- a/src/service/probe/quic.rs +++ b/src/service/probe/quic.rs @@ -50,15 +50,14 @@ impl QuicProbe { b"hq-29".as_slice(), ]; let client_cfg = quic_client_config(ctx.skip_cert_verify, &alpn)?; - let mut endpoint = Endpoint::client( - (if ctx.ip.is_ipv6() { - "[::]:0" - } else { - "0.0.0.0:0" - }) - .parse() - .unwrap(), - )?; + let bind_addr: SocketAddr = (if ctx.ip.is_ipv6() { + "[::]:0" + } else { + "0.0.0.0:0" + }) + .parse() + .map_err(|e| anyhow::anyhow!("failed to parse QUIC bind addr: {}", e))?; + let mut endpoint = Endpoint::client(bind_addr)?; endpoint.set_default_client_config(client_cfg); // Connect to the server (SNI is hostname or "localhost") @@ -155,7 +154,7 @@ impl QuicProbe { .header("Host", server_name) .header("User-Agent", "nrev/0.1 (probe)") .body(()) - .unwrap(); + .map_err(|e| anyhow::anyhow!("failed to build HTTP/3 request: {}", e))?; tracing::debug!( "HTTP/3 Probe: {}:{} - Sending request", @@ -200,7 +199,7 @@ impl QuicProbe { svc.raw = Some(format!("alpn=h3; status={}", res.status())); - Ok::(svc) + Ok::(svc) }; let (req_res, _drive_res) = tokio::join!(request, drive); From d04327d37ef79cc27bd55acdf7e8f8b930d28edc Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 20:17:00 +0900 Subject: [PATCH 06/21] feat: improve domain scan readability --- src/output/domain.rs | 80 +++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/src/output/domain.rs b/src/output/domain.rs index 945c8fb..248a486 100644 --- a/src/output/domain.rs +++ b/src/output/domain.rs @@ -2,72 +2,77 @@ use crate::{ dns::{Domain, DomainScanResult}, output::tree_label, }; -use std::net::IpAddr; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use termtree::Tree; +fn split_and_sort_ips(ips: I) -> (Vec, Vec) +where + I: IntoIterator, +{ + let mut v4 = Vec::new(); + let mut v6 = Vec::new(); + for ip in ips { + match ip { + IpAddr::V4(x) => v4.push(x), + IpAddr::V6(x) => v6.push(x), + } + } + v4.sort(); + v4.dedup(); + v6.sort(); + v6.dedup(); + (v4, v6) +} + /// Print the domain scan results in a tree structure. pub fn print_domain_tree(base_domain: &Domain, res: &DomainScanResult) { - // Create the root of the tree - let mut root = Tree::new(format!( - "Subdomains of {} - found: {} (elapsed: {:?})", + let mut root = Tree::new(tree_label(format!( + "Domain scan: {} (found: {}, elapsed: {:?})", base_domain.name, res.domains.len(), res.scan_time - )); + ))); // base domain node let mut base_node = Tree::new(tree_label(&base_domain.name)); - if !base_domain.ips.is_empty() { - let mut v4 = Vec::new(); - let mut v6 = Vec::new(); - for ip in &base_domain.ips { - match ip { - IpAddr::V4(x) => v4.push(x), - IpAddr::V6(x) => v6.push(x), - } - } - if !v4.is_empty() { - let mut a = Tree::new(tree_label("A")); - for ip in v4 { - a.push(Tree::new(ip.to_string())); - } - base_node.push(a); + let (base_v4, base_v6) = split_and_sort_ips(base_domain.ips.iter().copied()); + if !base_v4.is_empty() { + let mut a = Tree::new(tree_label(format!("A ({})", base_v4.len()))); + for ip in base_v4 { + a.push(Tree::new(ip.to_string())); } - if !v6.is_empty() { - let mut aaaa = Tree::new(tree_label("AAAA")); - for ip in v6 { - aaaa.push(Tree::new(ip.to_string())); - } - base_node.push(aaaa); + base_node.push(a); + } + if !base_v6.is_empty() { + let mut aaaa = Tree::new(tree_label(format!("AAAA ({})", base_v6.len()))); + for ip in base_v6 { + aaaa.push(Tree::new(ip.to_string())); } + base_node.push(aaaa); } // Add subdomains under the base domain let mut doms = res.domains.clone(); doms.sort_by(|a, b| a.name.cmp(&b.name)); + if doms.is_empty() { + base_node.push(Tree::new(tree_label("No subdomains resolved"))); + } + for d in doms { let mut node = Tree::new(d.name); - - let mut v4 = Vec::new(); - let mut v6 = Vec::new(); - for ip in d.ips { - match ip { - IpAddr::V4(x) => v4.push(x), - IpAddr::V6(x) => v6.push(x), - } - } + let (v4, v6) = split_and_sort_ips(d.ips); if !v4.is_empty() { - let mut a = Tree::new(tree_label("A")); + let mut a = Tree::new(tree_label(format!("A ({})", v4.len()))); for ip in v4 { a.push(Tree::new(ip.to_string())); } node.push(a); } if !v6.is_empty() { - let mut aaaa = Tree::new(tree_label("AAAA")); + let mut aaaa = Tree::new(tree_label(format!("AAAA ({})", v6.len()))); for ip in v6 { aaaa.push(Tree::new(ip.to_string())); } @@ -79,6 +84,5 @@ pub fn print_domain_tree(base_domain: &Domain, res: &DomainScanResult) { root.push(base_node); - println!("Scan report(s)"); println!("{}", root); } From 5e76c0648a82eff87a5a8bf3c81ed7433d301a50 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 20:26:43 +0900 Subject: [PATCH 07/21] feat: unify report formatting --- src/output/host.rs | 15 ++++++++++++--- src/output/nei.rs | 8 +++++--- src/output/ping.rs | 21 ++++++++++++++++++--- src/output/trace.rs | 16 +++++++++++----- 4 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/output/host.rs b/src/output/host.rs index 8c91588..f421c28 100644 --- a/src/output/host.rs +++ b/src/output/host.rs @@ -3,10 +3,16 @@ use termtree::Tree; /// Print the scan report results in a tree structure. pub fn print_report_tree(result: &ScanResult) { - let mut root = Tree::new(tree_label("Scan report(s)")); + let mut root = Tree::new(tree_label(format!( + "Host scan report (hosts: {}, elapsed: {:?})", + result.endpoints.len(), + result.scan_time + ))); // Create a tree for each endpoint - for ep in &result.endpoints { + let mut endpoints = result.endpoints.clone(); + endpoints.sort_by_key(|e| e.ip); + for ep in &endpoints { // Endpoint title let title = if let Some(hn) = &ep.hostname { format!("{} ({})", ep.ip, hn) @@ -57,7 +63,10 @@ pub fn print_report_tree(result: &ScanResult) { port.number, port.transport.as_str().to_uppercase() ))); - pnode.push(Tree::new(tree_label(format!("state: {:?}", pr.state)))); + pnode.push(Tree::new(tree_label(format!( + "state: {}", + pr.state.as_str() + )))); if let Some(name) = &pr.service.name { pnode.push(Tree::new(tree_label(format!("service: {}", name)))); } diff --git a/src/output/nei.rs b/src/output/nei.rs index a54bf1f..8931c89 100644 --- a/src/output/nei.rs +++ b/src/output/nei.rs @@ -8,9 +8,11 @@ pub fn print_neighbor_tree(entries: &[NeighborDiscoveryResult]) { return; } - let mut root = Tree::new("Neighbors".to_string()); + let mut root = Tree::new(format!("Neighbors (found: {})", entries.len())); - for e in entries { + let mut nodes = entries.to_vec(); + nodes.sort_by_key(|e| e.ip_addr); + for e in &nodes { let title = match &e.hostname { Some(h) => format!("{} ({})", e.ip_addr, h), None => format!("{}", e.ip_addr), @@ -29,7 +31,7 @@ pub fn print_neighbor_tree(entries: &[NeighborDiscoveryResult]) { ))); node.push(Tree::new(format!( - "Protoco: {}", + "Protocol: {}", e.protocol.as_str().to_uppercase() ))); diff --git a/src/output/ping.rs b/src/output/ping.rs index 9da8efc..a67af57 100644 --- a/src/output/ping.rs +++ b/src/output/ping.rs @@ -15,6 +15,19 @@ fn pct(loss: f64) -> String { format!("{:.1}%", loss) } +/// Format a Duration as HH:MM:SS.mmm +fn fmt_dur(d: Duration) -> String { + let s = d.as_secs(); + let ms = d.subsec_millis(); + format!( + "{:02}:{:02}:{:02}.{:03}", + s / 3600, + (s % 3600) / 60, + s % 60, + ms + ) +} + /// Print the ping scan results in a tree structure. pub fn print_ping_tree(res: &PingResult) { let s = &res.stat; @@ -49,7 +62,7 @@ pub fn print_ping_tree(res: &PingResult) { } summary.push(Tree::new(format!( "Protocol: {}", - format!("{:?}", res.protocol).to_uppercase() + res.protocol.as_str().to_uppercase() ))); match res.protocol { Protocol::Icmp => {} @@ -66,7 +79,7 @@ pub fn print_ping_tree(res: &PingResult) { s.received_count, s.transmitted_count ))); summary.push(Tree::new(format!("Packet loss: {}", pct(loss)))); - summary.push(Tree::new(format!("Elapsed: {:?}", res.elapsed_time))); + summary.push(Tree::new(format!("Elapsed: {}", fmt_dur(res.elapsed_time)))); if let Some(min) = &s.min { let mut rtt = Tree::new("RTT".to_string()); rtt.push(Tree::new(format!("MIN: {}", fmt_ms(min)))); @@ -82,8 +95,10 @@ pub fn print_ping_tree(res: &PingResult) { // replies if !s.responses.is_empty() { + let mut responses = s.responses.clone(); + responses.sort_by_key(|r| r.seq); let mut replies = Tree::new("Replies".to_string()); - for r in &s.responses { + for r in &responses { match r.probe_status.kind { ProbeStatusKind::Done => { let head = format!( diff --git a/src/output/trace.rs b/src/output/trace.rs index 93f64ef..e011590 100644 --- a/src/output/trace.rs +++ b/src/output/trace.rs @@ -32,7 +32,11 @@ fn fmt_ip_host(ip: IpAddr, host: &Option) -> String { /// Print the traceroute results in a tree structure. pub fn print_trace_tree(tr: &TraceResult, target: Host) { if tr.nodes.is_empty() { - println!("(no hops)"); + println!( + "Traceroute to {} - no hops (elapsed {})", + fmt_ip_host(target.ip, &target.hostname), + fmt_dur(tr.elapsed_time) + ); return; } @@ -43,17 +47,19 @@ pub fn print_trace_tree(tr: &TraceResult, target: Host) { .any(|n| n.ip_addr == target.ip && matches!(n.probe_status.kind, ProbeStatusKind::Done)); let mut root = if reached { Tree::new(format!( - "Traceroute to {} - reached ({} hops, elapsed {})", + "Traceroute to {} - reached ({} hops, elapsed {}, proto {})", fmt_ip_host(target.ip, &target.hostname), tr.nodes.len(), - fmt_dur(tr.elapsed_time) + fmt_dur(tr.elapsed_time), + tr.protocol.as_str().to_uppercase() )) } else { Tree::new(format!( - "Traceroute to {} - not reached ({} hops, elapsed {})", + "Traceroute to {} - not reached ({} hops, elapsed {}, proto {})", fmt_ip_host(target.ip, &target.hostname), tr.nodes.len(), - fmt_dur(tr.elapsed_time) + fmt_dur(tr.elapsed_time), + tr.protocol.as_str().to_uppercase() )) }; From e7e0b42cb80277cdf56713e563665aabecd5a440 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 21:16:24 +0900 Subject: [PATCH 08/21] feat: update CLI args --- src/cli/mod.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 55233d5..168b846 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -74,7 +74,7 @@ pub enum Command { /// Scan ports on the target(s) (TCP/QUIC) Port(PortScanArgs), - /// Discover alive hosts (ICMP/UDP/TCP etc.) + /// Discover alive hosts (ICMP/UDP/TCP) Host(HostScanArgs), /// Simple ping (ICMP/UDP/TCP) @@ -195,12 +195,12 @@ pub struct PortScanArgs { pub method: PortScanMethod, /// Enable service detection (banner/TLS/etc.) - #[arg(short='s', long, default_value_t = false, action=ArgAction::SetTrue)] + #[arg(short='S', long, default_value_t = false, action=ArgAction::SetTrue)] pub service_detect: bool, /// Enable OS fingerprinting /// for open ports, send one SYN to collect OS-fingerprint features - #[arg(short='o', long, default_value_t = false, action=ArgAction::SetTrue)] + #[arg(short='O', long, default_value_t = false, action=ArgAction::SetTrue)] pub os_detect: bool, /// Enable QUIC probing on UDP ports (e.g., 443/udp) From 724953430d55c8684668893eb550729b691f95bf Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 21:44:26 +0900 Subject: [PATCH 09/21] refactor: return proper exit codes --- src/main.rs | 67 +++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/src/main.rs b/src/main.rs index 59d823e..081789b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -27,6 +27,17 @@ use crate::db::DbInitializer; #[tokio::main] async fn main() { + let exit_code = match run().await { + Ok(_) => 0, + Err(e) => { + tracing::error!("{}", e); + 1 + } + }; + std::process::exit(exit_code); +} + +async fn run() -> anyhow::Result<()> { // Parse command line arguments let cli = Cli::parse(); // Initialize logger @@ -38,65 +49,50 @@ async fn main() { match cli.command { Command::Port(args) => { DbInitializer::with_all().init().await; - let r = cmd::port::run(args, cli.no_stdout, cli.output).await; - match r { - Ok(_) => {} - Err(e) => tracing::error!("Port scan failed: {}", e), - } + cmd::port::run(args, cli.no_stdout, cli.output) + .await + .map_err(|e| anyhow::anyhow!("Port scan failed: {}", e))?; } Command::Host(args) => { let db_ini = DbInitializer::new(); db_ini.with_os_db().with_oui_db().init().await; - let r = cmd::host::run(args, cli.no_stdout, cli.output).await; - match r { - Ok(_) => {} - Err(e) => tracing::error!("Host scan failed: {}", e), - } + cmd::host::run(args, cli.no_stdout, cli.output) + .await + .map_err(|e| anyhow::anyhow!("Host scan failed: {}", e))?; } Command::Ping(args) => { let db_ini = DbInitializer::new(); db_ini.with_os_db().with_oui_db().init().await; - let r = cmd::ping::run(args, cli.no_stdout, cli.output).await; - match r { - Ok(_) => {} - Err(e) => tracing::error!("Ping failed: {}", e), - } + cmd::ping::run(args, cli.no_stdout, cli.output) + .await + .map_err(|e| anyhow::anyhow!("Ping failed: {}", e))?; } Command::Trace(args) => { let db_ini = DbInitializer::new(); db_ini.with_oui_db().init().await; - let r = cmd::trace::run(args, cli.no_stdout, cli.output).await; - match r { - Ok(_) => {} - Err(e) => tracing::error!("Trace failed: {}", e), - } + cmd::trace::run(args, cli.no_stdout, cli.output) + .await + .map_err(|e| anyhow::anyhow!("Trace failed: {}", e))?; } Command::Nei(args) => { let db_ini = DbInitializer::new(); db_ini.with_oui_db().init().await; - let r = cmd::nei::run(args, cli.no_stdout, cli.output).await; - match r { - Ok(_) => {} - Err(e) => tracing::error!("Neighbor discovery failed: {}", e), - } + cmd::nei::run(args, cli.no_stdout, cli.output) + .await + .map_err(|e| anyhow::anyhow!("Neighbor discovery failed: {}", e))?; } Command::Domain(args) => { - let r = cmd::domain::run(args, cli.no_stdout, cli.output).await; - match r { - Ok(_) => {} - Err(e) => tracing::error!("Domain scan failed: {}", e), - } + cmd::domain::run(args, cli.no_stdout, cli.output) + .await + .map_err(|e| anyhow::anyhow!("Domain scan failed: {}", e))?; } Command::Interface(args) => { - let r = cmd::interface::show(&args); - match r { - Ok(_) => {} - Err(e) => tracing::error!("Show interfaces failed: {}", e), - } + cmd::interface::show(&args) + .map_err(|e| anyhow::anyhow!("Show interfaces failed: {}", e))?; } } tracing::info!( @@ -104,4 +100,5 @@ async fn main() { env!("CARGO_PKG_VERSION"), start_time.elapsed() ); + Ok(()) } From befd59899d014342b3abdb9cdc0706fec54317d2 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 21:45:37 +0900 Subject: [PATCH 10/21] refactor: remove unnecessary clone --- src/dns/mod.rs | 7 ++++++- src/interface.rs | 37 +++++++++++++++++-------------------- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/dns/mod.rs b/src/dns/mod.rs index 1a69a1d..ef8f2e5 100644 --- a/src/dns/mod.rs +++ b/src/dns/mod.rs @@ -44,7 +44,12 @@ pub async fn lookup_domain(hostname: &str, timeout: Duration) -> Domain { pub async fn lookup_ip(hostname: &str, timeout: Duration) -> Option> { let resolver = resolver::get_resolver().ok()?; match tokio::time::timeout(timeout, async move { resolver.lookup_ip(hostname).await }).await { - Ok(Ok(ips)) => Some(ips.iter().collect()), + Ok(Ok(ips)) => { + let mut out: Vec = ips.iter().collect(); + out.sort(); + out.dedup(); + Some(out) + } _ => None, } } diff --git a/src/interface.rs b/src/interface.rs index 7f3fe67..fd2f477 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -8,12 +8,12 @@ use std::{ /// Get interface information by IP address. pub fn get_interface_by_ip(ip_addr: IpAddr) -> Option { for iface in netdev::interface::get_interfaces() { - for ip in iface.ipv4.clone() { + for ip in &iface.ipv4 { if ip.addr() == ip_addr { return Some(iface); } } - for ip in iface.ipv6.clone() { + for ip in &iface.ipv6 { if ip.addr() == ip_addr { return Some(iface); } @@ -44,15 +44,12 @@ pub fn get_interface_by_name(name: String) -> Option { /// Get the first IPv4 address of the interface. pub fn get_interface_ipv4(iface: &Interface) -> Option { - for ip in iface.ipv4.clone() { - return Some(ip.addr()); - } - return None; + iface.ipv4.first().map(|ip| ip.addr()) } /// Get the first global IPv6 address of the interface. pub fn get_interface_global_ipv6(iface: &Interface) -> Option { - for ip in iface.ipv6.clone() { + for ip in &iface.ipv6 { if nex::net::ip::is_global_ipv6(&ip.addr()) { return Some(ip.addr()); } @@ -62,7 +59,7 @@ pub fn get_interface_global_ipv6(iface: &Interface) -> Option { /// Get the first local IPv6 address of the interface. pub fn get_interface_local_ipv6(iface: &Interface) -> Option { - for ip in iface.ipv6.clone() { + for ip in &iface.ipv6 { if !nex::net::ip::is_global_ipv6(&ip.addr()) { return Some(ip.addr()); } @@ -73,10 +70,10 @@ pub fn get_interface_local_ipv6(iface: &Interface) -> Option { /// Get all IP addresses of the interface as strings. pub fn get_interface_ips(iface: &Interface) -> Vec { let mut ips: Vec = Vec::new(); - for ip in iface.ipv4.clone() { + for ip in &iface.ipv4 { ips.push(ip.addr().to_string()); } - for ip in iface.ipv6.clone() { + for ip in &iface.ipv6 { ips.push(ip.addr().to_string()); } ips @@ -86,10 +83,10 @@ pub fn get_interface_ips(iface: &Interface) -> Vec { pub fn get_local_ips(if_index: u32) -> HashSet { let mut ips: HashSet = HashSet::new(); if let Some(interface) = get_interface_by_index(if_index) { - for ip in interface.ipv4.clone() { + for ip in &interface.ipv4 { ips.insert(IpAddr::V4(ip.addr())); } - for ip in interface.ipv6.clone() { + for ip in &interface.ipv6 { ips.insert(IpAddr::V6(ip.addr())); } } else { @@ -107,10 +104,10 @@ pub fn get_default_local_ips() -> HashSet { // Default interface IP addresses match netdev::get_default_interface() { Ok(default_interface) => { - for ip in default_interface.ipv4.clone() { + for ip in &default_interface.ipv4 { ips.insert(IpAddr::V4(ip.addr())); } - for ip in default_interface.ipv6.clone() { + for ip in &default_interface.ipv6 { ips.insert(IpAddr::V6(ip.addr())); } } @@ -127,10 +124,10 @@ pub fn get_default_local_ips() -> HashSet { /// Get all local IP addresses on the specified interface. pub fn get_interface_local_ips(iface: &Interface) -> HashSet { let mut ips: HashSet = HashSet::new(); - for ip in iface.ipv4.clone() { + for ip in &iface.ipv4 { ips.insert(IpAddr::V4(ip.addr())); } - for ip in iface.ipv6.clone() { + for ip in &iface.ipv6 { ips.insert(IpAddr::V6(ip.addr())); } // localhost IP addresses @@ -143,10 +140,10 @@ pub fn get_interface_local_ips(iface: &Interface) -> HashSet { pub fn get_local_ip_map() -> HashMap { let mut ip_map: HashMap = HashMap::new(); for iface in netdev::interface::get_interfaces() { - for ip in iface.ipv4.clone() { + for ip in &iface.ipv4 { ip_map.insert(IpAddr::V4(ip.addr()), iface.name.clone()); } - for ip in iface.ipv6.clone() { + for ip in &iface.ipv6 { ip_map.insert(IpAddr::V6(ip.addr()), iface.name.clone()); } } @@ -167,7 +164,7 @@ pub fn get_usable_interfaces() -> Vec { /// Get the MAC address of the interface, or MacAddr::zero() if not available. pub fn get_interface_macaddr(iface: &Interface) -> MacAddr { match &iface.mac_addr { - Some(mac_addr) => mac_addr.clone(), + Some(mac_addr) => *mac_addr, None => MacAddr::zero(), } } @@ -175,7 +172,7 @@ pub fn get_interface_macaddr(iface: &Interface) -> MacAddr { /// Get the MAC address of the gateway, or MacAddr::zero() if not available. pub fn get_gateway_macaddr(iface: &Interface) -> MacAddr { match &iface.gateway { - Some(gateway) => gateway.mac_addr.clone(), + Some(gateway) => gateway.mac_addr, None => MacAddr::zero(), } } From 842d14f3a9afd961ee4275d74a5467ed7dee362f Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 21:55:40 +0900 Subject: [PATCH 11/21] fix: max size check --- src/service/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/service/mod.rs b/src/service/mod.rs index 704f9bc..be6e9ed 100644 --- a/src/service/mod.rs +++ b/src/service/mod.rs @@ -230,10 +230,11 @@ where Ok(Ok(0)) => break, // Data read Ok(Ok(n)) => { - if out.len() > max_bytes { + if out.len().saturating_add(n) > max_bytes { bail!( - "response exceeded max_bytes ({} > {})", + "response exceeded max_bytes ({} + {} > {})", out.len(), + n, max_bytes ); } From 83a076eaa2c433831998c0277dd2e127da7c646b Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 21:55:57 +0900 Subject: [PATCH 12/21] fix: remove panic-prone paths --- src/scan/probe/quic.rs | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/scan/probe/quic.rs b/src/scan/probe/quic.rs index a2cd98a..114086e 100644 --- a/src/scan/probe/quic.rs +++ b/src/scan/probe/quic.rs @@ -1,4 +1,4 @@ -use std::{collections::BTreeMap, time::Duration}; +use std::{collections::BTreeMap, net::SocketAddr, time::Duration}; use crate::{ cli::PortScanMethod, @@ -58,20 +58,34 @@ pub async fn try_connect_ports( .for_each_concurrent(concurrency, move |socket_addr| { let ch_tx = ch_tx.clone(); let hostname = hostname.clone(); - let client_cfg = quic_client_config(true, &alpn).unwrap(); async move { - let mut endpoint = match quinn::Endpoint::client( - (if target.ip.is_ipv6() { - "[::]:0" - } else { - "0.0.0.0:0" - }) - .parse() - .unwrap(), - ) { + let client_cfg = match quic_client_config(true, &alpn) { + Ok(cfg) => cfg, + Err(e) => { + tracing::error!("Failed to build QUIC client config: {}", e); + return; + } + }; + let bind_addr: SocketAddr = match (if target.ip.is_ipv6() { + "[::]:0" + } else { + "0.0.0.0:0" + }) + .parse() + { + Ok(addr) => addr, + Err(e) => { + tracing::error!("Failed to parse QUIC bind addr: {}", e); + return; + } + }; + let mut endpoint = match quinn::Endpoint::client(bind_addr) { Ok(ep) => ep, - Err(_) => return, + Err(e) => { + tracing::error!("Failed to create QUIC endpoint: {}", e); + return; + } }; endpoint.set_default_client_config(client_cfg.clone()); let connect_fut = match endpoint.connect(socket_addr, hostname.as_str()) { @@ -82,7 +96,7 @@ pub async fn try_connect_ports( } }; let mut port_result = PortResult { - port: Port::new(socket_addr.port(), TransportProtocol::Udp), + port: Port::new(socket_addr.port(), TransportProtocol::Quic), state: PortState::Closed, service: ServiceInfo::default(), rtt_ms: None, From 97e0053692779ecc010842169f68954d7e50adf0 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:02:33 +0900 Subject: [PATCH 13/21] fix: harden logger path resolution --- src/log.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/log.rs b/src/log.rs index ee48344..a26668b 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use std::fs::File; use tracing::level_filters::LevelFilter; use tracing_indicatif::IndicatifLayer; @@ -46,10 +46,10 @@ pub fn init_logger(cli_args: &Cli) -> Result<()> { } // Determine log file path - let log_file_path = cli_args - .log_file_path - .clone() - .unwrap_or_else(|| crate::config::get_user_file_path("nrev.log").unwrap()); + let log_file_path = cli_args.log_file_path.clone().map(Ok).unwrap_or_else(|| { + crate::config::get_user_file_path("nrev.log") + .context("failed to resolve default log file path") + })?; // Open log file in append mode let file = File::options() From 1bebfa526c8624861a5e37db35416a0e55b6172a Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:03:03 +0900 Subject: [PATCH 14/21] feat: improve target parsing pipeline --- src/cli/host.rs | 182 +++++++++++++++++++++++++++++++----------------- 1 file changed, 118 insertions(+), 64 deletions(-) diff --git a/src/cli/host.rs b/src/cli/host.rs index 8488daa..0780941 100644 --- a/src/cli/host.rs +++ b/src/cli/host.rs @@ -1,68 +1,42 @@ use crate::endpoint::Host; use anyhow::{Context, Result}; +use futures::stream::{self, StreamExt}; use ipnet::IpNet; +use std::collections::{BTreeMap, HashSet}; use std::fs; -use std::{net::IpAddr, path::Path}; +use std::net::IpAddr; +use std::path::{Path, PathBuf}; -/// Resolve one target specification line (CIDR / IP / hostname) -async fn expand_one_target(t: &str) -> Result> { - let mut out = Vec::new(); - let resolver = crate::dns::resolver::get_resolver()?; - - // CIDR - if let Ok(net) = t.parse::() { - for ip in net.hosts() { - out.push(Host::new(ip)); - } - return Ok(out); - } - - // IP - if let Ok(ip) = t.parse::() { - out.push(Host::new(ip)); - return Ok(out); - } +const TARGET_RESOLVE_CONCURRENCY: usize = 64; - // Hostname - let ips = resolver - .lookup_ip(t) - .await - .with_context(|| format!("resolve {t}"))?; - for ip in ips { - out.push(Host::with_hostname(ip, t.to_string())); - } - Ok(out) +#[derive(Debug)] +enum TargetSpec { + Network(IpNet), + Address(IpAddr), + Hostname(String), } -/// Expand targets from a file (each line: CIDR / IP / hostname / @file) -async fn expand_file(path: &Path) -> Result> { - let text = fs::read_to_string(path) - .with_context(|| format!("read target list file {}", path.display()))?; - - // Normalize lines and prepare for recursive processing - let mut hosts = Vec::new(); - let mut nested_inputs = Vec::new(); - - for line in text.lines() { - let s = line.trim(); - if s.is_empty() || s.starts_with('#') { - continue; - } // Skip empty lines/comments - nested_inputs.push(s.to_string()); - } - - // Recursively interpret each entry in the file - for entry in nested_inputs { - let nested = expand_one_target(&entry).await?; - hosts.extend(nested); +impl TargetSpec { + fn parse(raw: &str) -> Self { + if let Ok(net) = raw.parse::() { + return Self::Network(net); + } + if let Ok(ip) = raw.parse::() { + return Self::Address(ip); + } + Self::Hostname(raw.to_string()) } +} - Ok(hosts) +fn canonicalize_for_seen(path: &Path) -> PathBuf { + fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf()) } -/// Parse target specifications (CIDR / IP / hostname / @file / existing file path) -pub async fn parse_target_hosts(inputs: &[String]) -> Result> { - let mut out = Vec::new(); +fn collect_target_tokens( + inputs: &[String], + seen_files: &mut HashSet, +) -> Result> { + let mut tokens = Vec::new(); for raw in inputs { let s = raw.trim(); @@ -70,7 +44,6 @@ pub async fn parse_target_hosts(inputs: &[String]) -> Result> { continue; } - // 1. Check if it's a file (with '@' hint or existing file path) let (is_file_hint, path_str) = if let Some(stripped) = s.strip_prefix('@') { (true, stripped) } else { @@ -79,19 +52,100 @@ pub async fn parse_target_hosts(inputs: &[String]) -> Result> { let path = Path::new(path_str); if is_file_hint || path.is_file() { - // Interpret as file - let hosts = expand_file(path).await?; - out.extend(hosts); + let canonical = canonicalize_for_seen(path); + if !seen_files.insert(canonical.clone()) { + continue; + } + + let text = fs::read_to_string(path) + .with_context(|| format!("read target list file {}", path.display()))?; + + let nested_inputs: Vec = text + .lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .map(ToString::to_string) + .collect(); + + if nested_inputs.is_empty() { + continue; + } + + let mut nested = collect_target_tokens(&nested_inputs, seen_files)?; + tokens.append(&mut nested); continue; } - // 2. Interpret as regular target - let hosts = expand_one_target(s).await?; - out.extend(hosts); + tokens.push(s.to_string()); + } + + Ok(tokens) +} + +/// Parse target specifications (CIDR / IP / hostname / @file / existing file path) +pub async fn parse_target_hosts(inputs: &[String]) -> Result> { + let mut seen_files = HashSet::new(); + let tokens = collect_target_tokens(inputs, &mut seen_files)?; + + let mut net_targets = Vec::new(); + let mut ip_targets = Vec::new(); + let mut hostnames = Vec::new(); + + for token in tokens { + match TargetSpec::parse(&token) { + TargetSpec::Network(net) => net_targets.push(net), + TargetSpec::Address(ip) => ip_targets.push(ip), + TargetSpec::Hostname(name) => hostnames.push(name), + } + } + + let resolver = crate::dns::resolver::get_resolver()?; + + let dns_resolved = stream::iter(hostnames.into_iter()) + .map(|name| { + let resolver = resolver.clone(); + async move { + let lookup = resolver + .lookup_ip(name.as_str()) + .await + .with_context(|| format!("resolve {}", name))?; + + let mut hosts = Vec::new(); + for ip in lookup { + hosts.push(Host::with_hostname(ip, name.clone())); + } + Ok::, anyhow::Error>(hosts) + } + }) + .buffer_unordered(TARGET_RESOLVE_CONCURRENCY) + .collect::>() + .await; + + let mut by_ip: BTreeMap = BTreeMap::new(); + + for net in net_targets { + for ip in net.hosts() { + by_ip.entry(ip).or_insert_with(|| Host::new(ip)); + } + } + + for ip in ip_targets { + by_ip.entry(ip).or_insert_with(|| Host::new(ip)); + } + + for resolved in dns_resolved { + let hosts = resolved?; + for host in hosts { + by_ip + .entry(host.ip) + .and_modify(|existing| { + if existing.hostname.is_none() { + existing.hostname = host.hostname.clone(); + } + }) + .or_insert(host); + } } - // Sort by IP & remove duplicates - out.sort_by_key(|e| e.ip); - out.dedup_by_key(|e| e.ip); - Ok(out) + Ok(by_ip.into_values().collect()) } From 053ce2c298035fa957f590ccb73cbea534f74918 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:12:12 +0900 Subject: [PATCH 15/21] chore: update cargo-dist --- .github/workflows/release.yml | 9 ++++----- dist-workspace.toml | 4 +--- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f48e57..3c59af5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,6 @@ -# This file was autogenerated by dist: https://github.com/astral-sh/cargo-dist +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist # # Copyright 2022-2024, axodotdev -# Copyright 2025 Astral Software Inc. # SPDX-License-Identifier: MIT or Apache-2.0 # # CI that: @@ -65,7 +64,7 @@ jobs: # we specify bash to get pipefail; it guards against the `curl` command # failing. otherwise `sh` won't catch that `curl` returned non-0 shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/astral-sh/cargo-dist/releases/download/v0.28.7/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.30.3/cargo-dist-installer.sh | sh" - name: Cache dist uses: actions/upload-artifact@v4 with: @@ -218,8 +217,8 @@ jobs: - plan - build-local-artifacts - build-global-artifacts - # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) - if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} runs-on: "ubuntu-22.04" diff --git a/dist-workspace.toml b/dist-workspace.toml index ba129f3..2fe9a50 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -4,15 +4,13 @@ members = ["cargo:."] # Config for 'dist' [dist] # The preferred dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.28.7" +cargo-dist-version = "0.30.3" # CI backends to support ci = "github" # The installers to generate for each app installers = ["shell"] -#installers = ["shell", "powershell"] # Target platforms to build apps for (Rust target-triple syntax) targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] -#targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] # Which actions to run on pull requests pr-run-mode = "plan" # Whether to install an updater program From 3a9135b84de314dd451e29b28efb85349b42230b Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:13:26 +0900 Subject: [PATCH 16/21] chore: update dist config --- dist-workspace.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dist-workspace.toml b/dist-workspace.toml index 2fe9a50..925f572 100644 --- a/dist-workspace.toml +++ b/dist-workspace.toml @@ -8,8 +8,10 @@ cargo-dist-version = "0.30.3" # CI backends to support ci = "github" # The installers to generate for each app +#installers = ["shell", "powershell"] installers = ["shell"] # Target platforms to build apps for (Rust target-triple syntax) +#targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu"] # Which actions to run on pull requests pr-run-mode = "plan" From d456181465bcee0a8d5843169f06536048f48bf9 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:39:22 +0900 Subject: [PATCH 17/21] chore: update README.md --- README.md | 60 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 723407d..702b100 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,12 @@ Cross-platform Network Mapper. Designed to be used in network scan, mapping and probes. ## Features -- Port Scan -- Host Scan -- Ping +- Port Scan (TCP/UDP/QUIC, Connect/SYN) +- Host Scan (ICMP/UDP/TCP) +- Ping (ICMP/UDP/TCP/QUIC) - Traceroute -- Neighbor Discovery -- Subdomain scan +- Neighbor Discovery (ARP/NDP) +- Subdomain Enumeration ## Supported platforms - Linux @@ -47,10 +47,22 @@ cargo binstall nrev ## Basic Usage ### Port Scan Example -To scan the default 1000 ports on a target, simply specify the target (-s for service detection, -o for OS detection) +To scan the default top 1000 ports on a target: ``` -nrev port yourcorpone.com -s -o -nrev port 192.168.1.10 -s -o +nrev port yourcorpone.com +nrev port 192.168.1.10 +``` + +Enable service detection and OS fingerprinting: +``` +nrev port yourcorpone.com -S -O +``` + +You can pass multiple targets, CIDR blocks, or `@`-prefixed target list files: +``` +nrev port 192.168.1.10 192.168.1.11 +nrev port 10.0.0.0/24 +nrev port @/path/to/targets.txt ``` ### Sub-commands and Options @@ -59,7 +71,7 @@ Usage: nrev [OPTIONS] Commands: port Scan ports on the target(s) (TCP/QUIC) - host Discover alive hosts (ICMP/UDP/TCP etc.) + host Discover alive hosts (ICMP/UDP/TCP) ping Simple ping (ICMP/UDP/TCP) trace Traceroute (UDP) nei Neighbor discovery (ARP/NDP) @@ -84,7 +96,7 @@ See `nrev -h` for more detail. ### Port scan Scan default 1000 ports and enable service and OS detection for open ports ``` -nrev port yourcorpone.com -s -o +nrev port yourcorpone.com -S -O ``` Specify the ports @@ -98,9 +110,9 @@ nrev port yourcorpone.com --ports 20-100 ``` #### Settings -By default, nrev determines the connection timeout or waiting time until packet reception (before concluding the scan task) based on the results of the initial PING. -The initial PING is executed in the order of ICMP Ping, UDP Ping, TCP Ping, and if successful, proceeds to the next scan task. -If all PING attempts fail, nrev exits before executing the scan. This step can be skipped by setting the `--noping` flag. +By default, nrev derives connect/wait timing from an initial ping phase. +If initial ping fails, nrev continues scanning with a safe default RTT. +You can skip the initial ping with `--no-ping`. For other settings, please refer to `nrev port -h` for details. ### Host scan @@ -110,7 +122,7 @@ nrev host 192.168.1.0/24 ``` ``` -nrev host /path/to/list/hostlist.txt +nrev host @/path/to/list/hostlist.txt ``` TCP Host scan @@ -147,7 +159,7 @@ nrev trace 8.8.8.8 --interval-ms 500 ### Subdomain scan ``` -nrev subdomain yourcorpone.com --wordlist /path/to/wordlist/top-1000.txt +nrev domain yourcorpone.com --wordlist /path/to/wordlist/top-1000.txt ``` ### Neighbor (ARP/NDP) @@ -160,6 +172,22 @@ nrev nei 192.168.1.1 nrev port 10.10.11.14 --interface tun0 ``` +## Output and Logging +Save command results as JSON: +``` +nrev -o result.json port yourcorpone.com -S -O +``` + +Run in non-interactive mode (no tree output to stdout): +``` +nrev --no-stdout -o hosts.json host 10.0.0.0/24 +``` + +Write logs to file: +``` +nrev --log-file --log-file-path /tmp/nrev.log port yourcorpone.com +``` + ## Privileges `nrev` uses a raw socket which require elevated privileges. Execute with administrator privileges. @@ -223,4 +251,4 @@ sudo chmod-bpf install - Place the Packet.lib file from the [Npcap SDK](https://npcap.com/#download) or WinPcap Developers pack in a directory named lib at the root of this repository. - You can use any of the locations listed in the %LIB% or $Env:LIB environment variables. - For the 64-bit toolchain, the Packet.lib is located in /Lib/x64/Packet.lib. - - For the 32-bit toolchain, the Packet.lib is located in /Lib/Packet.lib. \ No newline at end of file + - For the 32-bit toolchain, the Packet.lib is located in /Lib/Packet.lib. From cc1bb43e5acbd34707a0be98508a1e7a0501674f Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:45:56 +0900 Subject: [PATCH 18/21] feat: export only open ports in JSON output --- src/cmd/port.rs | 4 +++- src/output/port.rs | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/cmd/port.rs b/src/cmd/port.rs index 6761aea..0d713aa 100644 --- a/src/cmd/port.rs +++ b/src/cmd/port.rs @@ -214,7 +214,9 @@ pub async fn run(args: PortScanArgs, no_stdout: bool, output: Option) - print_report_tree(&rep); } if let Some(path) = &output { - match save_json_output(&rep, path, JsonStyle::Pretty) { + let mut file_rep = rep.clone(); + file_rep.retain_open_only(); + match save_json_output(&file_rep, path, JsonStyle::Pretty) { Ok(_) => { if !no_stdout { tracing::info!("JSON output saved to {}", path.display()); diff --git a/src/output/port.rs b/src/output/port.rs index 53a147e..8e86303 100644 --- a/src/output/port.rs +++ b/src/output/port.rs @@ -33,7 +33,7 @@ impl OsProbeResult { } /// Comprehensive scan report combining various scan results. -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct ScanReport { pub meta: ReportMeta, /// Keep IP as key for merging @@ -44,7 +44,7 @@ pub struct ScanReport { } /// Metadata about the scan report -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] pub struct ReportMeta { pub tool: String, // "nrev" pub version: String, // env!("CARGO_PKG_VERSION") @@ -64,7 +64,7 @@ impl Default for ReportMeta { } /// Statistics about the scan -#[derive(Serialize, Deserialize, Debug, Default)] +#[derive(Serialize, Deserialize, Debug, Default, Clone)] pub struct ReportStats { pub hosts_total: usize, pub ports_scanned: usize, @@ -164,6 +164,15 @@ impl ScanReport { self.endpoints.values().collect() } + /// Keep only open ports and drop endpoints without open ports. + pub fn retain_open_only(&mut self) { + self.endpoints.retain(|_, ep| { + ep.ports.retain(|_, pr| pr.state == PortState::Open); + !ep.ports.is_empty() + }); + self.recompute_stats(); + } + fn recompute_stats(&mut self) { self.stats.hosts_total = self.endpoints.len(); let mut ports_scanned = 0usize; From cdcb65ae339d3027354358a94413fcf26ab0da85 Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:52:56 +0900 Subject: [PATCH 19/21] chore: update description --- Cargo.toml | 2 +- README.md | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 797e297..526736d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "nrev" version = "0.5.1" edition = "2024" authors = ["shellrow "] -description = "Cross-platform Network Mapper" +description = "Cross-platform network mapper for discovery and probing." repository = "https://github.com/shellrow/nrev" homepage = "https://github.com/shellrow/nrev" documentation = "https://github.com/shellrow/nrev" diff --git a/README.md b/README.md index 702b100..2f1f5cc 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,7 @@ [crates-url]: https://crates.io/crates/nrev # nrev [![Crates.io][crates-badge]][crates-url] -Cross-platform Network Mapper. -Designed to be used in network scan, mapping and probes. +Cross-platform network mapper for discovery and probing. ## Features - Port Scan (TCP/UDP/QUIC, Connect/SYN) From 04dfc5dea265fe93acd691f0552c9c272e6a552d Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:55:12 +0900 Subject: [PATCH 20/21] chore: update description --- src/cli/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 168b846..6ff355a 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -14,7 +14,7 @@ use crate::{ /// nrev - Fast Network Mapper #[derive(Parser, Debug)] -#[command(author, version, about = "nrev - Cross-platform Network Mapper\nhttps://github.com/shellrow/nrev", long_about = None)] +#[command(author, version, about = "nrev - Cross-platform network mapper\nhttps://github.com/shellrow/nrev", long_about = None)] pub struct Cli { /// Global log level #[arg(long, default_value = "info")] From 941e9a29e2493d898a22dd708ed009a2932cab2f Mon Sep 17 00:00:00 2001 From: shellrow Date: Wed, 18 Feb 2026 22:57:52 +0900 Subject: [PATCH 21/21] chore: bump version to 0.6.0 --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc03b96..9f99fd8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1395,7 +1395,7 @@ dependencies = [ [[package]] name = "nrev" -version = "0.5.1" +version = "0.6.0" dependencies = [ "anyhow", "base64", diff --git a/Cargo.toml b/Cargo.toml index 526736d..9882e22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nrev" -version = "0.5.1" +version = "0.6.0" edition = "2024" authors = ["shellrow "] description = "Cross-platform network mapper for discovery and probing."