Fix MTU discovery.

This commit is contained in:
2025-08-11 12:09:57 +02:00
parent 592095162e
commit e6666870b9
3 changed files with 188 additions and 23 deletions
+17 -2
View File
@@ -122,22 +122,37 @@ pub enum MtuCommands {
target: String, target: String,
#[arg(short, long, value_enum, default_value = "both")] #[arg(short, long, value_enum, default_value = "both")]
ip_version: IpVersionArg, 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")] #[command(about = "Test common MTU sizes")]
Common { Common {
target: String, target: String,
#[arg(short, long, value_enum, default_value = "both")] #[arg(short, long, value_enum, default_value = "both")]
ip_version: IpVersionArg, 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")] #[command(about = "Test custom MTU range")]
Range { Range {
target: String, target: String,
#[arg(short, long, default_value = "68")] #[arg(long, default_value = "68")]
min: u16, min: u16,
#[arg(short, long, default_value = "1500")] #[arg(long, default_value = "1500")]
max: u16, max: u16,
#[arg(short, long, value_enum, default_value = "both")] #[arg(short, long, value_enum, default_value = "both")]
ip_version: IpVersionArg, ip_version: IpVersionArg,
#[arg(
long,
help = "Use sudo for more accurate MTU testing (requires interactive password prompt)"
)]
sudo: bool,
}, },
} }
+18 -8
View File
@@ -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<TestResult> { async fn handle_mtu_command(command: cli::MtuCommands, _timeout: Duration) -> Vec<TestResult> {
match command { match command {
cli::MtuCommands::Discover { target, ip_version } => { cli::MtuCommands::Discover {
target,
ip_version,
sudo,
} => {
let mut results = Vec::new(); let mut results = Vec::new();
for version in ip_version.to_versions() { for version in ip_version.to_versions() {
let result = mtu::full_mtu_discovery(&target, version).await; let discovery = mtu::MtuDiscovery::new(target.clone(), version).with_sudo(sudo);
results.push(result); results.push(discovery.discover().await);
} }
results results
} }
cli::MtuCommands::Common { target, ip_version } => { cli::MtuCommands::Common {
target,
ip_version,
sudo,
} => {
let mut results = Vec::new(); let mut results = Vec::new();
for version in ip_version.to_versions() { 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.extend(common_results);
} }
results results
@@ -210,11 +218,13 @@ async fn handle_mtu_command(command: cli::MtuCommands, _timeout: Duration) -> Ve
min, min,
max, max,
ip_version, ip_version,
sudo,
} => { } => {
let mut results = Vec::new(); let mut results = Vec::new();
for version in ip_version.to_versions() { for version in ip_version.to_versions() {
let discovery = let discovery = mtu::MtuDiscovery::new(target.clone(), version)
mtu::MtuDiscovery::new(target.clone(), version).with_range(min, max); .with_range(min, max)
.with_sudo(sudo);
results.push(discovery.discover().await); results.push(discovery.discover().await);
} }
results results
@@ -272,7 +282,7 @@ async fn handle_full_test(
pb.inc(1); pb.inc(1);
// Common MTU sizes // 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); all_results.extend(mtu_common);
pb.inc(1); pb.inc(1);
} }
+153 -13
View File
@@ -1,5 +1,6 @@
use crate::network::IpVersion; use crate::network::IpVersion;
use crate::utils::{measure_time, NetworkError, Result, TestResult}; use crate::utils::{measure_time, NetworkError, Result, TestResult};
use std::net::{IpAddr, ToSocketAddrs};
use std::time::Duration; use std::time::Duration;
pub struct MtuDiscovery { pub struct MtuDiscovery {
@@ -8,6 +9,7 @@ pub struct MtuDiscovery {
pub timeout: Duration, pub timeout: Duration,
pub max_mtu: u16, pub max_mtu: u16,
pub min_mtu: u16, pub min_mtu: u16,
pub use_sudo: bool,
} }
impl Default for MtuDiscovery { impl Default for MtuDiscovery {
@@ -18,6 +20,7 @@ impl Default for MtuDiscovery {
timeout: Duration::from_secs(5), timeout: Duration::from_secs(5),
max_mtu: 1500, max_mtu: 1500,
min_mtu: 68, min_mtu: 68,
use_sudo: false,
} }
} }
} }
@@ -37,20 +40,42 @@ impl MtuDiscovery {
self self
} }
pub fn with_sudo(mut self, use_sudo: bool) -> Self {
self.use_sudo = use_sudo;
self
}
pub async fn discover(&self) -> TestResult { pub async fn discover(&self) -> TestResult {
let test_name = format!("MTU discovery for {} ({:?})", self.target, self.ip_version); let test_name = format!("MTU discovery for {} ({:?})", self.target, self.ip_version);
let (duration, result) = measure_time(|| async { self.binary_search_mtu().await }).await; let (duration, result) = measure_time(|| async { self.binary_search_mtu().await }).await;
match result { match result {
Ok(mtu) => TestResult::new(test_name) Ok(mtu) => {
.success(duration, format!("Discovered MTU: {} bytes", 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), Err(error) => TestResult::new(test_name).failure(duration, error),
} }
} }
async fn binary_search_mtu(&self) -> Result<u16> { async fn binary_search_mtu(&self) -> Result<u16> {
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 high = self.max_mtu;
let mut best_mtu = low; let mut best_mtu = low;
@@ -71,6 +96,32 @@ impl MtuDiscovery {
Ok(best_mtu) 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::<IpAddr>().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<()> { async fn test_mtu_size(&self, mtu_size: u16) -> Result<()> {
// Check if we're in a CI environment where ping may not be available // 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() { 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 // Use system ping with packet size for MTU testing
let ping_cmd = match self.ip_version { let ping_cmd = match self.ip_version {
IpVersion::V4 => "ping", IpVersion::V4 => "ping",
@@ -101,19 +155,85 @@ impl MtuDiscovery {
} }
// Add timeout wrapper to prevent hanging // Add timeout wrapper to prevent hanging
let ping_future = tokio::process::Command::new(ping_cmd) let ping_future = if cfg!(target_os = "macos") {
.args(&[ 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", "-c",
"1", "1",
"-W", "-W",
"3000", // Reduce timeout from 5000 to 3000ms "3000", // timeout in milliseconds for Linux
"-M", "-M",
"do", // Don't fragment "do", // Don't fragment on Linux
"-s", "-s",
&payload_size.to_string(), &payload_size.to_string(),
&self.target, &target,
]) ]);
.output(); cmd.output()
};
let output = tokio::time::timeout(Duration::from_secs(5), ping_future) let output = tokio::time::timeout(Duration::from_secs(5), ping_future)
.await .await
@@ -123,17 +243,37 @@ impl MtuDiscovery {
if output.status.success() { if output.status.success() {
Ok(()) Ok(())
} else { } 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<TestResult> { pub async fn test_common_mtu_sizes(
target: &str,
ip_version: IpVersion,
use_sudo: bool,
) -> Vec<TestResult> {
let common_sizes = [68, 576, 1280, 1500, 4464, 9000]; let common_sizes = [68, 576, 1280, 1500, 4464, 9000];
let mut results = Vec::new(); let mut results = Vec::new();
for &size in &common_sizes { 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; let mut result = discovery.discover().await;
result.test_name = format!("MTU test {} bytes for {} ({:?})", size, target, ip_version); result.test_name = format!("MTU test {} bytes for {} ({:?})", size, target, ip_version);