anti_common/
lib.rs

1//! Common types and utilities for ping operations
2
3use std::net::{IpAddr, Ipv4Addr};
4use std::time::Duration;
5use thiserror::Error;
6
7/// Errors that can occur during ping operations
8#[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
38/// Result type for ping operations
39pub type PingResult<T> = Result<T, PingError>;
40
41/// Configuration for ping operations
42#[derive(Debug, Clone)]
43pub struct PingConfig {
44    /// Target IP address
45    pub target: Ipv4Addr,
46    /// Number of ping packets to send
47    pub count: u16,
48    /// Timeout for each ping
49    pub timeout: Duration,
50    /// Interval between pings
51    pub interval: Duration,
52    /// Packet size in bytes
53    pub packet_size: usize,
54    /// Custom identifier (if None, random will be generated)
55    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/// Result of a single ping operation
72#[derive(Debug, Clone)]
73pub struct PingReply {
74    /// Sequence number of the ping
75    pub sequence: u16,
76    /// Round-trip time
77    pub rtt: Duration,
78    /// Size of the response in bytes
79    pub bytes_received: usize,
80    /// Source address of the response
81    pub from: Ipv4Addr,
82    /// Time-to-live of the response packet
83    pub ttl: Option<u8>,
84}
85
86/// Summary statistics for a series of ping operations
87#[derive(Debug, Clone)]
88pub struct PingStatistics {
89    /// Total packets transmitted
90    pub packets_transmitted: u32,
91    /// Total packets received
92    pub packets_received: u32,
93    /// Packet loss percentage (0.0 to 100.0)
94    pub packet_loss: f64,
95    /// Minimum round-trip time
96    pub min_rtt: Option<Duration>,
97    /// Maximum round-trip time
98    pub max_rtt: Option<Duration>,
99    /// Average round-trip time
100    pub avg_rtt: Option<Duration>,
101    /// Standard deviation of round-trip times
102    pub stddev_rtt: Option<Duration>,
103}
104
105impl PingStatistics {
106    /// Create new empty statistics
107    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    /// Update statistics with a new ping reply
120    pub fn add_reply(&mut self, reply: &PingReply) {
121        self.packets_received += 1;
122
123        // Update min/max
124        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    /// Record a transmitted packet
129    pub fn add_transmitted(&mut self) {
130        self.packets_transmitted += 1;
131    }
132
133    /// Finalize statistics calculations
134    pub fn finalize(&mut self, rtts: &[Duration]) {
135        // Calculate packet loss
136        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        // Calculate average
143        if !rtts.is_empty() {
144            let total: Duration = rtts.iter().sum();
145            self.avg_rtt = Some(total / rtts.len() as u32);
146
147            // Calculate standard deviation
148            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/// Type of ping operation
171#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum PingMode {
173    /// ICMP echo request/reply
174    Icmp,
175    /// UDP packet with ICMP Port Unreachable response
176    Udp,
177    /// TCP connection attempt
178    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
191/// Calculate internet checksum for a byte array
192pub fn calculate_checksum(data: &[u8]) -> u16 {
193    let mut sum = 0u32;
194
195    // Sum all 16-bit words
196    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            // Handle odd-length data
201            sum += (chunk[0] as u32) << 8;
202        }
203    }
204
205    // Add carry bits
206    while (sum >> 16) != 0 {
207        sum = (sum & 0xFFFF) + (sum >> 16);
208    }
209
210    // One's complement
211    !sum as u16
212}
213
214/// Resolve hostname to IPv4 address
215pub fn resolve_hostname(hostname: &str) -> PingResult<Ipv4Addr> {
216    // Try to parse as IP address first
217    if let Ok(ip) = hostname.parse::<Ipv4Addr>() {
218        return Ok(ip);
219    }
220
221    // Handle localhost specially
222    if hostname == "localhost" {
223        return Ok(Ipv4Addr::new(127, 0, 0, 1));
224    }
225
226    // Use proper DNS resolution
227    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
246/// Resolve a hostname to all associated IPv4 and IPv6 addresses
247pub fn resolve_hostnames(hostname: &str) -> PingResult<Vec<IpAddr>> {
248    use std::net::{Ipv6Addr, ToSocketAddrs};
249
250    // Direct IP
251    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
282/// ICMP packet constants
283pub 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
290/// Common UDP ports for testing
291pub 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}