use crate::protocol::Target;
use crate::traceroute::TraceConfig;
use clap::Parser;
use std::net::IpAddr;
use std::str::FromStr;
use std::time::Duration;
#[derive(Parser, Debug)]
#[command(
name = "ntrace",
version = "0.1.3",
about = "Network port scanner and protocol analyzer",
long_about = "ntrace is a tool for scanning TCP/UDP ports, analyzing network protocols, and performing traceroute.",
next_line_help = true,
after_help = "EXAMPLES:
ntrace -H 192.168.1.1
ntrace -H wikipedia.org -p 1-1000
ntrace -H 10.0.0.1 -p 80,443,8080
ntrace -H 192.168.1.1 -p common --fast
ntrace -H wikipedia.org -o results.json
ntrace trace 1.1.1.1 --tcp --port 443 --max-hops 20
ntrace trace google.com --queries 5 --no-rdns"
)]
pub struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
#[arg(
short = 'H',
long,
help = "Target IP address, hostname, or web domain (e.g., 192.168.1.1, wikipedia.org)",
group = "ip_version",
help_heading = "TARGET SPECIFICATION"
)]
pub host: Option<String>,
#[arg(
long,
help = "Force IPv6 scanning",
group = "ip_version",
help_heading = "TARGET SPECIFICATION"
)]
pub ipv6: bool,
#[arg(
long,
help = "Force IPv4 scanning",
group = "ip_version",
help_heading = "TARGET SPECIFICATION"
)]
pub ipv4: bool,
#[arg(
short,
long,
default_value = "1-1000",
help = "Port range to scan (e.g., 1-1000, 80,443, or predefined groups)",
long_help = "Port range to scan. Can be individual ports (80,443), ranges (1-1000), or predefined groups:\n \
common: Most common ports\n \
well-known: Standard ports (1-1023)\n \
registered: Registered ports (1024-49151)\n \
dynamic: Dynamic ports (49152-65535)\n \
all: All ports (1-65535)",
help_heading = "PORT SPECIFICATION"
)]
pub ports: String,
#[arg(
short = 'P',
long,
default_value = "tcp",
help = "Protocol to scan (tcp, udp)",
help_heading = "SCAN TECHNIQUES"
)]
pub protocol: String,
#[arg(
short,
long,
help = "Perform service detection",
default_value_t = true,
help_heading = "SCAN TECHNIQUES"
)]
pub service_detection: bool,
#[arg(
long,
help = "Aggressive scan (more intrusive probes)",
help_heading = "SCAN TECHNIQUES"
)]
pub aggressive: bool,
#[arg(
long,
default_value_t = 2.0,
help = "Timeout for each port scan in seconds",
help_heading = "SCAN PERFORMANCE"
)]
pub timeout: f32,
#[arg(
long,
default_value_t = 100,
help = "Batch size for parallel scanning",
help_heading = "SCAN PERFORMANCE"
)]
pub batch_size: usize,
#[arg(
long,
help = "Rate limit in packets per second",
default_value_t = 1000,
help_heading = "SCAN PERFORMANCE"
)]
pub rate_limit: usize,
#[arg(
long,
help = "Fast scan with shorter timeouts (less accurate)",
help_heading = "SCAN PERFORMANCE"
)]
pub fast: bool,
#[arg(
long,
help = "Skip host discovery (ping)",
help_heading = "HOST DISCOVERY"
)]
pub skip_discovery: bool,
#[arg(
short,
long,
help = "Output file path (.json or .csv)",
help_heading = "OUTPUT OPTIONS"
)]
pub output: Option<String>,
#[arg(
short = 'v',
long,
help = "Verbose output (show closed ports)",
help_heading = "OUTPUT OPTIONS"
)]
pub verbose: bool,
#[arg(
long,
help = "Skip problematic ports that often cause hangs",
help_heading = "MISC OPTIONS"
)]
pub skip_problematic: bool,
#[arg(
long,
help = "Use SYN scanning for faster results (requires root/admin privileges)",
help_heading = "SCAN TECHNIQUES"
)]
pub syn_scan: bool,
}
#[derive(Parser, Debug)]
pub enum Command {
#[command(name = "trace")]
Trace {
target: String,
#[arg(long = "tcp", short = 'T')]
use_tcp: bool,
#[arg(long = "udp", short = 'U')]
use_udp: bool,
#[arg(long, short = 'p', default_value = "80")]
port: u16,
#[arg(long = "max-hops", short = 'm', default_value = "30")]
max_hops: u8,
#[arg(long, short = 'q', default_value = "3")]
queries: u8,
#[arg(long = "no-rdns", short = 'n')]
no_rdns: bool,
#[arg(long = "always-rdns", short = 'a')]
always_rdns: bool,
#[arg(long = "parallel-requests", default_value = "18")]
parallel_requests: u8,
#[arg(long = "send-time", short = 'z', default_value = "50")]
send_time_ms: u64,
#[arg(long = "ttl-time", short = 'i', default_value = "50")]
ttl_time_ms: u64,
#[arg(long = "timeout", default_value = "1000")]
timeout_ms: u64,
#[arg(long = "psize", default_value = "52")]
payload_size: usize,
#[arg(long = "route-path", short = 'P')]
route_path: bool,
#[arg(long = "table", short = 't')]
table: bool,
#[arg(long = "json", short = 'j')]
json: bool,
#[arg(long = "output", short = 'o')]
output: Option<String>,
#[arg(long = "fast-mode", short = 'f')]
fast_mode: bool,
},
}
impl Cli {
pub fn to_config(&self) -> Result<crate::scanner::ScanConfig, anyhow::Error> {
if self.command.is_some() {
return Err(anyhow::anyhow!("Using a subcommand"));
}
let host = self
.host
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Host is required"))?;
let target = match IpAddr::from_str(host) {
Ok(ip) => {
if self.ipv4 && ip.is_ipv6() {
return Err(anyhow::anyhow!(
"IPv6 address provided but --ipv4 flag was set"
));
} else if self.ipv6 && ip.is_ipv4() {
return Err(anyhow::anyhow!(
"IPv4 address provided but --ipv6 flag was set"
));
}
Target::Ip(ip)
}
Err(_) => {
if host.contains('.') && !host.starts_with('.') && !host.ends_with('.') {
Target::Domain(host.clone())
} else {
return Err(anyhow::anyhow!("Invalid host: {}", host));
}
}
};
let ports = self.parse_ports()?;
let protocol = match self.protocol.to_lowercase().as_str() {
"tcp" => crate::Protocol::Tcp,
"udp" => crate::Protocol::Udp,
_ => return Err(anyhow::anyhow!("Unsupported protocol: {}", self.protocol)),
};
Ok(crate::scanner::ScanConfig {
target,
ports,
timeout: if self.fast {
Duration::from_millis(100)
} else {
Duration::from_secs_f32(self.timeout)
},
protocol,
batch_size: self.batch_size,
max_retries: if self.fast { 1 } else { 3 },
retry_delay: if self.fast {
Duration::from_millis(100)
} else {
Duration::from_millis(500)
},
fail_fast: self.fast || self.skip_problematic,
})
}
pub fn to_trace_config(&self) -> Result<TraceConfig, anyhow::Error> {
if let Some(Command::Trace {
target,
use_tcp,
use_udp,
port,
max_hops,
queries,
no_rdns,
always_rdns,
parallel_requests,
send_time_ms,
ttl_time_ms,
timeout_ms,
payload_size,
fast_mode,
..
}) = &self.command
{
let target_obj = match IpAddr::from_str(target) {
Ok(ip) => {
if self.ipv4 && ip.is_ipv6() {
return Err(anyhow::anyhow!(
"IPv6 address provided but --ipv4 flag was set"
));
} else if self.ipv6 && ip.is_ipv4() {
return Err(anyhow::anyhow!(
"IPv4 address provided but --ipv6 flag was set"
));
}
Target::Ip(ip)
}
Err(_) => {
if target.contains('.') && !target.starts_with('.') && !target.ends_with('.') {
Target::Domain(target.clone())
} else {
return Err(anyhow::anyhow!("Invalid target: {}", target));
}
}
};
let protocol = if *use_tcp {
crate::Protocol::Tcp
} else if *use_udp {
crate::Protocol::Udp
} else {
crate::Protocol::Icmp
};
let resolve_hostnames = !no_rdns || *always_rdns;
Ok(TraceConfig {
target: target_obj,
protocol,
port: *port,
max_hops: *max_hops,
min_ttl: 1,
queries: *queries,
timeout_ms: *timeout_ms,
resolve_hostnames,
parallel_requests: *parallel_requests,
send_time_ms: *send_time_ms,
ttl_time_ms: *ttl_time_ms,
payload_size: *payload_size,
fast_mode: *fast_mode,
discover_mtu: false,
detect_asymmetry: false,
lookup_asn: false,
lookup_geo: false,
detect_mpls: false,
source_ip: None,
source_port: None,
tos: None,
interface: None,
first_hop_timeout_ms: Some(1200),
adaptive_timing: true,
})
} else {
Err(anyhow::anyhow!("Not a trace command"))
}
}
pub fn parse_ports(&self) -> Result<Vec<u16>, anyhow::Error> {
let mut ports = Vec::new();
match self.ports.to_lowercase().as_str() {
"common" => {
ports.extend_from_slice(&[
21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, 1723,
3306, 3389, 5900, 8080,
]);
}
"well-known" => {
ports.extend(1..=1023);
}
"registered" => {
ports.extend(1024..=49151);
}
"dynamic" => {
ports.extend(49152..=65535);
}
"all" => {
ports.extend(1..=65535);
}
_ => {
for part in self.ports.split(',') {
if part.contains('-') {
let range: Vec<&str> = part.split('-').collect();
if range.len() == 2 {
let start = range[0].parse::<u16>()?;
let end = range[1].parse::<u16>()?;
ports.extend(start..=end);
} else {
return Err(anyhow::anyhow!("Invalid port range: {}", part));
}
} else {
ports.push(part.parse::<u16>()?);
}
}
}
}
ports.sort();
ports.dedup();
Ok(ports)
}
}