use anti_common::{icmp, ports, PingConfig, PingError, PingReply, PingResult};
use socket2::{Domain, Protocol, Socket, Type};
use std::io::Read;
use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket};
use std::time::{Duration, Instant};
pub struct UdpPinger {
config: PingConfig,
udp_socket: UdpSocket,
icmp_socket: Socket,
base_port: u16,
}
impl UdpPinger {
pub fn new(config: PingConfig) -> PingResult<Self> {
let udp_socket =
UdpSocket::bind("0.0.0.0:0").map_err(|e| PingError::SocketCreation(e.to_string()))?;
let icmp_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::ICMPV4))
.map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
PingError::PermissionDenied {
context: "UDP ping requires ICMP socket access. Try running with elevated privileges or use TCP ping instead.".to_string(),
}
} else {
PingError::SocketCreation(e.to_string())
}
})?;
icmp_socket
.set_read_timeout(Some(config.timeout))
.map_err(|e| PingError::SocketCreation(e.to_string()))?;
let base_port = ports::TRACEROUTE_BASE;
Ok(Self {
config,
udp_socket,
icmp_socket,
base_port,
})
}
pub fn new_with_port(config: PingConfig, port: u16) -> PingResult<Self> {
let mut pinger = Self::new(config)?;
pinger.base_port = port;
Ok(pinger)
}
pub fn ping(&self, sequence: u16) -> PingResult<PingReply> {
let target_port = self.base_port.wrapping_add(sequence);
let target_addr = SocketAddr::new(IpAddr::V4(self.config.target), target_port);
let start_time = Instant::now();
let test_data = format!(
"UDP_PING_SEQ_{:04}_{}",
sequence,
start_time.elapsed().as_nanos()
);
self.udp_socket
.send_to(test_data.as_bytes(), target_addr)
.map_err(|e| PingError::SocketCreation(e.to_string()))?;
self.wait_for_icmp_response(sequence, target_port, start_time)
}
fn wait_for_icmp_response(
&self,
sequence: u16,
target_port: u16,
start_time: Instant,
) -> PingResult<PingReply> {
let mut buffer = [0u8; 1500]; let deadline = start_time + self.config.timeout;
loop {
let now = Instant::now();
if now >= deadline {
return Err(PingError::Timeout {
duration: self.config.timeout,
});
}
let remaining = deadline - now;
self.icmp_socket
.set_read_timeout(Some(remaining))
.map_err(|e| PingError::SocketCreation(e.to_string()))?;
match (&self.icmp_socket).read(&mut buffer) {
Ok(bytes_received) => {
let recv_time = Instant::now();
if let Some(reply) = self.parse_icmp_response(
&buffer[..bytes_received],
target_port,
sequence,
recv_time - start_time,
) {
return Ok(reply);
}
}
Err(e) => {
if e.kind() == std::io::ErrorKind::TimedOut {
return Err(PingError::Timeout {
duration: self.config.timeout,
});
} else if e.kind() == std::io::ErrorKind::WouldBlock {
continue;
} else {
return Err(PingError::SocketCreation(e.to_string()));
}
}
}
}
}
fn parse_icmp_response(
&self,
buffer: &[u8],
expected_port: u16,
sequence: u16,
rtt: Duration,
) -> Option<PingReply> {
if buffer.len() < 48 {
return None;
}
let ip_header_len = ((buffer[0] & 0x0F) * 4) as usize;
if buffer.len() < ip_header_len + 8 {
return None;
}
let source_ip = if buffer.len() >= 20 {
Ipv4Addr::new(buffer[12], buffer[13], buffer[14], buffer[15])
} else {
self.config.target
};
let icmp_data = &buffer[ip_header_len..];
if icmp_data.len() < 8 {
return None;
}
if icmp_data[0] != icmp::DEST_UNREACHABLE || icmp_data[1] != icmp::PORT_UNREACHABLE {
return None;
}
if icmp_data.len() < 28 {
return None; }
let orig_ip_data = &icmp_data[8..]; if orig_ip_data.len() < 20 {
return None;
}
if orig_ip_data[9] != 17 {
return None;
}
let orig_ip_header_len = ((orig_ip_data[0] & 0x0F) * 4) as usize;
if orig_ip_data.len() < orig_ip_header_len + 8 {
return None;
}
let orig_udp_data = &orig_ip_data[orig_ip_header_len..];
if orig_udp_data.len() < 8 {
return None;
}
let orig_dest_port = u16::from_be_bytes([orig_udp_data[2], orig_udp_data[3]]);
if orig_dest_port == expected_port {
Some(PingReply {
sequence,
rtt,
bytes_received: buffer.len(),
from: source_ip,
ttl: None, })
} else {
None
}
}
pub fn config(&self) -> &PingConfig {
&self.config
}
pub fn base_port(&self) -> u16 {
self.base_port
}
}
pub fn is_port_likely_closed(port: u16) -> bool {
match port {
33434..=33534 => true,
49152..=65535 => true,
1234 | 5678 | 9999 => true,
_ => false,
}
}
pub fn get_udp_port(sequence: u16, base_port: Option<u16>) -> u16 {
let base = base_port.unwrap_or(ports::TRACEROUTE_BASE);
base.wrapping_add(sequence % 100) }
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn test_port_selection() {
assert!(is_port_likely_closed(33434));
assert!(is_port_likely_closed(50000));
assert!(!is_port_likely_closed(80));
assert!(!is_port_likely_closed(443));
}
#[test]
fn test_get_udp_port() {
let port1 = get_udp_port(1, Some(10000));
let port2 = get_udp_port(2, Some(10000));
assert_eq!(port1, 10001);
assert_eq!(port2, 10002);
let port3 = get_udp_port(1, None);
assert_eq!(port3, ports::TRACEROUTE_BASE + 1);
}
#[test]
fn test_config_creation() {
let config = PingConfig {
target: Ipv4Addr::new(8, 8, 8, 8),
count: 1,
timeout: Duration::from_secs(2),
..Default::default()
};
assert_eq!(config.target, Ipv4Addr::new(8, 8, 8, 8));
assert_eq!(config.count, 1);
}
#[test]
fn test_parse_icmp_response_insufficient_data() {
let _config = PingConfig::default();
let buffer = vec![0u8; 10];
assert!(buffer.len() < 48);
}
}