ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
//! `TPACKET` ring definitions, alignment math, and ring-layout validation.
//!
//! The wire-level structs and most constants come from [`libc`]; this module adds the
//! `TPACKET_V*` version selectors (which `libc` does not expose), the `TPACKET_ALIGN` helper,
//! and a validated [`RingLayout`] that turns human-friendly sizes into the
//! [`libc::tpacket_req3`] / [`libc::tpacket_req`] structures the kernel expects.
//!
//! All logic here is pure and unit-tested without privileges; the actual `setsockopt`/`mmap`
//! calls live in [`super::ring`].

use std::ffi::c_int;

use crate::error::{Error, Result};

/// `PACKET_VERSION` selector for the v2 ring format (used here for the TX ring).
pub const TPACKET_V2: c_int = 1;
/// `PACKET_VERSION` selector for the v3 ring format (used here for the RX ring).
pub const TPACKET_V3: c_int = 2;

/// Rounds `x` up to the next [`libc::TPACKET_ALIGNMENT`] (16-byte) boundary.
#[must_use]
pub const fn tpacket_align(x: usize) -> usize {
    (x + libc::TPACKET_ALIGNMENT - 1) & !(libc::TPACKET_ALIGNMENT - 1)
}

/// The smallest sensible frame size: the aligned v3 header plus room for at least a
/// minimum-size Ethernet frame. Callers normally want this much larger (MTU-sized).
#[must_use]
pub const fn min_frame_size() -> usize {
    // 64 bytes covers a minimum-length Ethernet frame; the header is accounted for separately.
    tpacket_align(libc::TPACKET3_HDRLEN) + tpacket_align(64)
}

/// A validated `PACKET_RX_RING`/`PACKET_TX_RING` geometry.
///
/// The kernel imposes several constraints on ring geometry; [`RingLayout::new`] checks them up
/// front so misconfiguration surfaces as a clear [`Error::InvalidConfig`] rather than an opaque
/// `EINVAL` from `setsockopt`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct RingLayout {
    /// Size of each block in bytes (a multiple of the page size and of `frame_size`).
    pub block_size: u32,
    /// Number of blocks in the ring.
    pub block_count: u32,
    /// Size of each frame slot in bytes (a multiple of `TPACKET_ALIGNMENT`).
    pub frame_size: u32,
    /// Total number of frame slots: `frames_per_block * block_count`.
    pub frame_count: u32,
    /// v3 block-retire timeout in milliseconds (ignored for the v2 TX ring).
    pub retire_blk_tov_ms: u32,
}

impl RingLayout {
    /// Builds and validates a ring layout.
    ///
    /// * `block_size` must be a non-zero multiple of `page_size` and of `frame_size`.
    /// * `frame_size` must be a multiple of `TPACKET_ALIGNMENT` and at least [`min_frame_size`].
    /// * `block_count` must be non-zero.
    pub fn new(
        block_size: u32,
        block_count: u32,
        frame_size: u32,
        retire_blk_tov_ms: u32,
        page_size: u32,
    ) -> Result<Self> {
        if block_count == 0 {
            return Err(Error::invalid_config("block_count must be non-zero"));
        }
        if page_size == 0 {
            return Err(Error::invalid_config("page_size must be non-zero"));
        }
        if frame_size == 0 || (frame_size as usize) % libc::TPACKET_ALIGNMENT != 0 {
            return Err(Error::invalid_config(
                "frame_size must be a non-zero multiple of TPACKET_ALIGNMENT (16)",
            ));
        }
        if (frame_size as usize) < min_frame_size() {
            return Err(Error::invalid_config("frame_size is smaller than min_frame_size()"));
        }
        if block_size == 0 || block_size % page_size != 0 {
            return Err(Error::invalid_config("block_size must be a multiple of the page size"));
        }
        if block_size % frame_size != 0 {
            return Err(Error::invalid_config("block_size must be a multiple of frame_size"));
        }

        let frames_per_block = block_size / frame_size;
        let frame_count = frames_per_block
            .checked_mul(block_count)
            .ok_or(Error::invalid_config("ring geometry overflows u32"))?;

        // Guard against absurd allocations overflowing usize on the mmap.
        (block_size as usize)
            .checked_mul(block_count as usize)
            .ok_or(Error::invalid_config("ring size overflows usize"))?;

        Ok(Self { block_size, block_count, frame_size, frame_count, retire_blk_tov_ms })
    }

    /// Total mapped length of the ring in bytes.
    #[must_use]
    pub const fn map_len(&self) -> usize {
        self.block_size as usize * self.block_count as usize
    }

    /// Renders the layout as a [`libc::tpacket_req3`] for a v3 (RX) ring.
    #[must_use]
    pub fn to_req3(self) -> libc::tpacket_req3 {
        libc::tpacket_req3 {
            tp_block_size: self.block_size,
            tp_block_nr: self.block_count,
            tp_frame_size: self.frame_size,
            tp_frame_nr: self.frame_count,
            tp_retire_blk_tov: self.retire_blk_tov_ms,
            tp_sizeof_priv: 0,
            tp_feature_req_word: 0,
        }
    }

    /// Renders the layout as a [`libc::tpacket_req`] for a v1/v2 (TX) ring.
    #[must_use]
    pub fn to_req(self) -> libc::tpacket_req {
        libc::tpacket_req {
            tp_block_size: self.block_size,
            tp_block_nr: self.block_count,
            tp_frame_size: self.frame_size,
            tp_frame_nr: self.frame_count,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    const PAGE: u32 = 4096;

    #[test]
    fn align_rounds_up_to_16() {
        assert_eq!(tpacket_align(0), 0);
        assert_eq!(tpacket_align(1), 16);
        assert_eq!(tpacket_align(16), 16);
        assert_eq!(tpacket_align(17), 32);
        assert_eq!(tpacket_align(2048), 2048);
    }

    #[test]
    fn version_constants_match_kernel() {
        assert_eq!(TPACKET_V2, 1);
        assert_eq!(TPACKET_V3, 2);
    }

    #[test]
    fn valid_layout_computes_geometry() {
        let l = RingLayout::new(1 << 20, 8, 2048, 60, PAGE).unwrap();
        let frames_per_block = (1 << 20) / 2048;
        assert_eq!(l.frame_count, frames_per_block * 8);
        assert_eq!(l.map_len(), (1usize << 20) * 8);

        let req = l.to_req3();
        assert_eq!(req.tp_block_size, 1 << 20);
        assert_eq!(req.tp_block_nr, 8);
        assert_eq!(req.tp_frame_size, 2048);
        assert_eq!(req.tp_frame_nr, l.frame_count);
        assert_eq!(req.tp_retire_blk_tov, 60);
    }

    #[test]
    fn rejects_unaligned_frame_size() {
        assert!(RingLayout::new(1 << 20, 8, 2050, 60, PAGE).is_err());
    }

    #[test]
    fn rejects_block_not_multiple_of_page() {
        assert!(RingLayout::new(4097, 8, 2048, 60, PAGE).is_err());
    }

    #[test]
    fn rejects_block_not_multiple_of_frame() {
        // 12288 is a multiple of the page size but not of frame_size 2048? 12288/2048 = 6, so it is.
        // Use a frame size that does not divide the block evenly.
        assert!(RingLayout::new(PAGE, 8, 2048 + 16, 60, PAGE).is_err());
    }

    #[test]
    fn rejects_zero_block_count() {
        assert!(RingLayout::new(1 << 20, 0, 2048, 60, PAGE).is_err());
    }

    #[test]
    fn rejects_tiny_frame_size() {
        assert!(RingLayout::new(1 << 20, 8, 16, 60, PAGE).is_err());
    }

    proptest! {
        /// `RingLayout::new` never panics on any geometry, and every accepted layout satisfies the
        /// kernel's invariants.
        #[test]
        #[cfg_attr(miri, ignore = "proptest is slow under Miri and covers safe arithmetic")]
        fn ring_layout_invariants(
            block_size in 0u32..=1 << 24,
            block_count in 0u32..=128,
            frame_size in 0u32..=1 << 16,
            page in prop_oneof![Just(4096u32), Just(16384u32), Just(65536u32)],
        ) {
            if let Ok(l) = RingLayout::new(block_size, block_count, frame_size, 0, page) {
                prop_assert_eq!(l.block_size % page, 0);
                prop_assert_eq!(l.block_size % l.frame_size, 0);
                prop_assert_eq!(l.frame_count, (l.block_size / l.frame_size) * l.block_count);
                prop_assert_eq!(l.map_len(), l.block_size as usize * l.block_count as usize);
            }
        }
    }
}