From 91c407ed5be362582bc24ade45ff96a1b603e7b4 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Tue, 10 Feb 2026 13:24:32 -0600 Subject: [PATCH] Replace surge-ping library with command-line ping Switched from surge-ping Rust library to system ping command (iputils) to avoid requiring CAP_NET_RAW capability for ICMP ping. Changes: - Use tokio::process::Command to execute /bin/ping or /bin/ping6 - Parse response time from ping output (time=X.XX ms) - Removed surge-ping and rand dependencies from Cargo.toml - Added tokio "process" feature Benefits: - No special capabilities required (ping binary has setuid root) - Works in restricted container environments - Already available in Docker image (iputils package) - More portable across different container runtimes The system ping command is already installed in the Docker image and has setuid root permissions, allowing it to create raw sockets without requiring the container to have CAP_NET_RAW. --- Cargo.lock | 119 ---------------------------------------------------- Cargo.toml | 4 +- src/ping.rs | 119 ++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 83 insertions(+), 159 deletions(-) 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()); + } }