ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
//! Platform abstraction boundary for raw datalink channels.
//!
//! `RawChannel` is the seam between `ferranet`'s public API and the OS-specific code that
//! actually talks to the kernel. v0.1 ships a single Linux (`AF_PACKET`) implementation, but the
//! trait is deliberately minimal so additional backends — BSD/macOS BPF, Windows, or a
//! high-performance Linux `AF_XDP` path — can be added later without disturbing callers.

use std::io;

#[cfg(target_os = "linux")]
pub(crate) mod linux;

/// Receive/drop counters for a datalink channel.
///
/// Obtained from [`Receiver::stats`](crate::Receiver::stats), which returns **cumulative** totals
/// since the channel was opened (the kernel's underlying `PACKET_STATISTICS` counter is
/// reset-on-read, so ferranet accumulates it for you).
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct Stats {
    /// Packets delivered to the channel.
    pub received: u64,
    /// Packets the kernel dropped because the ring was full — i.e. data lost.
    pub dropped: u64,
    /// Number of times the v3 receive ring froze (ran out of free blocks); a ring-overflow signal.
    pub freezes: u64,
}

impl Stats {
    /// Returns `true` if any packets have been dropped.
    #[must_use]
    pub const fn has_drops(&self) -> bool {
        self.dropped > 0
    }

    /// Adds a reset-on-read delta into these cumulative totals.
    pub(crate) fn accumulate(&mut self, delta: Stats) {
        self.received = self.received.wrapping_add(delta.received);
        self.dropped = self.dropped.wrapping_add(delta.dropped);
        self.freezes = self.freezes.wrapping_add(delta.freezes);
    }
}

/// The transmit/control half of the platform abstraction boundary.
///
/// A backend implements this for its concrete channel type. Receive is intentionally *not* part
/// of this trait: zero-copy receive borrows from backend-owned memory with a backend-specific
/// lifetime, so it is exposed as inherent methods on each backend instead (see the Linux
/// `RxRing`). This keeps the shared contract small and honest while the receive surface stays
/// zero-copy.
pub(crate) trait RawChannel {
    /// Transmits a single frame, returning the number of bytes accepted by the kernel.
    fn send(&mut self, frame: &[u8]) -> io::Result<usize>;

    /// Transmits a batch of frames, returning the number of frames accepted.
    ///
    /// The default implementation sends frames one at a time; backends with a TX ring override
    /// this to fill many slots before a single flushing syscall.
    fn send_batch(&mut self, frames: &[&[u8]]) -> io::Result<usize> {
        let mut sent = 0;
        for frame in frames {
            match self.send(frame) {
                Ok(_) => sent += 1,
                // Once at least one frame is queued, a full TX buffer is a soft stop, not an error.
                Err(e) if sent > 0 && e.kind() == io::ErrorKind::WouldBlock => break,
                Err(e) => return Err(e),
            }
        }
        Ok(sent)
    }
}

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

    proptest::proptest! {
        /// Folding any sequence of reset-on-read deltas gives the wrapping sum of the deltas —
        /// the contract behind presenting the kernel's reset-on-read counters as monotonic
        /// totals (`Receiver::stats`).
        #[test]
        #[cfg_attr(miri, ignore = "proptest is slow under Miri and covers safe arithmetic")]
        fn stats_accumulate_folds_deltas(
            deltas in proptest::collection::vec(
                (0u64..u64::MAX / 2, 0u64..u64::MAX / 2, 0u64..u64::MAX / 2),
                0..20,
            )
        ) {
            let mut total = Stats::default();
            for &(received, dropped, freezes) in &deltas {
                total.accumulate(Stats { received, dropped, freezes });
            }
            let sum = |f: fn(&(u64, u64, u64)) -> u64| {
                deltas.iter().fold(0u64, |acc, d| acc.wrapping_add(f(d)))
            };
            proptest::prop_assert_eq!(total.received, sum(|d| d.0));
            proptest::prop_assert_eq!(total.dropped, sum(|d| d.1));
            proptest::prop_assert_eq!(total.freezes, sum(|d| d.2));
            proptest::prop_assert_eq!(total.has_drops(), total.dropped > 0);
        }
    }
}