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}