async-icmp 0.2.1

Async ICMP library
Documentation
//! 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());
    }
}