feat: use system ping instead of raw ICMP sockets
- Replace raw ICMP socket implementation with system ping command - Add regex-lite dependency for parsing ping output - Add tokio process feature for async command execution - Support macOS and Linux ping output formats - Add iputils to Dockerfile for setuid-root ping - Remove socket2 dependency (no longer needed) This eliminates the need for CAP_NET_RAW capability in containers.
This commit is contained in:
parent
84b7519ad0
commit
ee7ce7fe00
4 changed files with 189 additions and 326 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
|
@ -778,6 +778,12 @@ dependencies = [
|
|||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-lite"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
|
|
@ -905,6 +911,16 @@ version = "1.3.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
|
||||
dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
|
|
@ -923,16 +939,6 @@ version = "0.2.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff2a575449a5c487091e541c0cb4ccd83620167fd52363f816fe28f6f357fc00"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.5.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
version = "0.6.1"
|
||||
|
|
@ -1036,7 +1042,8 @@ dependencies = [
|
|||
"libc",
|
||||
"mio",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.1",
|
||||
"signal-hook-registry",
|
||||
"socket2",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
|
@ -1088,10 +1095,10 @@ dependencies = [
|
|||
"prost",
|
||||
"prost-build",
|
||||
"prost-types",
|
||||
"regex-lite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"snmp",
|
||||
"socket2 0.5.10",
|
||||
"tokio",
|
||||
"tokio-tungstenite",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ edition = "2021"
|
|||
|
||||
[dependencies]
|
||||
snmp = "0.2"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "net", "process"] }
|
||||
tokio-tungstenite = { version = "0.21", features = ["rustls-tls-webpki-roots"] }
|
||||
futures = "0.3"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
|
@ -13,7 +13,7 @@ serde_json = "1.0"
|
|||
clap = { version = "4.0", features = ["derive", "env"] }
|
||||
prost = "0.13"
|
||||
prost-types = "0.13"
|
||||
socket2 = "0.5"
|
||||
regex-lite = "0.1"
|
||||
|
||||
[build-dependencies]
|
||||
prost-build = "0.13"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,8 @@ FROM alpine:3.19
|
|||
|
||||
# Install runtime dependencies
|
||||
# docker-cli is needed for self-update functionality
|
||||
RUN apk add --no-cache ca-certificates su-exec docker-cli
|
||||
# iputils provides ping with setuid root (doesn't require CAP_NET_RAW)
|
||||
RUN apk add --no-cache ca-certificates su-exec docker-cli iputils
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /data
|
||||
|
|
|
|||
471
src/ping.rs
471
src/ping.rs
|
|
@ -1,18 +1,16 @@
|
|||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::net::IpAddr;
|
||||
use std::process::Stdio;
|
||||
use std::time::Duration;
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::timeout;
|
||||
|
||||
type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
|
||||
|
||||
/// ICMP message types
|
||||
const ICMP_ECHO: u8 = 8;
|
||||
const ICMP_ECHOREPLY: u8 = 0;
|
||||
|
||||
/// Pings an IP address using raw ICMP and returns the round-trip time.
|
||||
/// Pings an IP address using the system ping command and returns the round-trip time.
|
||||
///
|
||||
/// This implementation uses raw ICMP sockets instead of relying on the system
|
||||
/// `ping` command, making it suitable for containerized environments.
|
||||
/// This implementation uses the system `ping` binary which is typically setuid root,
|
||||
/// allowing ICMP operations without requiring CAP_NET_RAW or elevated privileges
|
||||
/// for the application itself.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
|
|
@ -23,206 +21,107 @@ const ICMP_ECHOREPLY: u8 = 0;
|
|||
///
|
||||
/// * `Ok(Duration)` - The round-trip time if successful
|
||||
/// * `Err(anyhow::Error)` - If the ping fails or times out
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// Raw ICMP sockets require elevated privileges (CAP_NET_RAW on Linux).
|
||||
/// The application should be run with appropriate capabilities.
|
||||
pub async fn ping(ip: IpAddr, timeout_duration: Duration) -> Result<Duration> {
|
||||
let start = std::time::Instant::now();
|
||||
let ip_str = ip.to_string();
|
||||
let timeout_secs = timeout_duration.as_secs().max(1);
|
||||
|
||||
match ip {
|
||||
IpAddr::V4(ipv4) => ping_ipv4(ipv4, timeout_duration).await?,
|
||||
IpAddr::V6(_) => {
|
||||
return Err("IPv6 ping not yet implemented".into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(start.elapsed())
|
||||
}
|
||||
|
||||
async fn ping_ipv4(ip: Ipv4Addr, timeout_duration: Duration) -> Result<()> {
|
||||
// Generate unique identifier and sequence number using timestamp
|
||||
// This provides sufficient randomness for ICMP ping identification
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
let identifier = (now.as_nanos() & 0xFFFF) as u16;
|
||||
let sequence = ((now.as_nanos() >> 16) & 0xFFFF) as u16;
|
||||
|
||||
// Build ICMP echo request packet
|
||||
let packet = build_icmp_echo_request(identifier, sequence);
|
||||
|
||||
// Create raw socket for ICMP
|
||||
// Note: This requires CAP_NET_RAW capability on Linux
|
||||
let socket = socket2::Socket::new_raw(
|
||||
socket2::Domain::IPV4,
|
||||
socket2::Type::DGRAM,
|
||||
Some(socket2::Protocol::ICMPV4),
|
||||
)?;
|
||||
|
||||
socket.set_nonblocking(true)?;
|
||||
|
||||
// Convert to tokio UdpSocket
|
||||
let std_socket: std::net::UdpSocket = socket.into();
|
||||
std_socket.set_nonblocking(true)?;
|
||||
let socket = UdpSocket::from_std(std_socket)?;
|
||||
|
||||
// Connect to the target IP (this is for sendto/recvfrom convenience)
|
||||
let addr = std::net::SocketAddr::new(IpAddr::V4(ip), 0);
|
||||
socket.connect(addr).await?;
|
||||
|
||||
// Send ICMP echo request
|
||||
socket.send(&packet).await?;
|
||||
|
||||
// Wait for ICMP echo reply with timeout
|
||||
let mut buf = [0u8; 1024];
|
||||
let n = timeout(timeout_duration, socket.recv(&mut buf))
|
||||
.await
|
||||
.map_err(|_| -> Box<dyn std::error::Error + Send + Sync> { "Ping timeout".into() })?
|
||||
.map_err(|e| -> Box<dyn std::error::Error + Send + Sync> {
|
||||
format!("Failed to receive ping reply: {}", e).into()
|
||||
})?;
|
||||
|
||||
// Parse and validate the reply
|
||||
parse_icmp_reply(&buf[..n], identifier, sequence)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_icmp_echo_request(identifier: u16, sequence: u16) -> Vec<u8> {
|
||||
// ICMP Echo Request format:
|
||||
// Type (8) | Code (0) | Checksum (16) | Identifier (16) | Sequence (16) | Data (variable)
|
||||
let type_code = ICMP_ECHO;
|
||||
let code = 0u8;
|
||||
let checksum = 0u16; // Placeholder
|
||||
let data = b"towerops_ping"; // Payload
|
||||
|
||||
// Build packet without checksum
|
||||
let mut packet = Vec::new();
|
||||
packet.push(type_code);
|
||||
packet.push(code);
|
||||
packet.extend_from_slice(&checksum.to_be_bytes());
|
||||
packet.extend_from_slice(&identifier.to_be_bytes());
|
||||
packet.extend_from_slice(&sequence.to_be_bytes());
|
||||
packet.extend_from_slice(data);
|
||||
|
||||
// Calculate and insert checksum
|
||||
let calculated_checksum = icmp_checksum(&packet);
|
||||
packet[2..4].copy_from_slice(&calculated_checksum.to_be_bytes());
|
||||
|
||||
packet
|
||||
}
|
||||
|
||||
fn parse_icmp_reply(packet: &[u8], expected_identifier: u16, expected_sequence: u16) -> Result<()> {
|
||||
// ICMP reply might be wrapped in an IP header
|
||||
// Try both raw ICMP and IP-wrapped formats
|
||||
|
||||
// Try to parse as raw ICMP first
|
||||
if let Ok(()) = try_parse_icmp(packet, expected_identifier, expected_sequence) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Check if this looks like an IP packet (version 4 in high nibble of first byte)
|
||||
if packet.len() >= 20 && (packet[0] >> 4) == 4 {
|
||||
// Extract IP header length from IHL field (low nibble of first byte)
|
||||
// IHL is in 32-bit words, so multiply by 4 to get bytes
|
||||
let ihl = (packet[0] & 0x0F) as usize * 4;
|
||||
|
||||
if ihl >= 20 && packet.len() > ihl {
|
||||
// Try to parse ICMP after skipping the IP header
|
||||
if let Ok(()) = try_parse_icmp(&packet[ihl..], expected_identifier, expected_sequence) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log diagnostic information to help debug
|
||||
// Determine the ICMP portion (might need to skip IP header)
|
||||
let icmp_packet = if packet.len() >= 20 && (packet[0] >> 4) == 4 {
|
||||
let ihl = (packet[0] & 0x0F) as usize * 4;
|
||||
if ihl >= 20 && packet.len() > ihl {
|
||||
&packet[ihl..]
|
||||
// Build ping command arguments based on OS
|
||||
let args = if cfg!(target_os = "macos") {
|
||||
// macOS: -W is timeout in ms
|
||||
vec![
|
||||
"-c".to_string(),
|
||||
"1".to_string(),
|
||||
"-W".to_string(),
|
||||
(timeout_secs * 1000).to_string(),
|
||||
ip_str.clone(),
|
||||
]
|
||||
} else {
|
||||
packet
|
||||
}
|
||||
} else {
|
||||
packet
|
||||
// Linux: -W is timeout in seconds
|
||||
vec![
|
||||
"-c".to_string(),
|
||||
"1".to_string(),
|
||||
"-W".to_string(),
|
||||
timeout_secs.to_string(),
|
||||
ip_str.clone(),
|
||||
]
|
||||
};
|
||||
|
||||
let packet_preview = if icmp_packet.len() >= 8 {
|
||||
format!(
|
||||
"type={} code={} id={} seq={} len={} (total={})",
|
||||
icmp_packet[0],
|
||||
icmp_packet.get(1).unwrap_or(&0),
|
||||
u16::from_be_bytes([
|
||||
*icmp_packet.get(4).unwrap_or(&0),
|
||||
*icmp_packet.get(5).unwrap_or(&0)
|
||||
]),
|
||||
u16::from_be_bytes([
|
||||
*icmp_packet.get(6).unwrap_or(&0),
|
||||
*icmp_packet.get(7).unwrap_or(&0)
|
||||
]),
|
||||
icmp_packet.len(),
|
||||
packet.len()
|
||||
)
|
||||
} else {
|
||||
format!("len={} (too short)", packet.len())
|
||||
};
|
||||
// Execute ping command with timeout
|
||||
let result = timeout(timeout_duration + Duration::from_secs(1), async {
|
||||
let output = Command::new("ping")
|
||||
.args(&args)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if output.status.success() {
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
parse_ping_output(&stdout)
|
||||
} else {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
Err(format!(
|
||||
"Invalid ICMP reply packet (expected seq={}): {}",
|
||||
expected_sequence, packet_preview
|
||||
"Ping failed for {}: {}{}",
|
||||
ip_str,
|
||||
stdout.trim(),
|
||||
stderr.trim()
|
||||
)
|
||||
.into())
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
fn try_parse_icmp(packet: &[u8], _expected_identifier: u16, expected_sequence: u16) -> Result<()> {
|
||||
if packet.len() < 8 {
|
||||
return Err("Packet too short".into());
|
||||
}
|
||||
|
||||
let icmp_type = packet[0];
|
||||
let icmp_code = packet[1];
|
||||
let identifier = u16::from_be_bytes([packet[4], packet[5]]);
|
||||
let sequence = u16::from_be_bytes([packet[6], packet[7]]);
|
||||
|
||||
// Note: When using SOCK_DGRAM for ICMP on Linux, the kernel manages the identifier field
|
||||
// and may overwrite what we set. We only validate type, code, and sequence number.
|
||||
// The sequence number is random and unique enough given our polling intervals (300s+).
|
||||
if icmp_type == ICMP_ECHOREPLY && icmp_code == 0 && sequence == expected_sequence {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!(
|
||||
"ICMP packet mismatch (type={}, code={}, id={}, seq={})",
|
||||
icmp_type, icmp_code, identifier, sequence
|
||||
)
|
||||
.into())
|
||||
match result {
|
||||
Ok(inner_result) => inner_result,
|
||||
Err(_) => Err(format!("Ping timeout for {}", ip_str).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn icmp_checksum(data: &[u8]) -> u16 {
|
||||
// ICMP checksum is the 16-bit one's complement of the one's complement sum
|
||||
let mut sum = 0u32;
|
||||
|
||||
// Sum all 16-bit words
|
||||
for chunk in data.chunks(2) {
|
||||
let word = if chunk.len() == 2 {
|
||||
u16::from_be_bytes([chunk[0], chunk[1]]) as u32
|
||||
} else {
|
||||
// Odd byte - pad with zero
|
||||
(chunk[0] as u32) << 8
|
||||
};
|
||||
sum += word;
|
||||
/// Parse the ping output to extract round-trip time as Duration.
|
||||
///
|
||||
/// Supports multiple output formats:
|
||||
/// - macOS: "round-trip min/avg/max/stddev = 1.234/1.234/1.234/0.000 ms"
|
||||
/// - Linux: "rtt min/avg/max/mdev = 1.234/1.234/1.234/0.000 ms"
|
||||
/// - Both: "time=X.XX ms" in the reply line
|
||||
fn parse_ping_output(output: &str) -> Result<Duration> {
|
||||
// Try macOS format: "round-trip min/avg/max/stddev = X/Y/Z/W ms"
|
||||
if let Some(caps) = regex_lite::Regex::new(r"round-trip.*=\s*[\d.]+/([\d.]+)/")
|
||||
.ok()
|
||||
.and_then(|re| re.captures(output))
|
||||
{
|
||||
if let Some(avg_ms) = caps.get(1) {
|
||||
if let Ok(ms) = avg_ms.as_str().parse::<f64>() {
|
||||
return Ok(Duration::from_secs_f64(ms / 1000.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fold 32-bit sum to 16 bits
|
||||
while sum >> 16 != 0 {
|
||||
sum = (sum & 0xFFFF) + (sum >> 16);
|
||||
// Try Linux format: "rtt min/avg/max/mdev = X/Y/Z/W ms"
|
||||
if let Some(caps) = regex_lite::Regex::new(r"rtt.*=\s*[\d.]+/([\d.]+)/")
|
||||
.ok()
|
||||
.and_then(|re| re.captures(output))
|
||||
{
|
||||
if let Some(avg_ms) = caps.get(1) {
|
||||
if let Ok(ms) = avg_ms.as_str().parse::<f64>() {
|
||||
return Ok(Duration::from_secs_f64(ms / 1000.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One's complement
|
||||
!sum as u16
|
||||
// Try extracting from "time=X.XX ms" in the reply line
|
||||
if let Some(caps) = regex_lite::Regex::new(r"time[=<]([\d.]+)\s*ms")
|
||||
.ok()
|
||||
.and_then(|re| re.captures(output))
|
||||
{
|
||||
if let Some(time_ms) = caps.get(1) {
|
||||
if let Ok(ms) = time_ms.as_str().parse::<f64>() {
|
||||
return Ok(Duration::from_secs_f64(ms / 1000.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("Could not parse ping output: {}", output.trim()).into())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -230,143 +129,99 @@ mod tests {
|
|||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_icmp_checksum() {
|
||||
// Test that checksum calculation is consistent
|
||||
// ICMP echo request packet: type=8, code=0, id=1, seq=1, data="abcd"
|
||||
let mut packet = vec![
|
||||
0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x61, 0x62, 0x63, 0x64,
|
||||
];
|
||||
fn test_parse_ping_output_macos() {
|
||||
let output = r#"PING 192.168.1.1 (192.168.1.1): 56 data bytes
|
||||
64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=1.234 ms
|
||||
|
||||
// Calculate checksum
|
||||
let checksum = icmp_checksum(&packet);
|
||||
assert_ne!(checksum, 0, "Checksum should not be zero");
|
||||
--- 192.168.1.1 ping statistics ---
|
||||
1 packets transmitted, 1 packets received, 0.0% packet loss
|
||||
round-trip min/avg/max/stddev = 1.234/1.234/1.234/0.000 ms"#;
|
||||
|
||||
// Insert checksum into packet
|
||||
packet[2..4].copy_from_slice(&checksum.to_be_bytes());
|
||||
|
||||
// Verify: recalculating checksum with checksum field included should give 0
|
||||
// (because the sum of all words including the checksum should wrap to 0xFFFF,
|
||||
// and one's complement of 0xFFFF is 0)
|
||||
let verification = icmp_checksum(&packet);
|
||||
assert_eq!(verification, 0, "Checksum verification should be 0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_icmp_echo_request() {
|
||||
let identifier = 0x0001;
|
||||
let sequence = 0x0001;
|
||||
let packet = build_icmp_echo_request(identifier, sequence);
|
||||
|
||||
// Verify packet structure
|
||||
assert_eq!(packet[0], ICMP_ECHO); // Type
|
||||
assert_eq!(packet[1], 0); // Code
|
||||
|
||||
// Verify identifier and sequence
|
||||
let id = u16::from_be_bytes([packet[4], packet[5]]);
|
||||
let seq = u16::from_be_bytes([packet[6], packet[7]]);
|
||||
assert_eq!(id, identifier);
|
||||
assert_eq!(seq, sequence);
|
||||
|
||||
// Verify checksum is non-zero
|
||||
let checksum = u16::from_be_bytes([packet[2], packet[3]]);
|
||||
assert_ne!(checksum, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_parse_icmp_success() {
|
||||
// Valid ICMP echo reply packet
|
||||
let packet = vec![
|
||||
0x00, 0x00, 0x00, 0x00, // type=0 (reply), code=0, checksum
|
||||
0x12, 0x34, // identifier
|
||||
0x56, 0x78, // sequence
|
||||
0x61, 0x62, 0x63, 0x64, // data
|
||||
];
|
||||
|
||||
let result = try_parse_icmp(&packet, 0x1234, 0x5678);
|
||||
let result = parse_ping_output(output);
|
||||
assert!(result.is_ok());
|
||||
let duration = result.unwrap();
|
||||
// Should be approximately 1.234ms
|
||||
assert!(duration.as_secs_f64() > 0.001);
|
||||
assert!(duration.as_secs_f64() < 0.002);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_parse_icmp_too_short() {
|
||||
let packet = vec![0x00, 0x00, 0x00];
|
||||
let result = try_parse_icmp(&packet, 0x1234, 0x5678);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("too short"));
|
||||
}
|
||||
fn test_parse_ping_output_linux() {
|
||||
let output = r#"PING 192.168.1.1 (192.168.1.1) 56(84) bytes of data.
|
||||
64 bytes from 192.168.1.1: icmp_seq=1 ttl=64 time=0.543 ms
|
||||
|
||||
#[test]
|
||||
fn test_try_parse_icmp_wrong_type() {
|
||||
let packet = vec![
|
||||
0x08, 0x00, 0x00, 0x00, // type=8 (request, not reply), code=0
|
||||
0x12, 0x34, // identifier
|
||||
0x56, 0x78, // sequence
|
||||
];
|
||||
--- 192.168.1.1 ping statistics ---
|
||||
1 packets transmitted, 1 received, 0% packet loss, time 0ms
|
||||
rtt min/avg/max/mdev = 0.543/0.543/0.543/0.000 ms"#;
|
||||
|
||||
let result = try_parse_icmp(&packet, 0x1234, 0x5678);
|
||||
assert!(result.is_err());
|
||||
assert!(result.unwrap_err().to_string().contains("mismatch"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_parse_icmp_wrong_sequence() {
|
||||
let packet = vec![
|
||||
0x00, 0x00, 0x00, 0x00, // type=0, code=0
|
||||
0x12, 0x34, // identifier (not validated)
|
||||
0x99, 0x99, // wrong sequence
|
||||
];
|
||||
|
||||
let result = try_parse_icmp(&packet, 0x1234, 0x5678);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_icmp_reply_raw() {
|
||||
// Raw ICMP packet (no IP header)
|
||||
let packet = vec![
|
||||
0x00, 0x00, 0x00, 0x00, // type=0, code=0, checksum
|
||||
0x12, 0x34, // identifier
|
||||
0x56, 0x78, // sequence
|
||||
];
|
||||
|
||||
let result = parse_icmp_reply(&packet, 0x1234, 0x5678);
|
||||
let result = parse_ping_output(output);
|
||||
assert!(result.is_ok());
|
||||
let duration = result.unwrap();
|
||||
// Should be approximately 0.543ms
|
||||
assert!(duration.as_secs_f64() > 0.0005);
|
||||
assert!(duration.as_secs_f64() < 0.001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_icmp_reply_with_ip_header() {
|
||||
// IPv4 packet with ICMP payload
|
||||
let packet = vec![
|
||||
0x45, 0x00, 0x00, 0x54, // IPv4 header: version=4, IHL=5 (20 bytes)
|
||||
0x00, 0x00, 0x00, 0x00, 0x40, 0x01, 0x00, 0x00, // TTL, protocol=ICMP
|
||||
0xc0, 0xa8, 0x01, 0x01, // Source IP
|
||||
0xc0, 0xa8, 0x01, 0x02, // Dest IP
|
||||
// ICMP payload starts here (at byte 20)
|
||||
0x00, 0x00, 0x00, 0x00, // type=0, code=0, checksum
|
||||
0x12, 0x34, // identifier
|
||||
0x56, 0x78, // sequence
|
||||
];
|
||||
fn test_parse_ping_output_time_only() {
|
||||
// Some systems may not include the summary line
|
||||
let output = "64 bytes from 192.168.1.1: icmp_seq=0 ttl=64 time=2.567 ms";
|
||||
|
||||
let result = parse_icmp_reply(&packet, 0x1234, 0x5678);
|
||||
let result = parse_ping_output(output);
|
||||
assert!(result.is_ok());
|
||||
let duration = result.unwrap();
|
||||
// Should be approximately 2.567ms
|
||||
assert!(duration.as_secs_f64() > 0.002);
|
||||
assert!(duration.as_secs_f64() < 0.003);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_icmp_reply_invalid() {
|
||||
let packet = vec![0x00, 0x00];
|
||||
let result = parse_icmp_reply(&packet, 0x1234, 0x5678);
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("Invalid ICMP reply"));
|
||||
fn test_parse_ping_output_time_less_than() {
|
||||
// Some ping implementations use time<1 ms for very fast responses
|
||||
let output = "64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time<1 ms";
|
||||
|
||||
let result = parse_ping_output(output);
|
||||
assert!(result.is_ok());
|
||||
let duration = result.unwrap();
|
||||
// Should be approximately 1ms (the < becomes the value)
|
||||
assert!(duration.as_secs_f64() < 0.002);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_icmp_reply_short_packet_error_message() {
|
||||
let packet = vec![0x01, 0x02, 0x03];
|
||||
let result = parse_icmp_reply(&packet, 0x1234, 0x5678);
|
||||
fn test_parse_ping_output_invalid() {
|
||||
let output = "some random text without ping data";
|
||||
let result = parse_ping_output(output);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_ping_output_empty() {
|
||||
let result = parse_ping_output("");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ping_localhost() {
|
||||
// This test actually pings localhost - skip if no network
|
||||
let ip: IpAddr = "127.0.0.1".parse().unwrap();
|
||||
let result = ping(ip, Duration::from_secs(5)).await;
|
||||
|
||||
// Localhost ping should succeed on most systems
|
||||
if result.is_ok() {
|
||||
let duration = result.unwrap();
|
||||
// Localhost should respond in < 100ms
|
||||
assert!(duration.as_millis() < 100);
|
||||
}
|
||||
// If it fails, that's okay too - some systems don't allow ping to localhost
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_ping_invalid_ip() {
|
||||
// This should fail - non-routable IP
|
||||
let ip: IpAddr = "192.0.2.1".parse().unwrap(); // TEST-NET-1, not routable
|
||||
let result = ping(ip, Duration::from_secs(2)).await;
|
||||
|
||||
// Should either timeout or fail
|
||||
assert!(result.is_err());
|
||||
let err_msg = result.unwrap_err().to_string();
|
||||
assert!(err_msg.contains("too short"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue