async_icmp/
lib.rs

1//! An unprivileged async ICMP socket library for macOS and Linux using `tokio`.
2//!
3//! This uses `PROT_ICMP` sockets, so `root` is not needed, though on Linux access to
4//! that socket type is limited by a sysctl (see Linux link below). Some distros set the sysctl to
5//! allow all users access out of the box, while others will need configuration.
6//!
7//! See also:
8//! - `examples/ping.rs` for how to use this library to build a fairly complete ping CLI tool.
9//! - Linux `PROT_ICMP` sockets: <https://lwn.net/Articles/443051/>
10//! - macOS `PROT_ICMP` sockets: <http://www.manpagez.com/man/4/icmp/>
11//! - ICMPv4: <https://www.rfc-editor.org/rfc/rfc792>
12//! - ICMPv6: <https://www.rfc-editor.org/rfc/rfc4443>
13//!
14//! # Getting started
15//!
16//! - Open an [`socket::IcmpSocket`]
17//! - Create a suitable [`message::echo::IcmpEchoRequest`] or other impl of [`message::EncodeIcmpMessage`] and send it with the socket
18//! - Handle received ICMP messages, either as raw bytes or with [`message::decode::DecodedIcmpMsg`]
19//! - Or, if you just want to ping (aka ICMP Echo), see [`ping`] for high-level ping-specific
20//!   functionality.
21//!
22//! # Features
23//!
24//! - `rand` (enabled by default): Generate random [`message::echo::EchoId`]s conveniently. Adds a dependency on
25//!   [`rand`](https://crates.io/crates/rand)
26//!
27//! # Platform differences
28//!
29//! See [`platform`] for functions to help you understand what behavior to expect on a given platform.
30//!
31//! The links above for Linux and macOS sockets are helpful for more detail, but here's the short
32//! version.
33//!
34//! ## Linux
35//!
36//! - You can only send Echo messages. Those messages will have their `id` field overwritten
37//!   with the local socket's port.
38//! - Reading messages only provides Echo Reply messages with a matching `id` (aka port).
39//! - Calculating checksums is not required.
40//! - IPv6 works like IPv4.
41//!
42//! ## macOS
43//!
44//! - You can use any `id` with your Echo messages.
45//! - Reading messages appears to be more or less unfiltered. You'll read the Echo you sent,
46//!   as well as the Echo Reply that may eventually arrive, regardless of its `id`.
47//! - IPv4 sent messages must have the checksum set (done by this library)
48//! - When reading IPv4 messages, the IPv4 headers are included as a prefix (stripped off by the library)
49//! - IPv6 appears to be less reliable than IPv4 -- higher packet rates tend to cause packet loss as
50//!   of macOS 15.2. See `exampes/packet_loss.rs` to try it on your system.
51//!
52//! # Examples
53//!
54//! These show the simplest possible usage. See the rest of the docs for more.
55//!
56//! ## Sending a single Echo
57//!
58//! ```no_run
59//! use std::{error, net};
60//! use async_icmp::{Icmpv4,
61//!     message::echo::{IcmpEchoRequest, EchoId, EchoSeq},
62//!     socket::{IcmpSocket, SocketConfig}};
63//!
64//! async fn send_ping() -> Result<(), Box<dyn error::Error>> {
65//!     let socket = IcmpSocket::<Icmpv4>::new(SocketConfig::default())?;
66//!     let mut echo = IcmpEchoRequest::from_fields(EchoId::from_be(100), EchoSeq::from_be(200), b"data");
67//!     socket.send_to( &mut echo, net::Ipv4Addr::LOCALHOST).await?;
68//!
69//!     Ok(())
70//! }
71//! ```
72//!
73//! ## Receiving an ICMP message
74//!
75//! ```no_run
76//! use async_icmp::{Icmpv4,
77//!     message::{decode::DecodedIcmpMsg, IcmpV4MsgType},
78//!     socket::{IcmpSocket, SocketConfig}};
79//! use std::error;
80//!
81//! async fn receive_and_decode() -> Result<(), Box<dyn error::Error>> {
82//!     let s = IcmpSocket::<Icmpv4>::new(SocketConfig::default())?;
83//!     let mut buf = vec![0; 10_000];
84//!     let (msg, _range) = s.recv(&mut buf).await?;
85//!     // decode basic ICMP structure, or use raw bytes in `msg` as needed
86//!     let decoded = DecodedIcmpMsg::decode(msg)?;
87//!     if decoded.msg_type() == IcmpV4MsgType::SourceQuench as u8 {
88//!         // decode body as source quench, if that's the msg type you want
89//!     }
90//!     // ... handle other msg types
91//!     Ok(())
92//! }
93//!
94//! ```
95
96#![deny(unsafe_code, missing_docs)]
97
98use std::{fmt, io, net, ops};
99
100pub mod message;
101pub mod socket;
102
103pub mod platform;
104
105pub mod ping;
106
107mod sealed {
108    pub trait Sealed {}
109}
110
111/// Trait abstracting over ICMPv4 and ICMPv6 behavior
112pub trait IcmpVersion: sealed::Sealed + fmt::Debug + Send + Sync + 'static {
113    /// The type of IP address used with this version
114    type Address: Into<net::IpAddr> + Copy + Send + Sync;
115    /// The type of `sockaddr` used with this version
116    type SocketAddr: Into<net::SocketAddr> + PartialEq + Eq + fmt::Debug + Copy;
117    /// The socket domain to use when opening a socket
118    const DOMAIN: socket2::Domain;
119    /// The socket protocol to use when opening a socket
120    const PROTOCOL: socket2::Protocol;
121    /// The sockaddr to bind to if none is specified in [`socket::SocketConfig`].
122    ///
123    /// This should be `0.0.0.0` or `::` with a `0` port so that the kernel chooses a free port.
124    ///
125    /// There must always be _some_ sockaddr bound during socket init, otherwise the local port
126    /// is not set until the first packet is sent.
127    const DEFAULT_BIND: Self::SocketAddr;
128
129    /// Extract the portion of the packet that comprises the ICMP message
130    fn extract_icmp_from_recv_packet(packet: &[u8]) -> io::Result<(&[u8], ops::Range<usize>)>;
131
132    /// Whether the ICMP checksum should be calculated when sending a message.
133    fn checksum_required() -> bool;
134}
135
136/// ICMPv4 marker impl of [`IcmpVersion`].
137///
138/// See [RFC 792](https://www.rfc-editor.org/rfc/rfc792).
139#[derive(Debug)]
140pub struct Icmpv4;
141impl sealed::Sealed for Icmpv4 {}
142impl IcmpVersion for Icmpv4 {
143    type Address = net::Ipv4Addr;
144    type SocketAddr = net::SocketAddrV4;
145
146    const DOMAIN: socket2::Domain = socket2::Domain::IPV4;
147    const PROTOCOL: socket2::Protocol = socket2::Protocol::ICMPV4;
148    const DEFAULT_BIND: Self::SocketAddr = net::SocketAddrV4::new(net::Ipv4Addr::UNSPECIFIED, 0);
149
150    fn extract_icmp_from_recv_packet(packet: &[u8]) -> io::Result<(&[u8], ops::Range<usize>)> {
151        if platform::ipv4_recv_prefix_ipv4_header() {
152            socket::strip_ipv4_header(packet).map_err(|_e| {
153                io::Error::new(io::ErrorKind::InvalidData, "Could not strip IPv4 header")
154            })
155        } else {
156            Ok((packet, 0..packet.len()))
157        }
158    }
159
160    fn checksum_required() -> bool {
161        platform::ipv4_send_checksum_required()
162    }
163}
164
165/// ICMPv6 marker impl of [`IcmpVersion`].
166///
167/// See [RFC 4443](https://www.rfc-editor.org/rfc/rfc4443),
168#[derive(Debug)]
169pub struct Icmpv6;
170impl sealed::Sealed for Icmpv6 {}
171impl IcmpVersion for Icmpv6 {
172    type Address = net::Ipv6Addr;
173    type SocketAddr = net::SocketAddrV6;
174
175    const DOMAIN: socket2::Domain = socket2::Domain::IPV6;
176    const PROTOCOL: socket2::Protocol = socket2::Protocol::ICMPV6;
177    const DEFAULT_BIND: Self::SocketAddr =
178        net::SocketAddrV6::new(net::Ipv6Addr::UNSPECIFIED, 0, 0, 0);
179
180    fn extract_icmp_from_recv_packet(packet: &[u8]) -> io::Result<(&[u8], ops::Range<usize>)> {
181        // No extra headers are provided on received ICMPv6 messages
182        Ok((packet, 0..packet.len()))
183    }
184
185    fn checksum_required() -> bool {
186        // macOS and Linux both do their own ICMPv6 checksums
187        false
188    }
189}
190
191/// The IP version used by a socket or IP address.
192///
193/// Used for selecting between IP versions at runtime, where suitable.
194#[allow(missing_docs)]
195#[derive(Debug, Eq, PartialEq, Clone, Copy)]
196pub enum IpVersion {
197    V4,
198    V6,
199}
200
201impl fmt::Display for IpVersion {
202    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
203        match self {
204            IpVersion::V4 => write!(f, "IPv4"),
205            IpVersion::V6 => write!(f, "IPv6"),
206        }
207    }
208}
209
210impl From<net::IpAddr> for IpVersion {
211    fn from(value: net::IpAddr) -> Self {
212        (&value).into()
213    }
214}
215impl From<&net::IpAddr> for IpVersion {
216    fn from(value: &net::IpAddr) -> Self {
217        match value {
218            net::IpAddr::V4(_) => Self::V4,
219            net::IpAddr::V6(_) => Self::V6,
220        }
221    }
222}
223impl From<net::Ipv4Addr> for IpVersion {
224    fn from(_value: net::Ipv4Addr) -> Self {
225        Self::V4
226    }
227}
228impl From<&net::Ipv4Addr> for IpVersion {
229    fn from(_value: &net::Ipv4Addr) -> Self {
230        Self::V4
231    }
232}
233impl From<net::Ipv6Addr> for IpVersion {
234    fn from(_value: net::Ipv6Addr) -> Self {
235        Self::V6
236    }
237}
238impl From<&net::Ipv6Addr> for IpVersion {
239    fn from(_value: &net::Ipv6Addr) -> Self {
240        Self::V6
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247
248    #[test]
249    fn ip_addr_into_ip_version() {
250        let ip_addr = net::IpAddr::V4(net::Ipv4Addr::LOCALHOST);
251        assert_eq!(IpVersion::V4, (&ip_addr).into());
252        assert_eq!(IpVersion::V4, ip_addr.into());
253    }
254
255    #[test]
256    fn ipv4_addr_into_ip_version() {
257        let ip_addr = net::Ipv4Addr::LOCALHOST;
258        assert_eq!(IpVersion::V4, (&ip_addr).into());
259        assert_eq!(IpVersion::V4, ip_addr.into());
260    }
261
262    #[test]
263    fn ipv6_addr_into_ip_version() {
264        let ip_addr = net::Ipv6Addr::LOCALHOST;
265        assert_eq!(IpVersion::V6, (&ip_addr).into());
266        assert_eq!(IpVersion::V6, ip_addr.into());
267    }
268}