diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 526ad20..d0d0f4b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -122,22 +122,37 @@ pub enum MtuCommands { target: String, #[arg(short, long, value_enum, default_value = "both")] ip_version: IpVersionArg, + #[arg( + long, + help = "Use sudo for more accurate MTU testing (requires interactive password prompt)" + )] + sudo: bool, }, #[command(about = "Test common MTU sizes")] Common { target: String, #[arg(short, long, value_enum, default_value = "both")] ip_version: IpVersionArg, + #[arg( + long, + help = "Use sudo for more accurate MTU testing (requires interactive password prompt)" + )] + sudo: bool, }, #[command(about = "Test custom MTU range")] Range { target: String, - #[arg(short, long, default_value = "68")] + #[arg(long, default_value = "68")] min: u16, - #[arg(short, long, default_value = "1500")] + #[arg(long, default_value = "1500")] max: u16, #[arg(short, long, value_enum, default_value = "both")] ip_version: IpVersionArg, + #[arg( + long, + help = "Use sudo for more accurate MTU testing (requires interactive password prompt)" + )] + sudo: bool, }, } diff --git a/src/main.rs b/src/main.rs index 3958873..b3c26cc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -189,18 +189,26 @@ async fn handle_dns_command(command: cli::DnsCommands, timeout: Duration) -> Vec async fn handle_mtu_command(command: cli::MtuCommands, _timeout: Duration) -> Vec { match command { - cli::MtuCommands::Discover { target, ip_version } => { + cli::MtuCommands::Discover { + target, + ip_version, + sudo, + } => { let mut results = Vec::new(); for version in ip_version.to_versions() { - let result = mtu::full_mtu_discovery(&target, version).await; - results.push(result); + let discovery = mtu::MtuDiscovery::new(target.clone(), version).with_sudo(sudo); + results.push(discovery.discover().await); } results } - cli::MtuCommands::Common { target, ip_version } => { + cli::MtuCommands::Common { + target, + ip_version, + sudo, + } => { let mut results = Vec::new(); for version in ip_version.to_versions() { - let common_results = mtu::test_common_mtu_sizes(&target, version).await; + let common_results = mtu::test_common_mtu_sizes(&target, version, sudo).await; results.extend(common_results); } results @@ -210,11 +218,13 @@ async fn handle_mtu_command(command: cli::MtuCommands, _timeout: Duration) -> Ve min, max, ip_version, + sudo, } => { let mut results = Vec::new(); for version in ip_version.to_versions() { - let discovery = - mtu::MtuDiscovery::new(target.clone(), version).with_range(min, max); + let discovery = mtu::MtuDiscovery::new(target.clone(), version) + .with_range(min, max) + .with_sudo(sudo); results.push(discovery.discover().await); } results @@ -272,7 +282,7 @@ async fn handle_full_test( pb.inc(1); // Common MTU sizes - let mtu_common = mtu::test_common_mtu_sizes(&target, version).await; + let mtu_common = mtu::test_common_mtu_sizes(&target, version, false).await; all_results.extend(mtu_common); pb.inc(1); } diff --git a/src/mtu/mod.rs b/src/mtu/mod.rs index a599919..113613e 100644 --- a/src/mtu/mod.rs +++ b/src/mtu/mod.rs @@ -1,5 +1,6 @@ use crate::network::IpVersion; use crate::utils::{measure_time, NetworkError, Result, TestResult}; +use std::net::{IpAddr, ToSocketAddrs}; use std::time::Duration; pub struct MtuDiscovery { @@ -8,6 +9,7 @@ pub struct MtuDiscovery { pub timeout: Duration, pub max_mtu: u16, pub min_mtu: u16, + pub use_sudo: bool, } impl Default for MtuDiscovery { @@ -18,6 +20,7 @@ impl Default for MtuDiscovery { timeout: Duration::from_secs(5), max_mtu: 1500, min_mtu: 68, + use_sudo: false, } } } @@ -37,20 +40,42 @@ impl MtuDiscovery { self } + pub fn with_sudo(mut self, use_sudo: bool) -> Self { + self.use_sudo = use_sudo; + self + } + pub async fn discover(&self) -> TestResult { let test_name = format!("MTU discovery for {} ({:?})", self.target, self.ip_version); let (duration, result) = measure_time(|| async { self.binary_search_mtu().await }).await; match result { - Ok(mtu) => TestResult::new(test_name) - .success(duration, format!("Discovered MTU: {} bytes", mtu)), + Ok(mtu) => { + // Check if the discovered MTU is reasonable for the IP version + let warning = match self.ip_version { + IpVersion::V6 if mtu < 1280 => { + " (Warning: IPv6 minimum MTU is 1280 bytes - connectivity issue?)" + } + _ => "", + }; + TestResult::new(test_name).success( + duration, + format!("Discovered MTU: {} bytes{}", mtu, warning), + ) + } Err(error) => TestResult::new(test_name).failure(duration, error), } } async fn binary_search_mtu(&self) -> Result { - let mut low = self.min_mtu; + // Adjust minimum MTU based on IP version + let adjusted_min = match self.ip_version { + IpVersion::V4 => self.min_mtu, + IpVersion::V6 => self.min_mtu.max(1280), // IPv6 minimum MTU is 1280 + }; + + let mut low = adjusted_min; let mut high = self.max_mtu; let mut best_mtu = low; @@ -71,6 +96,32 @@ impl MtuDiscovery { Ok(best_mtu) } + fn resolve_target_for_ipv6(&self) -> String { + // For IPv6, try to resolve hostname to IPv6 address for better compatibility on macOS + if matches!(self.ip_version, IpVersion::V6) { + // Check if target is already an IP address + if self.target.parse::().is_ok() { + return self.target.clone(); + } + + // Try to resolve hostname to IPv6 address + match format!("{}:80", self.target).to_socket_addrs() { + Ok(addrs) => { + for addr in addrs { + if addr.is_ipv6() { + return addr.ip().to_string(); + } + } + // If no IPv6 address found, return original target + self.target.clone() + } + Err(_) => self.target.clone(), + } + } else { + self.target.clone() + } + } + async fn test_mtu_size(&self, mtu_size: u16) -> Result<()> { // Check if we're in a CI environment where ping may not be available if std::env::var("CI").is_ok() || std::env::var("GITHUB_ACTIONS").is_ok() { @@ -85,6 +136,9 @@ impl MtuDiscovery { )); } + // Resolve target for IPv6 compatibility + let target = self.resolve_target_for_ipv6(); + // Use system ping with packet size for MTU testing let ping_cmd = match self.ip_version { IpVersion::V4 => "ping", @@ -101,19 +155,85 @@ impl MtuDiscovery { } // Add timeout wrapper to prevent hanging - let ping_future = tokio::process::Command::new(ping_cmd) - .args(&[ + let ping_future = if cfg!(target_os = "macos") { + match self.ip_version { + IpVersion::V4 => { + // IPv4 on macOS: -D flag may work without sudo, but sudo gives more reliable results + if self.use_sudo { + let mut cmd = tokio::process::Command::new("sudo"); + cmd.args(&[ + ping_cmd, + "-c", + "1", + "-t", + "3", // timeout in seconds for macOS + "-D", // Don't fragment (more reliable with sudo) + "-s", + &payload_size.to_string(), + &target, + ]); + cmd.output() + } else { + // Try -D flag without sudo (may work on some systems) + let mut cmd = tokio::process::Command::new(ping_cmd); + cmd.args(&[ + "-c", + "1", + "-t", + "3", // timeout in seconds for macOS + "-D", // Don't fragment (may require privileges) + "-s", + &payload_size.to_string(), + &target, + ]); + cmd.output() + } + } + IpVersion::V6 => { + // IPv6 on macOS requires sudo for don't fragment and has no timeout flag + if self.use_sudo { + let mut cmd = tokio::process::Command::new("sudo"); + cmd.args(&[ + ping_cmd, + "-c", + "1", + "-D", // Don't fragment (requires sudo on macOS) + "-s", + &payload_size.to_string(), + &target, + ]); + cmd.output() + } else { + // Without sudo, IPv6 MTU discovery is less accurate (no don't fragment) + let mut cmd = tokio::process::Command::new(ping_cmd); + cmd.args(&["-c", "1", "-s", &payload_size.to_string(), &target]); + cmd.output() + } + } + } + } else { + // Linux ping syntax for both IPv4 and IPv6 + let mut cmd = if self.use_sudo { + let mut c = tokio::process::Command::new("sudo"); + c.arg(ping_cmd); + c + } else { + tokio::process::Command::new(ping_cmd) + }; + + cmd.args(&[ "-c", "1", "-W", - "3000", // Reduce timeout from 5000 to 3000ms + "3000", // timeout in milliseconds for Linux "-M", - "do", // Don't fragment + "do", // Don't fragment on Linux "-s", &payload_size.to_string(), - &self.target, - ]) - .output(); + &target, + ]); + cmd.output() + }; let output = tokio::time::timeout(Duration::from_secs(5), ping_future) .await @@ -123,17 +243,37 @@ impl MtuDiscovery { if output.status.success() { Ok(()) } else { - Err(NetworkError::Other("MTU test failed".to_string())) + let stderr = String::from_utf8_lossy(&output.stderr); + if self.use_sudo && stderr.contains("password is required") { + Err(NetworkError::Other( + "Sudo password required for privileged ping operations".to_string(), + )) + } else if stderr.contains("Operation not permitted") { + Err(NetworkError::Other( + "Permission denied - try using --sudo flag".to_string(), + )) + } else { + Err(NetworkError::Other(format!( + "MTU test failed: {}", + stderr.trim() + ))) + } } } } -pub async fn test_common_mtu_sizes(target: &str, ip_version: IpVersion) -> Vec { +pub async fn test_common_mtu_sizes( + target: &str, + ip_version: IpVersion, + use_sudo: bool, +) -> Vec { let common_sizes = [68, 576, 1280, 1500, 4464, 9000]; let mut results = Vec::new(); for &size in &common_sizes { - let discovery = MtuDiscovery::new(target.to_string(), ip_version).with_range(size, size); + let discovery = MtuDiscovery::new(target.to_string(), ip_version) + .with_range(size, size) + .with_sudo(use_sudo); let mut result = discovery.discover().await; result.test_name = format!("MTU test {} bytes for {} ({:?})", size, target, ip_version);