async_icmp/socket/
mod.rs

1//! ICMP socket support.
2//!
3//! Sockets have an ICMP version as a type parameter, allowing precise types for
4//! IP address, etc.
5//!
6//! If the use case demands runtime selection of IP versions ala [`net::IpAddr`], where the
7//! version is determined at runtime, see [`SocketPair`].
8
9use crate::{
10    message::{echo::EchoId, EncodeIcmpMessage},
11    platform, IcmpVersion,
12};
13use std::{io, io::Read as _, marker, net, ops, os::fd};
14use tokio::io::unix;
15use winnow::{binary, combinator, Parser as _};
16
17mod pair;
18
19pub use pair::SocketPair;
20
21#[cfg(test)]
22mod tests;
23
24/// An ICMP socket.
25///
26/// Commonly this would be wrapped in an `Arc` so that it may be used from multiple tasks (e.g.
27/// one for sending, one for receiving).
28///
29/// # Platform differences
30///
31/// On Linux, ICMP Echo Request messages are rewritten to use the local port as the id, and only
32/// ICMP Echo Reply messages where the id = the local port will be returned from `recv()`.
33/// [`IcmpSocket::local_port`] and [`IcmpSocket::platform_echo_id`] exist for such use cases.
34/// See [`platform::icmp_send_overwrite_echo_id_with_local_port`].
35///
36/// On macOS, the kernel is less restrictive, so you can set whatever id you like. In addition,
37/// other ICMP packets will also be returned from `recv()`, so additional filtering may be needed
38/// depending on the use case.
39#[derive(Debug)]
40pub struct IcmpSocket<V> {
41    fd: unix::AsyncFd<IcmpSocketInner<V>>,
42    local_port: u16,
43}
44
45impl<V: IcmpVersion> IcmpSocket<V> {
46    /// Create a new socket for IP version `V`.
47    ///
48    /// When a socket is created, it's either IPv4 (socket type `AF_INET`) or IPv6 (`AF_INET6`), which
49    /// governs which type of IP address is valid to use with [`IcmpSocket::send_to`]
50    /// ([`net::Ipv4Addr`] or [`net::Ipv6Addr`]).
51    pub fn new(config: SocketConfig<V>) -> io::Result<Self> {
52        let fd = unix::AsyncFd::new(IcmpSocketInner::new(config)?)?;
53        let local_port = fd
54            .get_ref()
55            .socket
56            .local_addr()?
57            .as_socket()
58            .map(|sa| sa.port())
59            .ok_or_else(|| {
60                io::Error::new(io::ErrorKind::Other, "Socket is not AF_INET or AF_INET6?")
61            })?;
62        Ok(Self { fd, local_port })
63    }
64
65    /// Write the contents of a received ICMP message into `buf`, returning a tuple containing the
66    /// ICMP message and the range of indices in `buf` holding the message, in case mutable access
67    /// to the slice is desired.
68    ///
69    /// Bytes outside the returned `range` may have been written to, and skipped during subsequent
70    /// parsing.
71    ///
72    /// See [`crate::message::decode::DecodedIcmpMsg`] to extract basic ICMP message structure.
73    pub async fn recv<'a>(&self, buf: &'a mut [u8]) -> io::Result<(&'a [u8], ops::Range<usize>)> {
74        self.fd
75            .async_io(tokio::io::Interest::READABLE, |inner| {
76                // the read() impl is a simple wrapper around recv(2)
77                (&inner.socket).read(buf)
78            })
79            .await
80            .and_then(|len| V::extract_icmp_from_recv_packet(&buf[..len]))
81    }
82
83    /// Send `msg` to `addr`.
84    ///
85    /// If `msg` doesn't support the socket's IP version, an error will be returned.
86    pub async fn send_to(
87        &self,
88        msg: &mut impl EncodeIcmpMessage<V>,
89        addr: V::Address,
90    ) -> io::Result<()> {
91        self.fd
92            .async_io(tokio::io::Interest::WRITABLE, |inner| {
93                let buffer = msg.encode();
94
95                if V::checksum_required() {
96                    buffer.calculate_icmpv4_checksum();
97                }
98
99                // port is not used
100                let socket_addr = net::SocketAddr::new(addr.into(), 0);
101                inner.socket.send_to(buffer.as_slice(), &socket_addr.into())
102            })
103            .await
104            .map(|_| ())
105    }
106
107    /// Returns the local port of the socket.
108    ///
109    /// This is useful on Linux since the local port is used as the ICMP Echo ID regardless of what
110    /// is set in userspace.
111    ///
112    /// On macOS, the local port is always zero, but ICMP Echo ids are not tied to the local port,
113    /// so it's not an issue in practice.
114    ///
115    /// See [`platform::icmp_send_overwrite_echo_id_with_local_port`].
116    pub fn local_port(&self) -> u16 {
117        self.local_port
118    }
119
120    /// Returns the local port of the socket as the `id` to be used in an ICMP Echo Request message,
121    /// if the current platform is one that forces the id to match the local port.
122    ///
123    /// See [`platform::icmp_send_overwrite_echo_id_with_local_port`].
124    ///
125    /// # Examples
126    ///
127    /// Use the platform echo id, otherwise a random id.
128    /// ```
129    /// use async_icmp::{IcmpVersion, message::echo::EchoId, socket::IcmpSocket};
130    /// use std::io;
131    ///
132    /// fn echo_id<V: IcmpVersion>(socket: &IcmpSocket<V>) -> EchoId {
133    ///     socket.platform_echo_id().unwrap_or_else(rand::random)
134    /// }
135    /// ```
136    pub fn platform_echo_id(&self) -> Option<EchoId> {
137        if platform::icmp_send_overwrite_echo_id_with_local_port() {
138            Some(EchoId::from_be(self.local_port()))
139        } else {
140            None
141        }
142    }
143}
144
145/// Config for creating sockets.
146///
147/// Most use cases can use `SocketConfig::default()`.
148///
149/// To avoid compatibility concerns when more fields are added, use the `..` struct update syntax
150/// so that any new fields will be conveniently defaulted in existing invocations:
151///
152/// ```
153/// use std::net;
154/// use async_icmp::{Icmpv4, socket::SocketConfig};
155///
156/// let config: SocketConfig<Icmpv4> = SocketConfig {
157///     bind_to: Some(net::SocketAddrV4::new(net::Ipv4Addr::LOCALHOST, 1234)),
158///     ..SocketConfig::default()
159/// };
160/// ```
161#[derive(Debug, Clone)]
162pub struct SocketConfig<V: IcmpVersion> {
163    /// The sockaddr to bind the socket to. If specified with `Some`, the socket is always bound to
164    /// the address.
165    ///
166    /// If not specified, the behavior depends on the platform. On all supported platforms, a
167    /// socket's initial state is bound to the suitable `undefined` address (`0.0.0.0:0` or `:::0`).
168    ///
169    /// On Linux, explicitly binding that address causes the kernel to select a local port, which is
170    /// useful for ICMP Echo messages since Linux forces the echo id to be the local port.
171    ///
172    /// On macOS, binding that address makes no difference: an ICMP socket always has zero local
173    /// port, so the bind is not performed.
174    pub bind_to: Option<V::SocketAddr>,
175}
176
177impl<V: IcmpVersion> Default for SocketConfig<V> {
178    fn default() -> Self {
179        Self { bind_to: None }
180    }
181}
182
183/// A non-public type for the necessary impls to make AsyncFd work
184#[derive(Debug)]
185struct IcmpSocketInner<V> {
186    socket: socket2::Socket,
187    marker: marker::PhantomData<V>,
188}
189
190impl<V: IcmpVersion> IcmpSocketInner<V> {
191    fn new(config: SocketConfig<V>) -> io::Result<Self> {
192        let socket = socket2::Socket::new(V::DOMAIN, socket2::Type::DGRAM, Some(V::PROTOCOL))?;
193        socket.set_nonblocking(true)?;
194
195        // Sockets start bound to addr=undefined, port=0 according to local_addr on a fresh socket.
196        // By specifically binding to that same thing again, it forces the kernel to choose
197        // a local port, so it won't magically appear later.
198        match config.bind_to {
199            None => {
200                if platform::socket_bind_sets_nonzero_local_port() {
201                    socket.bind(&V::DEFAULT_BIND.into().into())?
202                }
203            }
204            Some(sockaddr) => socket.bind(&sockaddr.into().into())?,
205        }
206
207        Ok(Self {
208            socket,
209            marker: marker::PhantomData,
210        })
211    }
212}
213
214/// Required by [unix::AsyncFd]
215impl<V> fd::AsRawFd for IcmpSocketInner<V> {
216    fn as_raw_fd(&self) -> fd::RawFd {
217        self.socket.as_raw_fd()
218    }
219}
220
221// only used on macOS
222pub(crate) type WinnowError<'a, C> =
223    winnow::error::ParseError<winnow::Located<&'a [u8]>, winnow::error::ContextError<C>>;
224
225/// Returns a result with a tuple of `(data after the ipv4 header, index range of the data)`.
226///
227/// The index range is useful if the caller wants to treat the data as a `&mut [u8]`.
228// only used on macOS
229pub(crate) fn strip_ipv4_header(
230    input: &[u8],
231) -> Result<(&[u8], ops::Range<usize>), WinnowError<&'static str>> {
232    // discard complete ip header
233    combinator::preceded(
234        binary::bits::bits(
235            // get and take ipv4 header len
236            binary::length_take(
237                // verify and discard ip version, yielding just the header length
238                combinator::preceded(
239                    // 4 bit version
240                    binary::bits::pattern::<_, _, _, winnow::error::ContextError<&'static str>>(
241                        0x04_u8, 4_usize,
242                    )
243                    .context("Invalid version"),
244                    // 4 bit length in 32-bit words
245                    binary::bits::take(4_usize)
246                        // length includes the byte we just parsed
247                        .verify_map(|len: usize| {
248                            len.checked_mul(32).and_then(|prod| prod.checked_sub(8))
249                        }),
250                ),
251            ),
252        ),
253        combinator::rest::<_, winnow::error::ContextError<_>>.with_span(),
254    )
255    .parse(winnow::Located::new(input))
256}