feat(smartnetwork): add Rust-powered network diagnostics bridge and IP intelligence lookups

This commit is contained in:
2026-03-26 15:24:43 +00:00
parent e9dcd45acd
commit c3ac9b4f9e
34 changed files with 5499 additions and 3159 deletions

View File

@@ -0,0 +1,101 @@
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
}