ping_async/lib.rs
1//! Unprivileged Async Ping
2//!
3//! This crate provides asynchronous ICMP echo request (ping) functionality that works
4//! without requiring elevated privileges on Windows, macOS, and Linux platforms.
5//!
6//! ## Platform Support
7//!
8//! - **Windows**: Uses Windows APIs (`IcmpSendEcho2Ex` and `Icmp6SendEcho2`) that provide
9//! unprivileged ICMP functionality without requiring administrator rights.
10//! - **macOS/Linux**: Uses ICMP sockets with Tokio for async operations. On Linux, requires
11//! the `net.ipv4.ping_group_range` sysctl parameter to allow unprivileged ICMP sockets.
12//!
13//! ## Basic Usage
14//!
15//! ```rust,no_run
16//! use ping_async::IcmpEchoRequestor;
17//! use std::net::IpAddr;
18//!
19//! #[tokio::main]
20//! async fn main() -> std::io::Result<()> {
21//! let target = "8.8.8.8".parse::<IpAddr>().unwrap();
22//! let pinger = IcmpEchoRequestor::new(target, None, None, None)?;
23//!
24//! let reply = pinger.send().await?;
25//! println!("Reply from {}: {:?} in {:?}",
26//! reply.destination(),
27//! reply.status(),
28//! reply.round_trip_time()
29//! );
30//!
31//! Ok(())
32//! }
33//! ```
34
35#[cfg(not(target_os = "windows"))]
36mod icmp;
37
38mod platform;
39pub use platform::IcmpEchoRequestor;
40
41use std::net::IpAddr;
42use std::time::Duration;
43
44/// Default Time-To-Live (TTL) value for ICMP packets.
45/// This matches the default TTL used by most ping implementations.
46pub const PING_DEFAULT_TTL: u8 = 128;
47
48/// Default timeout duration for ICMP echo requests.
49/// Requests that don't receive a reply within this time will be marked as timed out.
50pub const PING_DEFAULT_TIMEOUT: Duration = Duration::from_secs(1);
51
52/// Default length of the data payload in ICMP echo request packets.
53/// This matches the default payload size used by most ping implementations.
54pub const PING_DEFAULT_REQUEST_DATA_LENGTH: usize = 32;
55
56/// Status of an ICMP echo request/reply exchange.
57///
58/// This enum represents the different outcomes that can occur when sending
59/// an ICMP echo request and waiting for a reply.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub enum IcmpEchoStatus {
62 /// The echo request was successful and a reply was received.
63 Success,
64 /// The echo request timed out - no reply was received within the timeout period.
65 TimedOut,
66 /// The destination was unreachable (network, host, port, or protocol unreachable).
67 Unreachable,
68 /// An unknown error occurred during the ping operation.
69 Unknown,
70}
71
72impl IcmpEchoStatus {
73 /// Converts the status to a `Result`, returning `Ok(())` for success or an error message for failures.
74 ///
75 /// # Examples
76 ///
77 /// ```rust
78 /// use ping_async::IcmpEchoStatus;
79 ///
80 /// let status = IcmpEchoStatus::Success;
81 /// assert!(status.ok().is_ok());
82 ///
83 /// let status = IcmpEchoStatus::TimedOut;
84 /// assert!(status.ok().is_err());
85 /// ```
86 pub fn ok(self) -> Result<(), String> {
87 match self {
88 Self::Success => Ok(()),
89 Self::TimedOut => Err("Timed out".to_string()),
90 Self::Unreachable => Err("Destination unreachable".to_string()),
91 Self::Unknown => Err("Unknown error".to_string()),
92 }
93 }
94}
95
96/// Reply received from an ICMP echo request.
97///
98/// Contains the destination IP address, status of the ping operation,
99/// and the measured round-trip time.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub struct IcmpEchoReply {
102 destination: IpAddr,
103 status: IcmpEchoStatus,
104 round_trip_time: Duration,
105}
106
107impl IcmpEchoReply {
108 /// Creates a new ICMP echo reply.
109 ///
110 /// # Arguments
111 ///
112 /// * `destination` - The IP address that was pinged
113 /// * `status` - The status of the ping operation
114 /// * `round_trip_time` - The measured round-trip time
115 pub fn new(destination: IpAddr, status: IcmpEchoStatus, round_trip_time: Duration) -> Self {
116 Self {
117 destination,
118 status,
119 round_trip_time,
120 }
121 }
122
123 /// Returns the destination IP address that was pinged.
124 pub fn destination(&self) -> IpAddr {
125 self.destination
126 }
127
128 /// Returns the status of the ping operation.
129 pub fn status(&self) -> IcmpEchoStatus {
130 self.status
131 }
132
133 /// Returns the measured round-trip time.
134 ///
135 /// For successful pings, this represents the time between sending the echo request
136 /// and receiving the echo reply. For failed pings, this may be zero or represent
137 /// the time until the failure was detected.
138 pub fn round_trip_time(&self) -> Duration {
139 self.round_trip_time
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[tokio::test]
148 async fn ping_localhost_v4() -> std::io::Result<()> {
149 let pinger = IcmpEchoRequestor::new("127.0.0.1".parse().unwrap(), None, None, None)?;
150 let reply = pinger.send().await?;
151
152 assert_eq!(reply.destination(), "127.0.0.1".parse::<IpAddr>().unwrap());
153 println!("IPv4 ping result: {reply:?}");
154
155 Ok(())
156 }
157
158 #[tokio::test]
159 async fn ping_localhost_v6() -> std::io::Result<()> {
160 let pinger = IcmpEchoRequestor::new("::1".parse().unwrap(), None, None, None)?;
161 let reply = pinger.send().await?;
162
163 assert_eq!(reply.destination(), "::1".parse::<IpAddr>().unwrap());
164 println!("IPv6 ping result: {reply:?}");
165
166 Ok(())
167 }
168
169 #[tokio::test]
170 async fn test_thread_safety() -> std::io::Result<()> {
171 let pinger = IcmpEchoRequestor::new("127.0.0.1".parse().unwrap(), None, None, None)?;
172
173 // Test that we can clone and use across threads
174 let pinger_clone = pinger.clone();
175 let handle = tokio::spawn(async move { pinger_clone.send().await });
176
177 let reply = handle.await.unwrap()?;
178 assert_eq!(reply.destination(), "127.0.0.1".parse::<IpAddr>().unwrap());
179
180 Ok(())
181 }
182
183 #[test]
184 fn test_send_sync_traits() {
185 // Compile-time verification that IcmpEchoRequestor implements Send + Sync
186 fn assert_send<T: Send>() {}
187 fn assert_sync<T: Sync>() {}
188
189 assert_send::<IcmpEchoRequestor>();
190 assert_sync::<IcmpEchoRequestor>();
191 assert_send::<IcmpEchoReply>();
192 assert_sync::<IcmpEchoReply>();
193 assert_send::<IcmpEchoStatus>();
194 assert_sync::<IcmpEchoStatus>();
195 }
196
197 #[tokio::test]
198 async fn test_concurrent_pings() -> std::io::Result<()> {
199 let pinger = IcmpEchoRequestor::new("127.0.0.1".parse().unwrap(), None, None, None)?;
200
201 // Spawn multiple concurrent ping tasks
202 let mut handles = Vec::new();
203 for _ in 0..5 {
204 let pinger_clone = pinger.clone();
205 let handle = tokio::spawn(async move { pinger_clone.send().await });
206 handles.push(handle);
207 }
208
209 // Wait for all pings to complete
210 for handle in handles {
211 let reply = handle.await.unwrap()?;
212 assert_eq!(reply.destination(), "127.0.0.1".parse::<IpAddr>().unwrap());
213 }
214
215 Ok(())
216 }
217
218 #[tokio::test]
219 async fn test_multiple_requestors_independent_routers() -> std::io::Result<()> {
220 // Create multiple requestors - each should have its own router when used
221 let pinger1 = IcmpEchoRequestor::new("127.0.0.1".parse().unwrap(), None, None, None)?;
222 let pinger2 = IcmpEchoRequestor::new("::1".parse().unwrap(), None, None, None)?;
223
224 // Both should work independently
225 let reply1 = pinger1.send().await?;
226 let reply2 = pinger2.send().await?;
227
228 assert_eq!(reply1.destination(), "127.0.0.1".parse::<IpAddr>().unwrap());
229 assert_eq!(reply2.destination(), "::1".parse::<IpAddr>().unwrap());
230
231 Ok(())
232 }
233}