mirror of
https://github.com/GyulyVGC/sniffnet.git
synced 2026-10-09 03:29:52 +00:00
Use surge-ping for latency checks
Switch latency measurement away from spawning ping and parsing platform-specific output. The new path keeps reusable IPv4 and IPv6 surge-ping clients, uses unique sequences for measurements, and keeps the loopback coverage end to end.
This commit is contained in:
parent
a58ceb74d4
commit
7eb9e43bf0
3 changed files with 133 additions and 107 deletions
65
Cargo.lock
generated
65
Cargo.lock
generated
|
|
@ -3043,6 +3043,12 @@ 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 = "no_std_io2"
|
||||
version = "0.9.3"
|
||||
|
|
@ -3844,6 +3850,48 @@ dependencies = [
|
|||
"plotters-backend",
|
||||
]
|
||||
|
||||
[[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 = "png"
|
||||
version = "0.17.16"
|
||||
|
|
@ -5040,6 +5088,7 @@ dependencies = [
|
|||
"serde_test",
|
||||
"serial_test",
|
||||
"splines",
|
||||
"surge-ping",
|
||||
"tokio",
|
||||
"toml 1.1.2+spec-1.1.0",
|
||||
"winres",
|
||||
|
|
@ -5134,6 +5183,22 @@ 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",
|
||||
"socket2",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svg_fmt"
|
||||
version = "0.4.5"
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ semver = "1.0.28"
|
|||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
serde_json = { version = "1.0.149", features = ["preserve_order"] }
|
||||
splines = "5.0.0"
|
||||
surge-ping = "0.8.4"
|
||||
tokio = { version = "1.52.1", features = ["macros", "fs"] }
|
||||
toml = "1.1.2"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,19 @@
|
|||
use std::net::IpAddr;
|
||||
use std::process::Command;
|
||||
use std::sync::atomic::{AtomicU16, Ordering};
|
||||
use std::sync::{Arc, LazyLock};
|
||||
use std::time::Duration;
|
||||
|
||||
use surge_ping::{Client, Config, ICMP, PingIdentifier, PingSequence};
|
||||
|
||||
const PING_TIMEOUT: Duration = Duration::from_secs(1);
|
||||
const PING_PAYLOAD: [u8; 8] = [0; 8];
|
||||
|
||||
static IPV4_CLIENT: LazyLock<Result<Arc<Client>, String>> =
|
||||
LazyLock::new(|| latency_client(ICMP::V4));
|
||||
static IPV6_CLIENT: LazyLock<Result<Arc<Client>, String>> =
|
||||
LazyLock::new(|| latency_client(ICMP::V6));
|
||||
static PING_SEQUENCE: AtomicU16 = AtomicU16::new(0);
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum LatencyStatus {
|
||||
Measuring,
|
||||
|
|
@ -20,139 +32,87 @@ impl LatencyStatus {
|
|||
}
|
||||
|
||||
pub async fn measure_latency(ip: IpAddr) -> (IpAddr, LatencyStatus) {
|
||||
(ip, measure_latency_inner(ip))
|
||||
(ip, measure_latency_inner(ip).await)
|
||||
}
|
||||
|
||||
fn measure_latency_inner(ip: IpAddr) -> LatencyStatus {
|
||||
let ip = ip.to_string();
|
||||
let output = ping_command(&ip).output();
|
||||
async fn measure_latency_inner(ip: IpAddr) -> LatencyStatus {
|
||||
let client = match client_for(ip) {
|
||||
Ok(client) => client,
|
||||
Err(error) => return LatencyStatus::Failed(error),
|
||||
};
|
||||
|
||||
match output {
|
||||
Ok(output) if output.status.success() => {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
parse_ping_latency(&stdout).map_or_else(
|
||||
|| LatencyStatus::Failed("Latency unavailable".to_string()),
|
||||
LatencyStatus::Measured,
|
||||
)
|
||||
}
|
||||
Ok(output) => {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
LatencyStatus::Failed(if stderr.is_empty() {
|
||||
"Latency unavailable".to_string()
|
||||
} else {
|
||||
stderr
|
||||
})
|
||||
}
|
||||
Err(_) => LatencyStatus::Failed("Unable to run ping".to_string()),
|
||||
let mut pinger = client.pinger(ip, ping_identifier()).await;
|
||||
pinger.timeout(PING_TIMEOUT);
|
||||
|
||||
match pinger.ping(next_sequence(), &PING_PAYLOAD).await {
|
||||
Ok((_packet, latency)) => LatencyStatus::Measured(latency),
|
||||
Err(error) => LatencyStatus::Failed(error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn ping_command(ip: &str) -> Command {
|
||||
let mut command = Command::new("ping");
|
||||
fn client_for(ip: IpAddr) -> Result<Arc<Client>, String> {
|
||||
let client = match ip {
|
||||
IpAddr::V4(_) => &*IPV4_CLIENT,
|
||||
IpAddr::V6(_) => &*IPV6_CLIENT,
|
||||
};
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
command.args(["-n", "1", "-w", "1000", ip]);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
command.args(["-c", "1", "-W", "1000", ip]);
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
{
|
||||
command.args(["-c", "1", "-W", "1", ip]);
|
||||
}
|
||||
|
||||
command
|
||||
client.as_ref().map(Arc::clone).map_err(ToString::to_string)
|
||||
}
|
||||
|
||||
fn parse_ping_latency(output: &str) -> Option<Duration> {
|
||||
let time_pos = output.find("time")?;
|
||||
let after_time = &output[time_pos + "time".len()..];
|
||||
let value_start = after_time.find(['=', '<'])?;
|
||||
let value = after_time[value_start + 1..].trim_start();
|
||||
let value_end = value
|
||||
.find(|c: char| !(c.is_ascii_digit() || c == '.'))
|
||||
.unwrap_or(value.len());
|
||||
let millis = value[..value_end].parse::<f64>().ok()?;
|
||||
fn latency_client(kind: ICMP) -> Result<Arc<Client>, String> {
|
||||
let config = match kind {
|
||||
ICMP::V4 => Config::default(),
|
||||
ICMP::V6 => Config::builder().kind(ICMP::V6).build(),
|
||||
};
|
||||
|
||||
Some(Duration::from_secs_f64(millis / 1000.0))
|
||||
Client::new(&config)
|
||||
.map(Arc::new)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
fn ping_identifier() -> PingIdentifier {
|
||||
PingIdentifier(std::process::id() as u16)
|
||||
}
|
||||
|
||||
fn next_sequence() -> PingSequence {
|
||||
PingSequence(PING_SEQUENCE.fetch_add(1, Ordering::Relaxed))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::process::Command;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use super::{LatencyStatus, measure_latency_inner, parse_ping_latency, ping_command};
|
||||
use super::{LatencyStatus, measure_latency_inner, next_sequence, ping_identifier};
|
||||
|
||||
fn command_args(command: &Command) -> Vec<String> {
|
||||
command
|
||||
.get_args()
|
||||
.map(|arg| arg.to_string_lossy().into_owned())
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn expected_ping_args(ip: &str) -> Vec<String> {
|
||||
["-n", "1", "-w", "1000", ip]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn expected_ping_args(ip: &str) -> Vec<String> {
|
||||
["-c", "1", "-W", "1000", ip]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(all(unix, not(target_os = "macos")))]
|
||||
fn expected_ping_args(ip: &str) -> Vec<String> {
|
||||
["-c", "1", "-W", "1", ip]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect()
|
||||
#[test]
|
||||
fn uses_process_id_as_ping_identifier() {
|
||||
assert_eq!(ping_identifier().0, std::process::id() as u16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_linux_ping_latency() {
|
||||
let output = "64 bytes from 1.1.1.1: icmp_seq=1 ttl=58 time=14.2 ms";
|
||||
fn increments_ping_sequence() {
|
||||
let first = next_sequence().0;
|
||||
let second = next_sequence().0;
|
||||
|
||||
assert_eq!(parse_ping_latency(output).unwrap().as_millis(), 14);
|
||||
assert_eq!(second, first.wrapping_add(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_windows_ping_latency() {
|
||||
let output = "Reply from 1.1.1.1: bytes=32 time=23ms TTL=55";
|
||||
#[tokio::test]
|
||||
async fn measures_ipv4_loopback_latency_end_to_end() {
|
||||
let status = measure_latency_inner(IpAddr::V4(Ipv4Addr::LOCALHOST)).await;
|
||||
|
||||
assert_eq!(parse_ping_latency(output).unwrap().as_millis(), 23);
|
||||
assert_measured_or_local_permission_error(status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sub_millisecond_latency() {
|
||||
let output = "64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time<1 ms";
|
||||
#[tokio::test]
|
||||
async fn measures_ipv6_loopback_latency_end_to_end() {
|
||||
let status = measure_latency_inner(IpAddr::V6(Ipv6Addr::LOCALHOST)).await;
|
||||
|
||||
assert_eq!(parse_ping_latency(output).unwrap().as_millis(), 1);
|
||||
assert_measured_or_local_permission_error(status);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_ping_command_for_current_platform() {
|
||||
let command = ping_command("1.1.1.1");
|
||||
|
||||
assert_eq!(command.get_program().to_string_lossy(), "ping");
|
||||
assert_eq!(command_args(&command), expected_ping_args("1.1.1.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn measures_loopback_latency_end_to_end() {
|
||||
let status = measure_latency_inner(IpAddr::V4(Ipv4Addr::LOCALHOST));
|
||||
|
||||
fn assert_measured_or_local_permission_error(status: LatencyStatus) {
|
||||
match status {
|
||||
LatencyStatus::Measured(_) => {}
|
||||
LatencyStatus::Failed(error)
|
||||
|
|
@ -160,10 +120,10 @@ mod tests {
|
|||
&& (error.contains("Operation not permitted")
|
||||
|| error.contains("Permission denied")) => {}
|
||||
LatencyStatus::Failed(error) => {
|
||||
panic!("expected loopback latency measurement, got error: {error}");
|
||||
panic!("expected loopback latency, got error: {error}");
|
||||
}
|
||||
LatencyStatus::Measuring => {
|
||||
panic!("expected completed loopback latency measurement");
|
||||
panic!("expected completed loopback latency");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue