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,
#[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,
},
}
+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> {
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);
}
+153 -13
View File
@@ -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<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 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::<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<()> {
// 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<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 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);