async-icmp 0.2.1

Async ICMP library
Documentation
//! Support for ICMP Echo messages.
//!
//! See RFC 792 p14 or RFC 4443 section 4.1.
//!
//! Note that on Linux, the `id` field is overwritten to be the socket's local port when an Echo
//! Request message is sent. See [`crate::platform::icmp_send_overwrite_echo_id_with_local_port`].

use crate::{
    message::{EncodeIcmpMessage, IcmpMessageBuffer, IcmpV4MsgType, IcmpV6MsgType},
    Icmpv4, Icmpv6,
};
#[cfg(feature = "rand")]
use rand::Rng;
use std::fmt;
use winnow::{combinator, token, Parser};

/// Echo message "identifier" field.
///
/// # Examples
///
/// Constructing from a `u16` with big-endian byte ordering:
///
/// ```
/// use async_icmp::message::echo::EchoId;
///
/// let id: EchoId = EchoId::from_be(123);
/// ```
///
/// Generating a random id via the `rand` `Distribution` impl (enabled via the `rand` feature):
///
/// ```
/// use async_icmp::message::echo::EchoId;
///
/// let id: EchoId = rand::random();
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct EchoId([u8; 2]);

impl EchoId {
    /// Construct an id from the big-endian bytes of `num`.
    pub fn from_be(num: u16) -> Self {
        Self(num.to_be_bytes())
    }
    /// Construct an id from the little-endian bytes of `num`.
    pub fn from_le(num: u16) -> Self {
        Self(num.to_le_bytes())
    }

    /// Returns the id as a byte array.
    pub fn as_array(&self) -> [u8; 2] {
        self.0
    }

    /// Returns the id as a length-2 slice.
    pub fn as_slice(&self) -> &[u8] {
        &self.0
    }

    /// Returns the bytes interpreted as big-endian
    pub fn as_be(&self) -> u16 {
        u16::from_be_bytes(self.0)
    }

    /// Returns the bytes interpreted as little-endian
    pub fn as_le(&self) -> u16 {
        u16::from_le_bytes(self.0)
    }
}

impl From<[u8; 2]> for EchoId {
    fn from(value: [u8; 2]) -> Self {
        Self(value)
    }
}

impl fmt::Debug for EchoId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "0x{}", hex::encode_upper(self.0))
    }
}

#[cfg(feature = "rand")]
impl rand::distributions::Distribution<EchoId> for rand::distributions::Standard {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> EchoId {
        EchoId::from_be(rng.gen())
    }
}

/// Echo message "sequence number" field.
///
/// # Examples
///
/// Constructing from a `u16` with big-endian byte ordering:
///
/// ```
/// use async_icmp::message::echo::EchoSeq;
///
/// let seq: EchoSeq = EchoSeq::from_be(456);
/// ```
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct EchoSeq([u8; 2]);

impl EchoSeq {
    /// Construct a seq num from the big-endian bytes of `num`.
    pub fn from_be(num: u16) -> Self {
        Self(num.to_be_bytes())
    }
    /// Construct a seq num from the little-endian bytes of `num`.
    pub fn from_le(num: u16) -> Self {
        Self(num.to_le_bytes())
    }

    /// Returns the sequence number as a byte array.
    pub fn as_array(&self) -> [u8; 2] {
        self.0
    }

    /// Returns the sequence number as a length-2 slice.
    pub fn as_slice(&self) -> &[u8] {
        &self.0
    }

    /// Returns the bytes interpreted as big-endian
    pub fn as_be(&self) -> u16 {
        u16::from_be_bytes(self.0)
    }

    /// Returns the bytes interpreted as little-endian
    pub fn as_le(&self) -> u16 {
        u16::from_le_bytes(self.0)
    }
}

impl From<[u8; 2]> for EchoSeq {
    fn from(value: [u8; 2]) -> Self {
        Self(value)
    }
}

impl fmt::Debug for EchoSeq {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "0x{}", hex::encode_upper(self.0))
    }
}

/// An ICMP "Echo request" message, suitable for use with [`crate::socket::IcmpSocket::send_to`].
///
/// This can be re-used for different requests, if needed.
///
/// Sending this ICMP message should result in an "Echo reply" message.
///
/// This message supports both IPv4 and IPv6.
///
/// See RFC 792 and RFC 4443.
#[derive(Clone, Debug)]
pub struct IcmpEchoRequest {
    /// Echo bytes (Following ICMP header):
    ///
    /// - 0-1: id
    /// - 2-3: seq
    /// - 4+ data
    ///
    /// Buffer body is always at least [`Self::ICMP_ECHO_HDR_LEN`] bytes.
    buf: IcmpMessageBuffer,
}

impl IcmpEchoRequest {
    /// Fortunately, both IPv4 and IPv6 Echo use an 4-byte header followed by arbitrary message
    /// data.
    ///
    /// The ICMP message type is set at encode time. Other fields are set with individual setters.
    const ICMP_ECHO_HDR_LEN: usize = 4;

    /// Construct a new ICMP "Echo request" message with `id` and `seq` set to 0, and empty `data`.
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        Self {
            // Type will be filled in at encode time.
            // Code is always 0.
            // Body holds space for id and seq.
            buf: IcmpMessageBuffer::new(0, 0, &[0; Self::ICMP_ECHO_HDR_LEN]),
        }
    }

    /// Construct a new request with the specified `id`, `seq`, and `data`.
    ///
    /// `data` is arbitrary data expected to be returned in the Echo reply.
    ///
    /// Some hosts seem to append some zero bytes in the reply.
    pub fn from_fields(id: EchoId, seq: EchoSeq, data: &[u8]) -> Self {
        let mut req = Self::new();
        req.set_id(id);
        req.set_seq(seq);
        req.set_data(data);

        req
    }

    /// The id of the echo request
    pub fn id(&self) -> EchoId {
        <[u8; 2]>::try_from(&self.buf.body()[..2])
            .map(EchoId::from)
            .unwrap()
    }

    /// The seq num of the echo request
    pub fn seq(&self) -> EchoSeq {
        <[u8; 2]>::try_from(&self.buf.body()[2..4])
            .map(EchoSeq::from)
            .unwrap()
    }

    /// Set a new id number
    pub fn set_id(&mut self, id: EchoId) {
        self.buf.body_mut()[..2].copy_from_slice(id.as_slice());
    }

    /// Set a new sequence number
    pub fn set_seq(&mut self, seq: EchoSeq) {
        self.buf.body_mut()[2..4].copy_from_slice(seq.as_slice());
    }

    /// Set new data to be echoed
    pub fn set_data(&mut self, data: &[u8]) {
        self.buf.truncate_body(Self::ICMP_ECHO_HDR_LEN);
        self.buf.extend_body(data.iter().cloned());
    }
}

impl EncodeIcmpMessage<Icmpv4> for IcmpEchoRequest {
    fn encode(&mut self) -> &mut IcmpMessageBuffer {
        self.buf.set_type(IcmpV4MsgType::EchoRequest as u8);
        &mut self.buf
    }
}

impl EncodeIcmpMessage<Icmpv6> for IcmpEchoRequest {
    fn encode(&mut self) -> &mut IcmpMessageBuffer {
        self.buf.set_type(IcmpV6MsgType::EchoRequest as u8);
        &mut self.buf
    }
}

/// Parse the body of an echo reply message into (`ident`, `seq`, `data`).
///
/// Since ICMPv4 and ICMPv6 share the same Echo Reply format, this works for either.
///
/// This should only be used on ICMP message bodies of the appropriate type
/// ([`IcmpV4MsgType::EchoReply`] or [`IcmpV6MsgType::EchoReply`], as appropiriate) and code (`0`)
/// as per the relevant RFCs.
pub fn parse_echo_reply(icmp_reply_body: &[u8]) -> Option<(EchoId, EchoSeq, &[u8])> {
    (
        token::take::<_, _, winnow::error::ContextError>(2_usize)
            .try_map(|slice| <[u8; 2]>::try_from(slice).map(EchoId::from)),
        token::take(2_usize).try_map(|slice| <[u8; 2]>::try_from(slice).map(EchoSeq::from)),
        combinator::rest,
    )
        .parse(icmp_reply_body)
        .ok()
}