Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@
[![Rustc Version 1.85.0+](https://img.shields.io/badge/rustc-1.85.0+-lightgray.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0/)
[![CI](https://github.com/M4SS-Code/massping/actions/workflows/ci.yml/badge.svg)](https://github.com/M4SS-Code/massping/actions/workflows/ci.yml)

Asynchronous ICMP ping library using Linux RAW sockets and the
Asynchronous ICMP ping library using Linux DGRAM sockets and the
tokio runtime.

This crate uses `SOCK_DGRAM` sockets with `IPPROTO_ICMP`/`IPPROTO_ICMPV6`
("ping sockets"), which allows sending ICMP echo requests without root
privileges on Linux.

## Features

* `stream`: implements `Stream` for `MeasureManyStream`.
Expand Down
19 changes: 17 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ pub mod raw_pinger;
mod socket;

/// A pinger for both [`Ipv4Addr`] and [`Ipv6Addr`] addresses.
///
/// Cloning is cheap: clones share the same sockets and background
/// receive tasks, which shut down when the last clone is dropped.
#[derive(Clone)]
pub struct DualstackPinger {
v4: V4Pinger,
v6: V6Pinger,
Expand All @@ -57,9 +61,14 @@ impl DualstackPinger {
/// Construct a new `DualstackPinger`.
///
/// For maximum efficiency the same instance of `DualstackPinger` should
/// be used for as long as possible, altough it might also
/// be used for as long as possible, although it might also
/// be beneficial to `Drop` the `DualstackPinger` and recreate it if
/// you are not going to be sending pings for a long period of time.
///
/// # Panics
///
/// Panics if called from outside a tokio runtime, as it spawns
/// background receive tasks.
pub fn new() -> io::Result<Self> {
let v4 = V4Pinger::new()?;
let v6 = V6Pinger::new()?;
Expand All @@ -71,6 +80,10 @@ impl DualstackPinger {
/// Creates [`DualstackMeasureManyStream`] which **lazily** sends ping
/// requests and [`Stream`]s the responses as they arrive.
///
/// # Panics
///
/// See [`Pinger::measure_many`].
///
/// [`Stream`]: futures_core::Stream
pub fn measure_many<I>(&self, addresses: I) -> DualstackMeasureManyStream<'_, I>
where
Expand Down Expand Up @@ -100,10 +113,12 @@ impl DualstackPinger {
/// like [`tokio::time::timeout`] should be used to prevent the program
/// from hanging indefinitely.
///
/// Leaking this method might crate a slowly forever growing memory leak.
/// Leaking this stream may create a memory leak that lasts until the
/// [`DualstackPinger`] is dropped.
///
/// [`Stream`]: futures_core::Stream
/// [`tokio::time::timeout`]: tokio::time::timeout
#[must_use = "streams do nothing unless polled"]
pub struct DualstackMeasureManyStream<'a, I: Iterator<Item = IpAddr>> {
v4: MeasureManyStream<'a, Ipv4Addr, FilterIpAddr<I, Ipv4Addr>>,
v6: MeasureManyStream<'a, Ipv6Addr, FilterIpAddr<I, Ipv6Addr>>,
Expand Down
79 changes: 77 additions & 2 deletions src/packet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ pub struct EchoReplyPacket<V: IpVersion> {

impl<V: IpVersion> EchoRequestPacket<V> {
/// Build a new ICMP echo request packet
///
/// Note that when the packet is sent through a Linux `SOCK_DGRAM` ICMP
/// socket ("ping socket"), as done by this crate, the kernel overwrites
/// `identifier` with the socket's own identifier and recomputes the
/// checksum. The kernel also delivers only the echo replies whose
/// identifier matches the socket, so replies don't need to be checked
/// against the value given here.
pub fn new(identifier: u16, sequence_number: u16, payload: &[u8]) -> Self {
let mut buf = BytesMut::zeroed(ICMP_HEADER_LEN + payload.len());

Expand Down Expand Up @@ -66,6 +73,11 @@ impl<V: IpVersion> EchoRequestPacket<V> {
pub(crate) fn as_bytes(&self) -> &[u8] {
&self.buf
}

/// Get the payload of this echo request.
pub(crate) fn payload(&self) -> Bytes {
self.buf.slice(ICMP_HEADER_LEN..)
}
}

impl<V: IpVersion> EchoReplyPacket<V> {
Expand Down Expand Up @@ -103,6 +115,9 @@ impl<V: IpVersion> EchoReplyPacket<V> {
}

/// Get the ICMP packet identifier
///
/// On Linux ping sockets this is the kernel-assigned identifier of the
/// receiving socket, not the value passed to [`EchoRequestPacket::new`].
pub fn identifier(&self) -> u16 {
self.identifier
}
Expand Down Expand Up @@ -141,11 +156,71 @@ fn internet_checksum(data: &[u8]) -> u16 {

#[cfg(test)]
mod tests {
use std::net::Ipv4Addr;
use std::net::{Ipv4Addr, Ipv6Addr};

use bytes::Bytes;

use super::EchoReplyPacket;
use super::{EchoReplyPacket, EchoRequestPacket, internet_checksum};

/// Well-known example from the "Internet checksum" Wikipedia article:
/// an IPv4 header (checksum field zeroed) whose checksum is 0xB861.
#[test]
fn internet_checksum_reference_vector() {
let ip_header = [
0x45, 0x00, 0x00, 0x73, 0x00, 0x00, 0x40, 0x00, 0x40, 0x11, 0x00, 0x00, 0xc0, 0xa8,
0x00, 0x01, 0xc0, 0xa8, 0x00, 0xc7,
];
assert_eq!(internet_checksum(&ip_header), 0xb861);
}

#[test]
fn internet_checksum_empty() {
assert_eq!(internet_checksum(&[]), 0xffff);
}

/// The end-around carry must be folded back into the sum.
#[test]
fn internet_checksum_folds_carry() {
assert_eq!(internet_checksum(&[0xff; 8]), 0x0000);
}

/// The trailing byte of odd-length data is padded with a zero byte,
/// i.e. it forms the high-order byte of the last 16-bit word.
#[test]
fn internet_checksum_odd_length() {
assert_eq!(internet_checksum(&[0x01, 0x02, 0x03]), !(0x0102 + 0x0300));
}

#[test]
fn echo_request_packet_v4_layout() {
let packet = EchoRequestPacket::<Ipv4Addr>::new(0x1234, 0x5678, b"test");
let buf = packet.as_bytes();

assert_eq!(buf.len(), 12);
assert_eq!(buf[0], 8, "ICMPv4 echo request type");
assert_eq!(buf[1], 0, "code");
assert_eq!(buf[2..4], 0xa779u16.to_be_bytes(), "checksum");
assert_eq!(buf[4..6], 0x1234u16.to_be_bytes(), "identifier");
assert_eq!(buf[6..8], 0x5678u16.to_be_bytes(), "sequence number");
assert_eq!(&buf[8..], b"test");

// A packet whose checksum field is correct sums to zero.
assert_eq!(internet_checksum(buf), 0);
assert_eq!(&packet.payload()[..], b"test");
}

#[test]
fn echo_request_packet_v6_uses_icmpv6_type() {
// Odd-length payload to also exercise the checksum padding.
let packet = EchoRequestPacket::<Ipv6Addr>::new(1, 2, b"abc");
let buf = packet.as_bytes();

assert_eq!(buf[0], 128, "ICMPv6 echo request type");
// The kernel recomputes the ICMPv6 checksum (it includes a
// pseudo-header userspace can't know), but the packet must still be
// self-consistent under the plain internet checksum.
assert_eq!(internet_checksum(buf), 0);
}

#[test]
fn from_reply_rejects_truncated_packet() {
Expand Down
Loading