use std::net::{IpAddr, Ipv4Addr};
use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum PingError {
#[error("Failed to create socket: {0}")]
SocketCreation(String),
#[error("Permission denied: {context}")]
PermissionDenied { context: String },
#[error("Timeout after {duration:?}")]
Timeout { duration: Duration },
#[error("Invalid response: {reason}")]
InvalidResponse { reason: String },
#[error("Network unreachable")]
NetworkUnreachable,
#[error("Host unreachable")]
HostUnreachable,
#[error("Port unreachable")]
PortUnreachable,
#[error("Configuration error: {message}")]
Configuration { message: String },
#[error("Invalid target: {0}")]
InvalidTarget(String),
}
pub type PingResult<T> = Result<T, PingError>;
#[derive(Debug, Clone)]
pub struct PingConfig {
pub target: Ipv4Addr,
pub count: u16,
pub timeout: Duration,
pub interval: Duration,
pub packet_size: usize,
pub identifier: Option<u16>,
}
impl Default for PingConfig {
fn default() -> Self {
Self {
target: Ipv4Addr::new(127, 0, 0, 1),
count: 4,
timeout: Duration::from_secs(5),
interval: Duration::from_secs(1),
packet_size: 64,
identifier: None,
}
}
}
#[derive(Debug, Clone)]
pub struct PingReply {
pub sequence: u16,
pub rtt: Duration,
pub bytes_received: usize,
pub from: Ipv4Addr,
pub ttl: Option<u8>,
}
#[derive(Debug, Clone)]
pub struct PingStatistics {
pub packets_transmitted: u32,
pub packets_received: u32,
pub packet_loss: f64,
pub min_rtt: Option<Duration>,
pub max_rtt: Option<Duration>,
pub avg_rtt: Option<Duration>,
pub stddev_rtt: Option<Duration>,
}
impl PingStatistics {
pub fn new() -> Self {
Self {
packets_transmitted: 0,
packets_received: 0,
packet_loss: 0.0,
min_rtt: None,
max_rtt: None,
avg_rtt: None,
stddev_rtt: None,
}
}
pub fn add_reply(&mut self, reply: &PingReply) {
self.packets_received += 1;
self.min_rtt = Some(self.min_rtt.map_or(reply.rtt, |min| min.min(reply.rtt)));
self.max_rtt = Some(self.max_rtt.map_or(reply.rtt, |max| max.max(reply.rtt)));
}
pub fn add_transmitted(&mut self) {
self.packets_transmitted += 1;
}
pub fn finalize(&mut self, rtts: &[Duration]) {
self.packet_loss = if self.packets_transmitted > 0 {
100.0 * (1.0 - (self.packets_received as f64 / self.packets_transmitted as f64))
} else {
0.0
};
if !rtts.is_empty() {
let total: Duration = rtts.iter().sum();
self.avg_rtt = Some(total / rtts.len() as u32);
if let Some(avg) = self.avg_rtt {
let variance: f64 = rtts
.iter()
.map(|rtt| {
let diff = rtt.as_secs_f64() - avg.as_secs_f64();
diff * diff
})
.sum::<f64>()
/ rtts.len() as f64;
self.stddev_rtt = Some(Duration::from_secs_f64(variance.sqrt()));
}
}
}
}
impl Default for PingStatistics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PingMode {
Icmp,
Udp,
Tcp,
}
impl std::fmt::Display for PingMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PingMode::Icmp => write!(f, "ICMP"),
PingMode::Udp => write!(f, "UDP"),
PingMode::Tcp => write!(f, "TCP"),
}
}
}
pub fn calculate_checksum(data: &[u8]) -> u16 {
let mut sum = 0u32;
for chunk in data.chunks(2) {
if chunk.len() == 2 {
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
} else {
sum += (chunk[0] as u32) << 8;
}
}
while (sum >> 16) != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!sum as u16
}
pub fn resolve_hostname(hostname: &str) -> PingResult<Ipv4Addr> {
if let Ok(ip) = hostname.parse::<Ipv4Addr>() {
return Ok(ip);
}
if hostname == "localhost" {
return Ok(Ipv4Addr::new(127, 0, 0, 1));
}
use std::net::ToSocketAddrs;
let hostname_with_port = format!("{}:80", hostname);
match hostname_with_port.to_socket_addrs() {
Ok(mut addrs) => {
if let Some(addr) = addrs.next() {
if let std::net::IpAddr::V4(ipv4) = addr.ip() {
return Ok(ipv4);
}
}
Err(PingError::Configuration {
message: "Could not resolve to IPv4 address".to_string(),
})
}
Err(_) => Err(PingError::Configuration {
message: format!("Cannot resolve hostname: {}", hostname),
}),
}
}
pub fn resolve_hostnames(hostname: &str) -> PingResult<Vec<IpAddr>> {
use std::net::{Ipv6Addr, ToSocketAddrs};
if let Ok(ip) = hostname.parse::<IpAddr>() {
return Ok(vec![ip]);
}
if hostname == "localhost" {
return Ok(vec![
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
IpAddr::V6(Ipv6Addr::LOCALHOST),
]);
}
let host_port = format!("{}:0", hostname);
match host_port.to_socket_addrs() {
Ok(addrs) => {
let mut ips: Vec<IpAddr> = addrs.map(|a| a.ip()).collect();
ips.sort();
ips.dedup();
if ips.is_empty() {
Err(PingError::Configuration {
message: "Could not resolve hostname".to_string(),
})
} else {
Ok(ips)
}
}
Err(_) => Err(PingError::Configuration {
message: format!("Cannot resolve hostname: {}", hostname),
}),
}
}
pub mod icmp {
pub const ECHO_REQUEST: u8 = 8;
pub const ECHO_REPLY: u8 = 0;
pub const DEST_UNREACHABLE: u8 = 3;
pub const PORT_UNREACHABLE: u8 = 3;
}
pub mod ports {
pub const DNS: u16 = 53;
pub const HTTP: u16 = 80;
pub const HTTPS: u16 = 443;
pub const NTP: u16 = 123;
pub const SNMP: u16 = 161;
pub const SSH: u16 = 22;
pub const TRACEROUTE_BASE: u16 = 33434;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_checksum() {
let data = [0x45, 0x00, 0x00, 0x3c];
let checksum = calculate_checksum(&data);
assert_ne!(checksum, 0);
}
#[test]
fn test_ping_statistics() {
let mut stats = PingStatistics::new();
stats.add_transmitted();
stats.add_transmitted();
let reply1 = PingReply {
sequence: 1,
rtt: Duration::from_millis(10),
bytes_received: 64,
from: Ipv4Addr::new(8, 8, 8, 8),
ttl: Some(64),
};
stats.add_reply(&reply1);
let rtts = vec![Duration::from_millis(10)];
stats.finalize(&rtts);
assert_eq!(stats.packets_transmitted, 2);
assert_eq!(stats.packets_received, 1);
assert_eq!(stats.packet_loss, 50.0);
}
#[test]
fn test_resolve_localhost() {
assert_eq!(
resolve_hostname("localhost").unwrap(),
Ipv4Addr::new(127, 0, 0, 1)
);
}
#[test]
fn test_resolve_ip() {
assert_eq!(
resolve_hostname("8.8.8.8").unwrap(),
Ipv4Addr::new(8, 8, 8, 8)
);
}
#[test]
fn test_resolve_hostnames_local() {
let ips = resolve_hostnames("localhost").unwrap();
assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
}
}