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.
This commit is contained in:
parent
4da510dff1
commit
91c407ed5b
3 changed files with 83 additions and 159 deletions
119
Cargo.lock
generated
119
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
117
src/ping.rs
117
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<f64> {
|
||||
|
|
@ -12,52 +15,86 @@ pub async fn ping_device(ip_address: &str, timeout_ms: u64) -> Result<f64> {
|
|||
.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)
|
||||
// Convert timeout to seconds (ping uses seconds, min 1)
|
||||
let timeout_secs = std::cmp::max(1, timeout_ms / 1000);
|
||||
|
||||
// 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 timeout")?
|
||||
.context("Ping failed")?;
|
||||
.context("Ping command timed out")?
|
||||
.context("Failed to execute ping command")?;
|
||||
|
||||
// Convert Duration to milliseconds (f64 for sub-millisecond precision)
|
||||
let ms = duration.as_secs_f64() * 1000.0;
|
||||
// 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()));
|
||||
}
|
||||
|
||||
Ok(ms)
|
||||
// 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<f64> {
|
||||
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::<f64>()
|
||||
.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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue