Skip to main content

single_ping/
lib.rs

1//! A simple ICMP ping library for Rust.
2//!
3//! This library provides a straightforward way to send ICMP echo requests (pings)
4//! to hosts using both IPv4 and IPv6. It requires elevated privileges (root/sudo)
5//! to create raw sockets.
6//!
7//! # Examples
8//!
9//! ```no_run
10//! use single_ping::ping;
11//!
12//! let result = ping("example.com", 1000, 32).unwrap();
13//! println!("Ping successful with {}ms latency", result.latency_ms);
14//! ```
15use socket2::{Domain, Protocol, Socket, Type};
16use std::net::{IpAddr, ToSocketAddrs};
17use std::time::{Duration, Instant};
18
19/// Result of a ping operation.
20///
21/// Contains information about whether the ping was successful and the measured latency.
22pub struct PingResult {
23    /// Whether the packet was dropped (no response received or invalid response)
24    pub dropped: bool,
25    /// Latency in milliseconds
26    pub latency_ms: u64,
27}
28
29pub fn ping(host: &str, timeout: u64, size: u64) -> Result<PingResult, Box<dyn std::error::Error>> {
30    // Convert timeout from milliseconds to Duration
31    let timeout_duration = Duration::from_millis(timeout);
32
33    // Resolve the host to an IP address
34    let ip = resolve_host(host)?;
35
36    // Create raw ICMP socket based on IP version
37    let (socket, _domain, _protocol) = match ip {
38        IpAddr::V4(_) => {
39            let socket = Socket::new_raw(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))
40                .unwrap_or_else(|_| {
41                    println!("Failed to create IPv4 socket. You may need to run with `sudo`.");
42                    std::process::exit(1);
43                });
44            (socket, Domain::IPV4, Protocol::ICMPV4)
45        }
46        IpAddr::V6(_) => {
47            let socket = Socket::new_raw(Domain::IPV6, Type::RAW, Some(Protocol::ICMPV6))?;
48            (socket, Domain::IPV6, Protocol::ICMPV6)
49        }
50    };
51
52    socket.set_read_timeout(Some(timeout_duration)).unwrap();
53
54    // Build ICMP packet based on IP version
55    let packet = match ip {
56        IpAddr::V4(_) => build_icmpv4_packet(size),
57        IpAddr::V6(_) => build_icmpv6_packet(size),
58    };
59
60    // Send packet
61    let start_time = Instant::now();
62    let addr = std::net::SocketAddr::new(ip, 0);
63    socket.send_to(&packet, &addr.into()).unwrap();
64
65    // Receive reply
66    let mut buffer = [std::mem::MaybeUninit::<u8>::uninit(); 1024];
67    match socket.recv_from(&mut buffer) {
68        Ok((bytes_received, _)) => {
69            let end_time = Instant::now();
70            let latency = end_time.duration_since(start_time).as_millis() as u64;
71
72            // Validate reply based on IP version
73            let is_valid = match ip {
74                IpAddr::V4(_) => validate_icmpv4_reply(&buffer, bytes_received),
75                IpAddr::V6(_) => validate_icmpv6_reply(&buffer, bytes_received),
76            };
77
78            Ok(PingResult {
79                dropped: !is_valid,
80                latency_ms: latency,
81            })
82        }
83        Err(_) => {
84            // Timeout or other error
85            let end_time = Instant::now();
86            let latency = end_time.duration_since(start_time).as_millis() as u64;
87            Ok(PingResult {
88                dropped: true,
89                latency_ms: latency,
90            })
91        }
92    }
93}
94
95/// Calculates the ICMP checksum for a packet.
96///
97/// The checksum is calculated as the one's complement of the one's complement sum
98/// of all 16-bit words in the packet. This is the standard Internet checksum algorithm
99/// used for ICMP packets.
100///
101/// # Arguments
102///
103/// * `packet` - The packet data for which to calculate the checksum
104///
105/// # Returns
106///
107/// The calculated checksum as a 16-bit unsigned integer
108fn calculate_icmp_checksum(packet: &[u8]) -> u16 {
109    let mut sum: u32 = 0;
110
111    // Sum all 16-bit words
112    for chunk in packet.chunks(2) {
113        let word = if chunk.len() == 2 {
114            u16::from_be_bytes([chunk[0], chunk[1]])
115        } else {
116            u16::from_be_bytes([chunk[0], 0])
117        };
118        sum += word as u32;
119    }
120
121    // Add carry bits
122    while (sum >> 16) != 0 {
123        sum = (sum & 0xFFFF) + (sum >> 16);
124    }
125
126    // One's complement
127    !sum as u16
128}
129
130/// Resolves a hostname or IP address string to an `IpAddr`.
131///
132/// This function first attempts to parse the input as a direct IP address.
133/// If that fails, it performs DNS resolution to convert a hostname to an IP address.
134///
135/// # Arguments
136///
137/// * `host` - A string containing either an IP address (e.g., "8.8.8.8") or hostname (e.g., "google.com")
138///
139/// # Returns
140///
141/// * `Ok(IpAddr)` - The resolved IP address
142/// * `Err(std::io::Error)` - If the host cannot be parsed or resolved
143fn resolve_host(host: &str) -> Result<IpAddr, std::io::Error> {
144    // First try to parse as IP address directly
145    if let Ok(ip) = host.parse::<IpAddr>() {
146        return Ok(ip);
147    }
148
149    // Try DNS resolution
150    let address = format!("{}:80", host); // Add dummy port for resolution
151    match address.to_socket_addrs() {
152        Ok(mut addrs) => {
153            if let Some(addr) = addrs.next() {
154                Ok(addr.ip())
155            } else {
156                Err(std::io::Error::new(
157                    std::io::ErrorKind::NotFound,
158                    format!("No addresses found for host: {}", host),
159                ))
160            }
161        }
162        Err(e) => Err(std::io::Error::new(
163            std::io::ErrorKind::HostUnreachable,
164            format!("Failed to resolve host {}: {}", host, e),
165        )),
166    }
167}
168
169/// Builds an ICMPv4 Echo Request packet.
170///
171/// Constructs a complete ICMP packet with the standard 8-byte header followed by
172/// data payload. The packet includes:
173/// - Type field set to 8 (Echo Request)
174/// - Code field set to 0
175/// - A calculated checksum
176/// - Identifier and sequence number fields
177/// - Data payload filled with a repeating pattern
178///
179/// # Arguments
180///
181/// * `size` - The size of the data payload in bytes (excluding the 8-byte header)
182///
183/// # Returns
184///
185/// A vector containing the complete ICMP packet with calculated checksum
186fn build_icmpv4_packet(size: u64) -> Vec<u8> {
187    // Build ICMP Echo Request packet
188    // ICMP header (8 bytes) + size padding
189    let mut packet = vec![0u8; 8 + size as usize];
190
191    // ICMP Header
192    packet[0] = 8; // Type: Echo Request
193    packet[1] = 0; // Code: 0
194    packet[2] = 0; // Checksum (will be calculated)
195    packet[3] = 0; // Checksum
196    packet[4] = 0; // Identifier (can be process ID)
197    packet[5] = 1; // Identifier
198    packet[6] = 0; // Sequence number
199    packet[7] = 1; // Sequence number
200
201    // Fill data portion with pattern
202    for i in 8..packet.len() {
203        packet[i] = (i % 256) as u8;
204    }
205
206    // Calculate checksum
207    let checksum = calculate_icmp_checksum(&packet);
208    packet[2] = (checksum >> 8) as u8;
209    packet[3] = (checksum & 0xff) as u8;
210
211    packet
212}
213
214/// Builds an ICMPv6 Echo Request packet.
215///
216/// Constructs a complete ICMPv6 packet with the standard 8-byte header followed by
217/// data payload. The packet includes:
218/// - Type field set to 128 (Echo Request for ICMPv6)
219/// - Code field set to 0
220/// - Checksum fields set to 0 (calculated by the kernel for IPv6)
221/// - Identifier and sequence number fields
222/// - Data payload filled with a repeating pattern
223///
224/// # Arguments
225///
226/// * `size` - The size of the data payload in bytes (excluding the 8-byte header)
227///
228/// # Returns
229///
230/// A vector containing the complete ICMPv6 packet (checksum will be calculated by kernel)
231fn build_icmpv6_packet(size: u64) -> Vec<u8> {
232    // Build ICMPv6 Echo Request packet
233    // ICMPv6 header (8 bytes) + size padding
234    let mut packet = vec![0u8; 8 + size as usize];
235
236    // ICMPv6 Header
237    packet[0] = 128; // Type: Echo Request (ICMPv6)
238    packet[1] = 0; // Code: 0
239    packet[2] = 0; // Checksum (calculated by kernel for IPv6)
240    packet[3] = 0; // Checksum
241    packet[4] = 0; // Identifier
242    packet[5] = 1; // Identifier
243    packet[6] = 0; // Sequence number
244    packet[7] = 1; // Sequence number
245
246    // Fill data portion with pattern
247    for i in 8..packet.len() {
248        packet[i] = (i % 256) as u8;
249    }
250
251    packet
252}
253
254/// Validates an ICMPv4 Echo Reply packet.
255///
256/// Checks if the received packet is a valid ICMP Echo Reply by examining the ICMP type field.
257/// The function accounts for the IPv4 header (20 bytes) before the ICMP data.
258///
259/// # Arguments
260///
261/// * `buffer` - The raw packet buffer containing the IPv4 header and ICMP data
262/// * `bytes_received` - The number of bytes received in the buffer
263///
264/// # Returns
265///
266/// `true` if the packet is a valid Echo Reply (type 0), `false` otherwise
267fn validate_icmpv4_reply(buffer: &[std::mem::MaybeUninit<u8>], bytes_received: usize) -> bool {
268    // Basic validation - check if it's an ICMP Echo Reply
269    if bytes_received >= 28 {
270        // IP header (20) + ICMP header (8)
271        let icmp_type = unsafe { buffer[20].assume_init() }; // ICMP type is at offset 20 (after IP header)
272        icmp_type == 0 // Echo Reply
273    } else {
274        false
275    }
276}
277
278/// Validates an ICMPv6 Echo Reply packet.
279///
280/// Checks if the received packet is a valid ICMPv6 Echo Reply by examining the ICMP type field.
281/// Unlike IPv4, ICMPv6 packets do not have an IP header in the received data.
282///
283/// # Arguments
284///
285/// * `buffer` - The raw packet buffer containing the ICMPv6 data
286/// * `bytes_received` - The number of bytes received in the buffer
287///
288/// # Returns
289///
290/// `true` if the packet is a valid Echo Reply (type 129), `false` otherwise
291fn validate_icmpv6_reply(buffer: &[std::mem::MaybeUninit<u8>], bytes_received: usize) -> bool {
292    // For ICMPv6, no IP header to skip, ICMP header starts immediately
293    if bytes_received >= 8 {
294        let icmp_type = unsafe { buffer[0].assume_init() };
295        icmp_type == 129 // ICMPv6 Echo Reply
296    } else {
297        false
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304    use std::net::{Ipv4Addr, Ipv6Addr};
305
306    #[test]
307    fn test_resolve_host_ipv4() {
308        let result = resolve_host("127.0.0.1");
309        assert!(result.is_ok());
310        assert_eq!(result.unwrap(), IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
311    }
312
313    #[test]
314    fn test_resolve_host_ipv6() {
315        let result = resolve_host("::1");
316        assert!(result.is_ok());
317        assert_eq!(
318            result.unwrap(),
319            IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
320        );
321    }
322
323    #[test]
324    fn test_resolve_host_localhost() {
325        let result = resolve_host("localhost");
326        assert!(result.is_ok());
327        // localhost should resolve to either 127.0.0.1 or ::1
328        let ip = result.unwrap();
329        assert!(
330            ip == IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))
331                || ip == IpAddr::V6(Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1))
332        );
333    }
334
335    #[test]
336    fn test_resolve_host_invalid() {
337        let result = resolve_host("invalid.domain.that.does.not.exist.12345");
338        assert!(result.is_err());
339    }
340
341    #[test]
342    fn test_build_icmpv4_packet() {
343        let packet = build_icmpv4_packet(8);
344
345        // Should be 8 (header) + 8 (size) = 16 bytes
346        assert_eq!(packet.len(), 16);
347
348        // Check ICMP header fields
349        assert_eq!(packet[0], 8); // Type: Echo Request
350        assert_eq!(packet[1], 0); // Code: 0
351        assert_eq!(packet[4], 0); // Identifier high byte
352        assert_eq!(packet[5], 1); // Identifier low byte
353        assert_eq!(packet[6], 0); // Sequence high byte
354        assert_eq!(packet[7], 1); // Sequence low byte
355
356        // Check data pattern
357        for i in 8..packet.len() {
358            assert_eq!(packet[i], (i % 256) as u8);
359        }
360
361        // Checksum should be calculated (non-zero)
362        assert!(packet[2] != 0 || packet[3] != 0);
363    }
364
365    #[test]
366    fn test_build_icmpv6_packet() {
367        let packet = build_icmpv6_packet(8);
368
369        // Should be 8 (header) + 8 (size) = 16 bytes
370        assert_eq!(packet.len(), 16);
371
372        // Check ICMPv6 header fields
373        assert_eq!(packet[0], 128); // Type: Echo Request (ICMPv6)
374        assert_eq!(packet[1], 0); // Code: 0
375        assert_eq!(packet[2], 0); // Checksum (calculated by kernel)
376        assert_eq!(packet[3], 0); // Checksum
377        assert_eq!(packet[4], 0); // Identifier high byte
378        assert_eq!(packet[5], 1); // Identifier low byte
379        assert_eq!(packet[6], 0); // Sequence high byte
380        assert_eq!(packet[7], 1); // Sequence low byte
381
382        // Check data pattern
383        for i in 8..packet.len() {
384            assert_eq!(packet[i], (i % 256) as u8);
385        }
386    }
387
388    #[test]
389    fn test_validate_icmpv4_reply_valid() {
390        // Create a mock IPv4 ICMP Echo Reply packet
391        let mut buffer = [std::mem::MaybeUninit::<u8>::uninit(); 1024];
392
393        // Fill IP header (20 bytes) + ICMP header (8 bytes)
394        for i in 0..28 {
395            buffer[i] = std::mem::MaybeUninit::new(0);
396        }
397        // Set ICMP type to Echo Reply (0) at offset 20
398        buffer[20] = std::mem::MaybeUninit::new(0);
399
400        assert!(validate_icmpv4_reply(&buffer, 28));
401    }
402
403    #[test]
404    fn test_validate_icmpv4_reply_invalid_type() {
405        let mut buffer = [std::mem::MaybeUninit::<u8>::uninit(); 1024];
406
407        // Fill IP header (20 bytes) + ICMP header (8 bytes)
408        for i in 0..28 {
409            buffer[i] = std::mem::MaybeUninit::new(0);
410        }
411        // Set ICMP type to something other than Echo Reply
412        buffer[20] = std::mem::MaybeUninit::new(8); // Echo Request instead of Reply
413
414        assert!(!validate_icmpv4_reply(&buffer, 28));
415    }
416
417    #[test]
418    fn test_validate_icmpv4_reply_too_short() {
419        let buffer = [std::mem::MaybeUninit::<u8>::uninit(); 1024];
420
421        // Packet too short (less than 28 bytes)
422        assert!(!validate_icmpv4_reply(&buffer, 20));
423    }
424
425    #[test]
426    fn test_validate_icmpv6_reply_valid() {
427        let mut buffer = [std::mem::MaybeUninit::<u8>::uninit(); 1024];
428
429        // Set ICMPv6 type to Echo Reply (129) at offset 0
430        buffer[0] = std::mem::MaybeUninit::new(129);
431        for i in 1..8 {
432            buffer[i] = std::mem::MaybeUninit::new(0);
433        }
434
435        assert!(validate_icmpv6_reply(&buffer, 8));
436    }
437
438    #[test]
439    fn test_validate_icmpv6_reply_invalid_type() {
440        let mut buffer = [std::mem::MaybeUninit::<u8>::uninit(); 1024];
441
442        // Set ICMPv6 type to something other than Echo Reply
443        buffer[0] = std::mem::MaybeUninit::new(128); // Echo Request instead of Reply
444        for i in 1..8 {
445            buffer[i] = std::mem::MaybeUninit::new(0);
446        }
447
448        assert!(!validate_icmpv6_reply(&buffer, 8));
449    }
450
451    #[test]
452    fn test_validate_icmpv6_reply_too_short() {
453        let buffer = [std::mem::MaybeUninit::<u8>::uninit(); 1024];
454
455        // Packet too short (less than 8 bytes)
456        assert!(!validate_icmpv6_reply(&buffer, 4));
457    }
458
459    #[test]
460    fn test_calculate_icmp_checksum() {
461        // Test with a simple packet
462        let mut packet = vec![8, 0, 0, 0, 0, 1, 0, 1]; // Basic ICMP header
463
464        // Clear checksum field
465        packet[2] = 0;
466        packet[3] = 0;
467
468        let checksum = calculate_icmp_checksum(&packet);
469
470        // Checksum should be non-zero for this packet
471        assert_ne!(checksum, 0);
472
473        // Verify checksum by including it in the packet and recalculating
474        packet[2] = (checksum >> 8) as u8;
475        packet[3] = (checksum & 0xff) as u8;
476
477        let verify_checksum = calculate_icmp_checksum(&packet);
478        assert_eq!(verify_checksum, 0); // Should be 0 when checksum is correct
479    }
480}