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}