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}