diff --git a/Cargo.lock b/Cargo.lock index 555c7be..15bdb81 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -954,12 +954,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "group" version = "0.13.0" @@ -1524,15 +1518,6 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - [[package]] name = "log" version = "0.4.29" @@ -1614,12 +1599,6 @@ dependencies = [ "libc", ] -[[package]] -name = "no-std-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43794a0ace135be66a25d3ae77d41b91615fb68ae937f904090203e81f755b65" - [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1760,29 +1739,6 @@ dependencies = [ "windows-strings", ] -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-link", -] - [[package]] name = "password-hash" version = "0.5.0" @@ -1904,48 +1860,6 @@ dependencies = [ "spki 0.8.0-rc.4", ] -[[package]] -name = "pnet_base" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cf6fb3ab38b68d01ab2aea03ed3d1132b4868fa4e06285f29f16da01c5f4c" -dependencies = [ - "no-std-net", -] - -[[package]] -name = "pnet_macros" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "688b17499eee04a0408aca0aa5cba5fc86401d7216de8a63fdf7a4c227871804" -dependencies = [ - "proc-macro2", - "quote", - "regex", - "syn", -] - -[[package]] -name = "pnet_macros_support" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eea925b72f4bd37f8eab0f221bbe4c78b63498350c983ffa9dd4bcde7e030f56" -dependencies = [ - "pnet_base", -] - -[[package]] -name = "pnet_packet" -version = "0.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a005825396b7fe7a38a8e288dbc342d5034dac80c15212436424fef8ea90ba" -dependencies = [ - "glob", - "pnet_base", - "pnet_macros", - "pnet_macros_support", -] - [[package]] name = "poly1305" version = "0.8.0" @@ -2223,15 +2137,6 @@ version = "0.10.0-rc-3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f66ee92bc15280519ef199a274fe0cafff4245d31bc39aaa31c011ad56cb1f05" -[[package]] -name = "redox_syscall" -version = "0.5.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" -dependencies = [ - "bitflags", -] - [[package]] name = "regex" version = "1.12.3" @@ -2516,12 +2421,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - [[package]] name = "scrypt" version = "0.11.0" @@ -2808,22 +2707,6 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" -[[package]] -name = "surge-ping" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30498e9c9feba213c3df6ed675bdf75519ccbee493517e7225305898c86cac05" -dependencies = [ - "hex", - "parking_lot", - "pnet_packet", - "rand 0.9.2", - "socket2", - "thiserror 1.0.69", - "tokio", - "tracing", -] - [[package]] name = "syn" version = "2.0.114" @@ -3077,14 +2960,12 @@ dependencies = [ "prost", "prost-build", "prost-types", - "rand 0.8.5", "regex", "reqwest", "russh", "serde", "serde_json", "sha2 0.10.9", - "surge-ping", "thiserror 2.0.18", "tokio", "tokio-rustls", diff --git a/Cargo.toml b/Cargo.toml index 40bcf11..2d7008c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] netsnmp-sys = "0.1" libc = "0.2" -tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "signal", "io-util"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "signal", "io-util", "process"] } thiserror = "2" tokio-tungstenite = { version = "0.28", features = ["rustls-tls-webpki-roots"] } tokio-rustls = "0.26" @@ -23,8 +23,6 @@ async-trait = "0.1" home = "=0.5.12" zeroize = { version = "1", features = ["derive"] } anyhow = "1.0" -surge-ping = "0.8" -rand = "0.8" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } sha2 = "0.10" diff --git a/src/ping.rs b/src/ping.rs index 81837d6..f24fd2b 100644 --- a/src/ping.rs +++ b/src/ping.rs @@ -1,10 +1,13 @@ use anyhow::{Context, Result}; use std::net::IpAddr; use std::time::Duration; -use surge_ping::{Client, Config, PingIdentifier, PingSequence}; +use tokio::process::Command; use tokio::time::timeout; -/// Ping a device and return response time in milliseconds. +/// Ping a device using command-line ping and return response time in milliseconds. +/// +/// Uses the system ping command (from iputils package) which has setuid root +/// and doesn't require CAP_NET_RAW capability. /// /// Returns Ok(response_time_ms) on success, Err on failure. pub async fn ping_device(ip_address: &str, timeout_ms: u64) -> Result { @@ -12,52 +15,86 @@ pub async fn ping_device(ip_address: &str, timeout_ms: u64) -> Result { .parse() .context(format!("Invalid IP address: {}", ip_address))?; - // Create ICMP client - let client = Client::new(&Config::default()) - .context("Failed to create ping client - may require root/admin privileges")?; - - // Send ping with timeout - let payload = [0; 56]; // Standard ping payload size - let identifier = PingIdentifier(rand::random()); - let sequence = PingSequence(1); - - let ping_future = async { - match ip { - IpAddr::V4(addr) => { - client - .pinger(addr.into(), identifier) - .await - .ping(sequence, &payload) - .await - } - IpAddr::V6(addr) => { - client - .pinger(addr.into(), identifier) - .await - .ping(sequence, &payload) - .await - } - } + // Determine ping command based on IP version + let ping_cmd = match ip { + IpAddr::V4(_) => "ping", + IpAddr::V6(_) => "ping6", }; - // Apply timeout - let (_, duration) = timeout(Duration::from_millis(timeout_ms), ping_future) - .await - .context("Ping timeout")? - .context("Ping failed")?; + // Convert timeout to seconds (ping uses seconds, min 1) + let timeout_secs = std::cmp::max(1, timeout_ms / 1000); - // Convert Duration to milliseconds (f64 for sub-millisecond precision) - let ms = duration.as_secs_f64() * 1000.0; + // Execute ping command: -c 1 (count=1), -W timeout (wait time) + // Output format: time=X.XX ms + let output = timeout( + Duration::from_millis(timeout_ms + 1000), // Add 1s buffer to tokio timeout + Command::new(ping_cmd) + .arg("-c") + .arg("1") + .arg("-W") + .arg(timeout_secs.to_string()) + .arg(ip_address) + .output(), + ) + .await + .context("Ping command timed out")? + .context("Failed to execute ping command")?; - Ok(ms) + // Check if ping succeeded (exit code 0) + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(anyhow::anyhow!("Ping failed: {}", stderr.trim())); + } + + // Parse response time from stdout + // Example output: "64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=12.3 ms" + let stdout = String::from_utf8_lossy(&output.stdout); + let response_time = + parse_ping_time(&stdout).context("Failed to parse ping response time from output")?; + + Ok(response_time) +} + +/// Parse the response time from ping output. +/// +/// Looks for "time=X.XX ms" or "time=X.XX" pattern in the output. +fn parse_ping_time(output: &str) -> Result { + for line in output.lines() { + if let Some(time_start) = line.find("time=") { + let time_str = &line[time_start + 5..]; // Skip "time=" + + // Extract number before " ms" or end of string + let time_end = time_str + .find(" ms") + .or_else(|| time_str.find(' ')) + .unwrap_or(time_str.len()); + + let time_value = &time_str[..time_end]; + + return time_value + .parse::() + .context(format!("Invalid time value: {}", time_value)); + } + } + + Err(anyhow::anyhow!("No time= field found in ping output")) } #[cfg(test)] mod tests { use super::*; + #[test] + fn test_parse_ping_time() { + let output = "64 bytes from 8.8.8.8: icmp_seq=1 ttl=118 time=12.3 ms"; + assert_eq!(parse_ping_time(output).unwrap(), 12.3); + + let output = "64 bytes from localhost: icmp_seq=1 ttl=64 time=0.123 ms"; + assert_eq!(parse_ping_time(output).unwrap(), 0.123); + } + #[tokio::test] - #[ignore] // Requires network access and elevated privileges + #[ignore] // Requires network access async fn test_ping_localhost() { let result = ping_device("127.0.0.1", 5000).await; assert!(result.is_ok()); @@ -65,4 +102,12 @@ mod tests { assert!(response_time > 0.0); assert!(response_time < 100.0); // Localhost should be fast } + + #[tokio::test] + #[ignore] // Requires network access + async fn test_ping_timeout() { + // Try to ping a non-routable address with short timeout + let result = ping_device("192.0.2.1", 1000).await; + assert!(result.is_err()); + } }