102 lines
3.1 KiB
Rust
102 lines
3.1 KiB
Rust
|
|
use std::net::IpAddr;
|
||
|
|
use std::time::{Duration, Instant};
|
||
|
|
use surge_ping::{Client, Config, PingIdentifier, PingSequence, ICMP};
|
||
|
|
use tokio::time::timeout;
|
||
|
|
|
||
|
|
#[derive(Debug)]
|
||
|
|
pub struct PingResult {
|
||
|
|
pub alive: bool,
|
||
|
|
pub times: Vec<f64>,
|
||
|
|
pub min: f64,
|
||
|
|
pub max: f64,
|
||
|
|
pub avg: f64,
|
||
|
|
pub stddev: f64,
|
||
|
|
pub packet_loss: f64,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub async fn ping(host: &str, count: u32, timeout_ms: u64) -> Result<PingResult, String> {
|
||
|
|
let addr: IpAddr = resolve_host(host).await?;
|
||
|
|
let timeout_dur = Duration::from_millis(timeout_ms);
|
||
|
|
|
||
|
|
let config = match addr {
|
||
|
|
IpAddr::V4(_) => Config::default(),
|
||
|
|
IpAddr::V6(_) => Config::builder().kind(ICMP::V6).build(),
|
||
|
|
};
|
||
|
|
let client = Client::new(&config).map_err(|e| format!("Failed to create ping client: {e}"))?;
|
||
|
|
let mut pinger = client.pinger(addr, PingIdentifier(rand_u16())).await;
|
||
|
|
|
||
|
|
let mut times: Vec<f64> = Vec::with_capacity(count as usize);
|
||
|
|
let mut alive_count: u32 = 0;
|
||
|
|
|
||
|
|
for seq in 0..count {
|
||
|
|
let payload = vec![0u8; 56];
|
||
|
|
let start = Instant::now();
|
||
|
|
|
||
|
|
match timeout(timeout_dur, pinger.ping(PingSequence(seq as u16), &payload)).await {
|
||
|
|
Ok(Ok((_packet, rtt))) => {
|
||
|
|
let ms = rtt.as_secs_f64() * 1000.0;
|
||
|
|
times.push(ms);
|
||
|
|
alive_count += 1;
|
||
|
|
}
|
||
|
|
Ok(Err(_)) => {
|
||
|
|
times.push(f64::NAN);
|
||
|
|
}
|
||
|
|
Err(_) => {
|
||
|
|
// timeout
|
||
|
|
let _ = start; // suppress unused warning
|
||
|
|
times.push(f64::NAN);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
let valid: Vec<f64> = times.iter().copied().filter(|t| !t.is_nan()).collect();
|
||
|
|
let min = valid.iter().copied().fold(f64::INFINITY, f64::min);
|
||
|
|
let max = valid.iter().copied().fold(f64::NEG_INFINITY, f64::max);
|
||
|
|
let avg = if valid.is_empty() {
|
||
|
|
f64::NAN
|
||
|
|
} else {
|
||
|
|
valid.iter().sum::<f64>() / valid.len() as f64
|
||
|
|
};
|
||
|
|
let stddev = if valid.is_empty() {
|
||
|
|
f64::NAN
|
||
|
|
} else {
|
||
|
|
let variance = valid.iter().map(|v| (v - avg).powi(2)).sum::<f64>() / valid.len() as f64;
|
||
|
|
variance.sqrt()
|
||
|
|
};
|
||
|
|
let packet_loss = ((count - alive_count) as f64 / count as f64) * 100.0;
|
||
|
|
|
||
|
|
Ok(PingResult {
|
||
|
|
alive: alive_count > 0,
|
||
|
|
times,
|
||
|
|
min: if min.is_infinite() { f64::NAN } else { min },
|
||
|
|
max: if max.is_infinite() { f64::NAN } else { max },
|
||
|
|
avg,
|
||
|
|
stddev,
|
||
|
|
packet_loss,
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn resolve_host(host: &str) -> Result<IpAddr, String> {
|
||
|
|
// Try parsing as IP first
|
||
|
|
if let Ok(addr) = host.parse::<IpAddr>() {
|
||
|
|
return Ok(addr);
|
||
|
|
}
|
||
|
|
// DNS resolution
|
||
|
|
let addrs = tokio::net::lookup_host(format!("{host}:0"))
|
||
|
|
.await
|
||
|
|
.map_err(|e| format!("DNS resolution failed for {host}: {e}"))?;
|
||
|
|
|
||
|
|
for addr in addrs {
|
||
|
|
return Ok(addr.ip());
|
||
|
|
}
|
||
|
|
Err(format!("No addresses found for {host}"))
|
||
|
|
}
|
||
|
|
|
||
|
|
fn rand_u16() -> u16 {
|
||
|
|
// Simple random using current time
|
||
|
|
let now = std::time::SystemTime::now()
|
||
|
|
.duration_since(std::time::UNIX_EPOCH)
|
||
|
|
.unwrap_or_default();
|
||
|
|
(now.subsec_nanos() % 65536) as u16
|
||
|
|
}
|