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