1use std::net::{IpAddr, Ipv4Addr};
4use std::time::Duration;
5use thiserror::Error;
6
7#[derive(Error, Debug, Clone)]
9pub enum PingError {
10 #[error("Failed to create socket: {0}")]
11 SocketCreation(String),
12
13 #[error("Permission denied: {context}")]
14 PermissionDenied { context: String },
15
16 #[error("Timeout after {duration:?}")]
17 Timeout { duration: Duration },
18
19 #[error("Invalid response: {reason}")]
20 InvalidResponse { reason: String },
21
22 #[error("Network unreachable")]
23 NetworkUnreachable,
24
25 #[error("Host unreachable")]
26 HostUnreachable,
27
28 #[error("Port unreachable")]
29 PortUnreachable,
30
31 #[error("Configuration error: {message}")]
32 Configuration { message: String },
33
34 #[error("Invalid target: {0}")]
35 InvalidTarget(String),
36}
37
38pub type PingResult<T> = Result<T, PingError>;
40
41#[derive(Debug, Clone)]
43pub struct PingConfig {
44 pub target: Ipv4Addr,
46 pub count: u16,
48 pub timeout: Duration,
50 pub interval: Duration,
52 pub packet_size: usize,
54 pub identifier: Option<u16>,
56}
57
58impl Default for PingConfig {
59 fn default() -> Self {
60 Self {
61 target: Ipv4Addr::new(127, 0, 0, 1),
62 count: 4,
63 timeout: Duration::from_secs(5),
64 interval: Duration::from_secs(1),
65 packet_size: 64,
66 identifier: None,
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct PingReply {
74 pub sequence: u16,
76 pub rtt: Duration,
78 pub bytes_received: usize,
80 pub from: Ipv4Addr,
82 pub ttl: Option<u8>,
84}
85
86#[derive(Debug, Clone)]
88pub struct PingStatistics {
89 pub packets_transmitted: u32,
91 pub packets_received: u32,
93 pub packet_loss: f64,
95 pub min_rtt: Option<Duration>,
97 pub max_rtt: Option<Duration>,
99 pub avg_rtt: Option<Duration>,
101 pub stddev_rtt: Option<Duration>,
103}
104
105impl PingStatistics {
106 pub fn new() -> Self {
108 Self {
109 packets_transmitted: 0,
110 packets_received: 0,
111 packet_loss: 0.0,
112 min_rtt: None,
113 max_rtt: None,
114 avg_rtt: None,
115 stddev_rtt: None,
116 }
117 }
118
119 pub fn add_reply(&mut self, reply: &PingReply) {
121 self.packets_received += 1;
122
123 self.min_rtt = Some(self.min_rtt.map_or(reply.rtt, |min| min.min(reply.rtt)));
125 self.max_rtt = Some(self.max_rtt.map_or(reply.rtt, |max| max.max(reply.rtt)));
126 }
127
128 pub fn add_transmitted(&mut self) {
130 self.packets_transmitted += 1;
131 }
132
133 pub fn finalize(&mut self, rtts: &[Duration]) {
135 self.packet_loss = if self.packets_transmitted > 0 {
137 100.0 * (1.0 - (self.packets_received as f64 / self.packets_transmitted as f64))
138 } else {
139 0.0
140 };
141
142 if !rtts.is_empty() {
144 let total: Duration = rtts.iter().sum();
145 self.avg_rtt = Some(total / rtts.len() as u32);
146
147 if let Some(avg) = self.avg_rtt {
149 let variance: f64 = rtts
150 .iter()
151 .map(|rtt| {
152 let diff = rtt.as_secs_f64() - avg.as_secs_f64();
153 diff * diff
154 })
155 .sum::<f64>()
156 / rtts.len() as f64;
157
158 self.stddev_rtt = Some(Duration::from_secs_f64(variance.sqrt()));
159 }
160 }
161 }
162}
163
164impl Default for PingStatistics {
165 fn default() -> Self {
166 Self::new()
167 }
168}
169
170#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum PingMode {
173 Icmp,
175 Udp,
177 Tcp,
179}
180
181impl std::fmt::Display for PingMode {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 match self {
184 PingMode::Icmp => write!(f, "ICMP"),
185 PingMode::Udp => write!(f, "UDP"),
186 PingMode::Tcp => write!(f, "TCP"),
187 }
188 }
189}
190
191pub fn calculate_checksum(data: &[u8]) -> u16 {
193 let mut sum = 0u32;
194
195 for chunk in data.chunks(2) {
197 if chunk.len() == 2 {
198 sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
199 } else {
200 sum += (chunk[0] as u32) << 8;
202 }
203 }
204
205 while (sum >> 16) != 0 {
207 sum = (sum & 0xFFFF) + (sum >> 16);
208 }
209
210 !sum as u16
212}
213
214pub fn resolve_hostname(hostname: &str) -> PingResult<Ipv4Addr> {
216 if let Ok(ip) = hostname.parse::<Ipv4Addr>() {
218 return Ok(ip);
219 }
220
221 if hostname == "localhost" {
223 return Ok(Ipv4Addr::new(127, 0, 0, 1));
224 }
225
226 use std::net::ToSocketAddrs;
228 let hostname_with_port = format!("{}:80", hostname);
229 match hostname_with_port.to_socket_addrs() {
230 Ok(mut addrs) => {
231 if let Some(addr) = addrs.next() {
232 if let std::net::IpAddr::V4(ipv4) = addr.ip() {
233 return Ok(ipv4);
234 }
235 }
236 Err(PingError::Configuration {
237 message: "Could not resolve to IPv4 address".to_string(),
238 })
239 }
240 Err(_) => Err(PingError::Configuration {
241 message: format!("Cannot resolve hostname: {}", hostname),
242 }),
243 }
244}
245
246pub fn resolve_hostnames(hostname: &str) -> PingResult<Vec<IpAddr>> {
248 use std::net::{Ipv6Addr, ToSocketAddrs};
249
250 if let Ok(ip) = hostname.parse::<IpAddr>() {
252 return Ok(vec![ip]);
253 }
254
255 if hostname == "localhost" {
256 return Ok(vec![
257 IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
258 IpAddr::V6(Ipv6Addr::LOCALHOST),
259 ]);
260 }
261
262 let host_port = format!("{}:0", hostname);
263 match host_port.to_socket_addrs() {
264 Ok(addrs) => {
265 let mut ips: Vec<IpAddr> = addrs.map(|a| a.ip()).collect();
266 ips.sort();
267 ips.dedup();
268 if ips.is_empty() {
269 Err(PingError::Configuration {
270 message: "Could not resolve hostname".to_string(),
271 })
272 } else {
273 Ok(ips)
274 }
275 }
276 Err(_) => Err(PingError::Configuration {
277 message: format!("Cannot resolve hostname: {}", hostname),
278 }),
279 }
280}
281
282pub mod icmp {
284 pub const ECHO_REQUEST: u8 = 8;
285 pub const ECHO_REPLY: u8 = 0;
286 pub const DEST_UNREACHABLE: u8 = 3;
287 pub const PORT_UNREACHABLE: u8 = 3;
288}
289
290pub mod ports {
292 pub const DNS: u16 = 53;
293 pub const HTTP: u16 = 80;
294 pub const HTTPS: u16 = 443;
295 pub const NTP: u16 = 123;
296 pub const SNMP: u16 = 161;
297 pub const SSH: u16 = 22;
298 pub const TRACEROUTE_BASE: u16 = 33434;
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304
305 #[test]
306 fn test_checksum() {
307 let data = [0x45, 0x00, 0x00, 0x3c];
308 let checksum = calculate_checksum(&data);
309 assert_ne!(checksum, 0);
310 }
311
312 #[test]
313 fn test_ping_statistics() {
314 let mut stats = PingStatistics::new();
315
316 stats.add_transmitted();
317 stats.add_transmitted();
318
319 let reply1 = PingReply {
320 sequence: 1,
321 rtt: Duration::from_millis(10),
322 bytes_received: 64,
323 from: Ipv4Addr::new(8, 8, 8, 8),
324 ttl: Some(64),
325 };
326
327 stats.add_reply(&reply1);
328
329 let rtts = vec![Duration::from_millis(10)];
330 stats.finalize(&rtts);
331
332 assert_eq!(stats.packets_transmitted, 2);
333 assert_eq!(stats.packets_received, 1);
334 assert_eq!(stats.packet_loss, 50.0);
335 }
336
337 #[test]
338 fn test_resolve_localhost() {
339 assert_eq!(
340 resolve_hostname("localhost").unwrap(),
341 Ipv4Addr::new(127, 0, 0, 1)
342 );
343 }
344
345 #[test]
346 fn test_resolve_ip() {
347 assert_eq!(
348 resolve_hostname("8.8.8.8").unwrap(),
349 Ipv4Addr::new(8, 8, 8, 8)
350 );
351 }
352
353 #[test]
354 fn test_resolve_hostnames_local() {
355 let ips = resolve_hostnames("localhost").unwrap();
356 assert!(ips.contains(&IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
357 }
358}