async-icmp 0.2.1

Async ICMP library
Documentation
/// Try to figure out why macOS tends to drop packets when pings are sent quickly, and especially
/// for IPv6.
///
/// On macOS, this works reliably at all pause times for `127.0.0.1`, but starts choking on `::1`
/// at around 560us. On Linux, both work perfectly.
use async_icmp::{
    message::{
        decode::DecodedIcmpMsg,
        echo::{parse_echo_reply, EchoSeq, IcmpEchoRequest},
        IcmpV4MsgType, IcmpV6MsgType,
    },
    socket::{SocketConfig, SocketPair},
    IpVersion,
};
use clap::Parser as _;
use log::{info, warn};
use owo_colors::OwoColorize;
use std::{collections, net, sync::Arc, time};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("INFO"))
        .format_timestamp_millis()
        .init();

    let cli = Cli::parse();

    let count = 1000_u16;
    info!("Count = {count}");

    for pause_time in [
        time::Duration::from_millis(10),
        time::Duration::from_millis(5),
        time::Duration::from_millis(2),
        time::Duration::from_millis(1),
        time::Duration::from_micros(500),
        time::Duration::from_micros(200),
        time::Duration::from_micros(100),
        time::Duration::from_micros(50),
        time::Duration::from_micros(20),
        time::Duration::from_micros(10),
        time::Duration::from_micros(1),
    ] {
        info!("Pause time: {}", humantime::Duration::from(pause_time));

        let socket_pair = Arc::new(SocketPair::new(
            SocketConfig::default(),
            SocketConfig::default(),
        )?);

        let ip_version = cli.dest.into();
        let reply_msg_type = match ip_version {
            IpVersion::V4 => IcmpV4MsgType::EchoReply as u8,
            IpVersion::V6 => IcmpV6MsgType::EchoReply as u8,
        };

        let orig_id = socket_pair
            .platform_echo_id(ip_version)
            .unwrap_or_else(rand::random);
        let orig_data = rand::random::<[u8; 32]>().to_vec();

        let mut req = IcmpEchoRequest::from_fields(orig_id, EchoSeq::from_be(0), &orig_data);

        let rx_socket = socket_pair.clone();
        let receiver = tokio::spawn(async move {
            let mut received = collections::HashSet::new();
            let mut buf = vec![0; 1_000];

            while received.len() < count.into() {
                match tokio::time::timeout(
                    // longer than any plausible ping response
                    time::Duration::from_secs(1),
                    rx_socket.recv_either(ip_version, &mut buf),
                )
                .await
                {
                    Ok(res) => match res {
                        Ok((msg, _range)) => DecodedIcmpMsg::decode(msg)
                            .ok()
                            .iter()
                            .filter(|decoded| {
                                decoded.msg_type() == reply_msg_type && decoded.msg_code() == 0
                            })
                            .filter_map(|decoded| parse_echo_reply(decoded.body()))
                            .filter(|(id, _seq, data)| id == &orig_id && data == &orig_data)
                            .for_each(|(_id, seq, _data)| {
                                received.insert(seq);
                            }),
                        Err(e) => {
                            warn!("Read error: {e}");
                            return received;
                        }
                    },
                    Err(_) => {
                        warn!("Read timeout");
                        return received;
                    }
                };
            }

            received
        });

        for seq in (0..count).map(EchoSeq::from_be) {
            req.set_seq(seq);
            socket_pair.send_to_either(&mut req, cli.dest).await?;
            tokio::time::sleep(pause_time).await;
        }

        let received_seqs = receiver.await?;
        let loss = usize::from(count) - received_seqs.len();
        info!(
            "Loss: {}",
            if loss > 0 {
                format!(
                    "{} ({:.2})%",
                    loss,
                    (1.0 - received_seqs.len() as f64 / (count as f64)) * 100.0
                )
                .yellow()
                .to_string()
            } else {
                "0".green().to_string()
            }
        );
    }

    Ok(())
}

#[derive(clap::Parser)]
struct Cli {
    /// Ip address to ping
    dest: net::IpAddr,
}