use super::network_bind::IpFamily;
use crate::model::{IpVersionComparison, IpVersionResult};
use anyhow::Result;
use reqwest::Url;
use std::net::{IpAddr, SocketAddr};
use std::time::{Duration, Instant};
use tokio::net::lookup_host;
const TEST_DURATION: Duration = Duration::from_secs(3);
pub async fn compare_ip_versions(
base_url: &str,
user_agent: &str,
bind_ip: Option<IpAddr>,
cert_path: Option<&std::path::Path>,
family: Option<IpFamily>,
) -> Result<IpVersionComparison> {
let url = Url::parse(base_url)?;
let hostname = url
.host_str()
.ok_or_else(|| anyhow::anyhow!("No host in URL"))?;
let port = url.port_or_known_default().unwrap_or(443);
let lookup_target = format!("{}:{}", hostname, port);
let addrs: Vec<SocketAddr> = lookup_host(&lookup_target).await?.collect();
let mut ipv4_addr: Option<IpAddr> = None;
let mut ipv6_addr: Option<IpAddr> = None;
for addr in addrs {
match addr.ip() {
ip @ IpAddr::V4(_) if ipv4_addr.is_none() => ipv4_addr = Some(ip),
ip @ IpAddr::V6(_) if ipv6_addr.is_none() => ipv6_addr = Some(ip),
_ => {}
}
if ipv4_addr.is_some() && ipv6_addr.is_some() {
break;
}
}
let test_v4 = family != Some(IpFamily::V6);
let test_v6 = family != Some(IpFamily::V4);
let ipv4_result = Some(if !test_v4 {
skipped_result(family)
} else if let Some(ip) = ipv4_addr {
test_ip_version(base_url, hostname, port, ip, user_agent, bind_ip, cert_path).await
} else {
unavailable_result("No IPv4 address resolved")
});
let ipv6_result = Some(if !test_v6 {
skipped_result(family)
} else if let Some(ip) = ipv6_addr {
test_ip_version(base_url, hostname, port, ip, user_agent, bind_ip, cert_path).await
} else {
unavailable_result("No IPv6 address resolved")
});
Ok(IpVersionComparison {
ipv4_result,
ipv6_result,
})
}
fn unavailable_result(error: &str) -> IpVersionResult {
IpVersionResult {
ip_address: "N/A".to_string(),
download_mbps: 0.0,
upload_mbps: 0.0,
latency_ms: 0.0,
available: false,
error: Some(error.to_string()),
}
}
fn skipped_result(family: Option<IpFamily>) -> IpVersionResult {
let reason = family
.map(|f| format!("Skipped ({} in effect)", f.flag()))
.unwrap_or_else(|| "Skipped".to_string());
unavailable_result(&reason)
}
async fn test_ip_version(
base_url: &str,
hostname: &str,
port: u16,
ip: IpAddr,
user_agent: &str,
bind_ip: Option<IpAddr>,
cert_path: Option<&std::path::Path>,
) -> IpVersionResult {
use super::network_bind;
let socket_addr = SocketAddr::new(ip, port);
let mut builder = reqwest::Client::builder()
.user_agent(user_agent)
.timeout(Duration::from_secs(30))
.resolve(hostname, socket_addr);
if let Some(path) = cert_path {
match super::cert::load_reqwest_certificate(path) {
Ok(cert) => builder = builder.add_root_certificate(cert),
Err(e) => {
return IpVersionResult {
ip_address: ip.to_string(),
download_mbps: 0.0,
upload_mbps: 0.0,
latency_ms: 0.0,
available: false,
error: Some(format!("Failed to load certificate: {}", e)),
};
}
}
}
let client = match network_bind::apply_local_address(builder, bind_ip).build() {
Ok(c) => c,
Err(e) => {
return IpVersionResult {
ip_address: ip.to_string(),
download_mbps: 0.0,
upload_mbps: 0.0,
latency_ms: 0.0,
available: false,
error: Some(format!("Failed to build client: {}", e)),
};
}
};
let latency_ms = match measure_latency(&client, base_url).await {
Ok(lat) => lat,
Err(e) => {
return IpVersionResult {
ip_address: ip.to_string(),
download_mbps: 0.0,
upload_mbps: 0.0,
latency_ms: 0.0,
available: false,
error: Some(format!("Latency test failed: {}", e)),
};
}
};
let download_mbps = match run_download_test(&client, base_url, TEST_DURATION).await {
Ok(mbps) => mbps,
Err(e) => {
return IpVersionResult {
ip_address: ip.to_string(),
download_mbps: 0.0,
upload_mbps: 0.0,
latency_ms,
available: false,
error: Some(format!("Download test failed: {}", e)),
};
}
};
let upload_mbps = match run_upload_test(&client, base_url, TEST_DURATION).await {
Ok(mbps) => mbps,
Err(e) => {
return IpVersionResult {
ip_address: ip.to_string(),
download_mbps,
upload_mbps: 0.0,
latency_ms,
available: true, error: Some(format!("Upload test failed: {}", e)),
};
}
};
IpVersionResult {
ip_address: ip.to_string(),
download_mbps,
upload_mbps,
latency_ms,
available: true,
error: None,
}
}
async fn measure_latency(client: &reqwest::Client, base_url: &str) -> Result<f64> {
let url = format!("{}/__down?bytes=0", base_url);
let start = Instant::now();
let _resp = client.get(&url).send().await?;
Ok(start.elapsed().as_secs_f64() * 1000.0)
}
async fn run_download_test(
client: &reqwest::Client,
base_url: &str,
duration: Duration,
) -> Result<f64> {
let url = format!("{}/__down?bytes=5000000", base_url); let start = Instant::now();
let mut total_bytes: u64 = 0;
while start.elapsed() < duration {
let resp = client.get(&url).send().await?;
let bytes = resp.bytes().await?;
total_bytes += bytes.len() as u64;
}
let elapsed_secs = start.elapsed().as_secs_f64();
let mbps = (total_bytes as f64 * 8.0) / (elapsed_secs * 1_000_000.0);
Ok(mbps)
}
async fn run_upload_test(
client: &reqwest::Client,
base_url: &str,
duration: Duration,
) -> Result<f64> {
let url = format!("{}/__up", base_url);
let upload_data = vec![0u8; 5_000_000]; let start = Instant::now();
let mut total_bytes: u64 = 0;
while start.elapsed() < duration {
let _resp = client.post(&url).body(upload_data.clone()).send().await?;
total_bytes += upload_data.len() as u64;
}
let elapsed_secs = start.elapsed().as_secs_f64();
let mbps = (total_bytes as f64 * 8.0) / (elapsed_secs * 1_000_000.0);
Ok(mbps)
}