ferranet 0.1.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
//! The `AF_PACKET` socket: creation, binding, basic send/receive, and option helpers.
//!
//! [`PacketSocket`] owns the file descriptor (via [`OwnedFd`], so it is closed on drop) and is
//! the foundation both the basic and ring backends build on. It concentrates the raw `libc`
//! syscalls behind a small safe surface; every `unsafe` block carries a `SAFETY` note.

use std::ffi::c_int;
use std::io;
use std::mem;
use std::os::fd::{AsFd, AsRawFd, BorrowedFd, FromRawFd, OwnedFd, RawFd};

use crate::error::{Error, Result};
use crate::interface::IfIndex;
use crate::sys::Stats;

/// `ETH_P_ALL` in host byte order — capture/inject every protocol.
pub const ETH_P_ALL: u16 = 0x0003;

/// The socket flavour: whether the kernel hands us the link-layer header or strips it.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SocketKind {
    /// `SOCK_RAW`: frames include the full Ethernet header on both RX and TX.
    Raw,
    /// `SOCK_DGRAM`: the kernel strips/adds the link-layer header for you.
    Dgram,
}

impl SocketKind {
    const fn as_raw(self) -> c_int {
        match self {
            SocketKind::Raw => libc::SOCK_RAW,
            SocketKind::Dgram => libc::SOCK_DGRAM,
        }
    }
}

/// An open, interface-bound `AF_PACKET` socket.
#[derive(Debug)]
pub struct PacketSocket {
    fd: OwnedFd,
    ifindex: IfIndex,
}

impl PacketSocket {
    /// Opens an `AF_PACKET` socket and binds it to `ifindex`.
    ///
    /// `protocol` is given in host byte order (e.g. [`ETH_P_ALL`]). The socket is created
    /// non-blocking and close-on-exec.
    pub fn open(ifindex: IfIndex, protocol: u16, kind: SocketKind) -> Result<Self> {
        let proto_be = i32::from(protocol.to_be());
        // SAFETY: a plain socket(2) call; arguments are valid constants. The returned fd is
        // immediately adopted by an OwnedFd so it cannot leak.
        let raw = unsafe {
            libc::socket(
                libc::AF_PACKET,
                kind.as_raw() | libc::SOCK_NONBLOCK | libc::SOCK_CLOEXEC,
                proto_be,
            )
        };
        if raw < 0 {
            return Err(Error::Socket(io::Error::last_os_error()));
        }
        // SAFETY: `raw` is a fresh, valid, owned fd returned by socket(2) just above.
        let fd = unsafe { OwnedFd::from_raw_fd(raw) };
        let sock = PacketSocket { fd, ifindex };
        sock.bind(protocol)?;
        Ok(sock)
    }

    fn bind(&self, protocol: u16) -> Result<()> {
        // SAFETY: sockaddr_ll is plain-old-data; an all-zero value is a valid initial state.
        let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() };
        addr.sll_family = libc::AF_PACKET as u16;
        addr.sll_protocol = protocol.to_be();
        addr.sll_ifindex = self.ifindex.0 as c_int;

        // SAFETY: `addr` is a fully-initialised sockaddr_ll; we pass its real size. The fd is
        // valid for the duration of the call.
        let rc = unsafe {
            libc::bind(
                self.fd.as_raw_fd(),
                (&raw const addr).cast::<libc::sockaddr>(),
                mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::Bind(io::Error::last_os_error()));
        }
        Ok(())
    }

    /// Sends a single frame. Returns `WouldBlock` if the kernel's send buffer is full.
    pub fn send(&self, frame: &[u8]) -> io::Result<usize> {
        // SAFETY: `frame` is a valid readable slice of `frame.len()` bytes; the fd is valid.
        let n = unsafe {
            libc::send(
                self.fd.as_raw_fd(),
                frame.as_ptr().cast(),
                frame.len(),
                libc::MSG_DONTWAIT,
            )
        };
        if n < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(n as usize)
    }

    /// Receives a single frame into `buf`, returning `(len, packet_type)`.
    ///
    /// `len` may exceed `buf.len()` if the frame was truncated; the returned slice length is
    /// always clamped to what was written. `packet_type` is the raw `sll_pkttype`.
    pub fn recv_into(&self, buf: &mut [u8]) -> io::Result<(usize, u8)> {
        // SAFETY: sockaddr_ll is plain-old-data; an all-zero value is a valid initial state.
        let mut addr: libc::sockaddr_ll = unsafe { mem::zeroed() };
        let mut addr_len = mem::size_of::<libc::sockaddr_ll>() as libc::socklen_t;
        // SAFETY: `buf` is a valid writable slice of `buf.len()` bytes; `addr`/`addr_len` are a
        // valid sockaddr storage and its length. The fd is valid.
        let n = unsafe {
            libc::recvfrom(
                self.fd.as_raw_fd(),
                buf.as_mut_ptr().cast(),
                buf.len(),
                libc::MSG_DONTWAIT,
                (&raw mut addr).cast::<libc::sockaddr>(),
                &raw mut addr_len,
            )
        };
        if n < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok((n as usize, addr.sll_pkttype))
    }

    /// Enables or disables promiscuous mode on the bound interface.
    pub fn set_promiscuous(&self, on: bool) -> Result<()> {
        // SAFETY: packet_mreq is plain-old-data; an all-zero value is a valid initial state.
        let mut mreq: libc::packet_mreq = unsafe { mem::zeroed() };
        mreq.mr_ifindex = self.ifindex.0 as c_int;
        mreq.mr_type = libc::PACKET_MR_PROMISC as u16;
        let opt = if on { libc::PACKET_ADD_MEMBERSHIP } else { libc::PACKET_DROP_MEMBERSHIP };
        // SAFETY: `mreq` is a fully-initialised packet_mreq passed with its true size.
        let rc = unsafe {
            libc::setsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                opt,
                (&raw const mreq).cast(),
                mem::size_of::<libc::packet_mreq>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::SetSockOpt {
                option: "PACKET_MR_PROMISC",
                source: io::Error::last_os_error(),
            });
        }
        Ok(())
    }

    /// Joins this socket to a `PACKET_FANOUT` group, so the kernel load-balances received frames
    /// across all sockets in the group (one per core, for multi-queue RX scaling).
    ///
    /// `mode` is a `PACKET_FANOUT_*` selector (already including any flags). The kernel encodes the
    /// request as `group_id | (mode << 16)`.
    pub fn set_fanout(&self, group_id: u16, mode: u16) -> Result<()> {
        self.set_packet_opt_int(libc::PACKET_FANOUT, "PACKET_FANOUT", fanout_arg(group_id, mode))
    }

    /// Sets an integer-valued `SOL_PACKET` option.
    pub fn set_packet_opt_int(&self, opt: c_int, name: &'static str, val: c_int) -> Result<()> {
        // SAFETY: `val` is a valid c_int passed with size_of::<c_int>(); fd is valid.
        let rc = unsafe {
            libc::setsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                opt,
                (&raw const val).cast(),
                mem::size_of::<c_int>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::SetSockOpt { option: name, source: io::Error::last_os_error() });
        }
        Ok(())
    }

    /// Sets a `SOL_PACKET` option from an arbitrary POD request struct (e.g. a ring request).
    ///
    /// # Safety
    ///
    /// `T` must be a plain-old-data type the kernel expects for `opt`; `req` is copied by value
    /// into the kernel, so it must be fully initialised with no padding requirements violated.
    pub unsafe fn set_packet_opt<T>(&self, opt: c_int, name: &'static str, req: &T) -> Result<()> {
        // SAFETY: by this function's contract `req` is a valid `T` of the size the kernel expects
        // for `opt`; we pass its real size. The fd is valid.
        let rc = unsafe {
            libc::setsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                opt,
                (req as *const T).cast(),
                mem::size_of::<T>() as libc::socklen_t,
            )
        };
        if rc < 0 {
            return Err(Error::SetSockOpt { option: name, source: io::Error::last_os_error() });
        }
        Ok(())
    }

    /// Reads and resets the kernel's `tpacket_stats_v3` counters.
    pub fn statistics(&self) -> io::Result<Stats> {
        // SAFETY: tpacket_stats_v3 is plain-old-data; an all-zero value is a valid initial state.
        let mut stats: libc::tpacket_stats_v3 = unsafe { mem::zeroed() };
        let mut len = mem::size_of::<libc::tpacket_stats_v3>() as libc::socklen_t;
        // SAFETY: `stats`/`len` are valid storage and its length; getsockopt fills `stats` and
        // updates `len`. The fd is valid.
        let rc = unsafe {
            libc::getsockopt(
                self.fd.as_raw_fd(),
                libc::SOL_PACKET,
                libc::PACKET_STATISTICS,
                (&raw mut stats).cast(),
                &raw mut len,
            )
        };
        if rc < 0 {
            return Err(io::Error::last_os_error());
        }
        Ok(Stats {
            received: u64::from(stats.tp_packets),
            dropped: u64::from(stats.tp_drops),
            freezes: u64::from(stats.tp_freeze_q_cnt),
        })
    }
}

/// Encodes a `PACKET_FANOUT` request: the group id in the low 16 bits, the mode (plus any flags)
/// in the high 16 bits.
pub fn fanout_arg(group_id: u16, mode: u16) -> c_int {
    c_int::from(group_id) | (c_int::from(mode) << 16)
}

/// Blocks until `fd` is readable (or `timeout_ms` elapses), returning whether it became readable.
///
/// `timeout_ms` of `-1` waits indefinitely. Used by the synchronous receive path.
pub fn poll_readable(fd: BorrowedFd<'_>, timeout_ms: c_int) -> io::Result<bool> {
    let mut pfd = libc::pollfd { fd: fd.as_raw_fd(), events: libc::POLLIN, revents: 0 };
    // SAFETY: `pfd` is a single valid pollfd; we pass a count of 1.
    let rc = unsafe { libc::poll(&raw mut pfd, 1, timeout_ms) };
    if rc < 0 {
        return Err(io::Error::last_os_error());
    }
    Ok(rc > 0 && pfd.revents & libc::POLLIN != 0)
}

impl AsFd for PacketSocket {
    fn as_fd(&self) -> BorrowedFd<'_> {
        self.fd.as_fd()
    }
}

impl AsRawFd for PacketSocket {
    fn as_raw_fd(&self) -> RawFd {
        self.fd.as_raw_fd()
    }
}

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

    #[test]
    fn fanout_arg_encoding() {
        // group id in low 16 bits, mode in high 16 bits.
        assert_eq!(fanout_arg(0x1234, libc::PACKET_FANOUT_LB as u16), (1 << 16) | 0x1234);
        assert_eq!(fanout_arg(0, libc::PACKET_FANOUT_HASH as u16), 0);
        assert_eq!(fanout_arg(0xffff, libc::PACKET_FANOUT_CPU as u16), (2 << 16) | 0xffff);
    }

    #[test]
    #[cfg_attr(miri, ignore = "opens an AF_PACKET socket; not interpretable by Miri")]
    fn open_without_privileges_reports_socket_error() {
        // Opening an AF_PACKET socket needs CAP_NET_RAW. In an unprivileged environment this must
        // fail with a clear Error::Socket rather than panicking; with privileges it succeeds.
        match PacketSocket::open(IfIndex(1), ETH_P_ALL, SocketKind::Raw) {
            Ok(sock) => {
                // We have CAP_NET_RAW: the socket should hold a valid descriptor.
                assert!(sock.as_raw_fd() >= 0);
            }
            Err(Error::Socket(_)) | Err(Error::Bind(_)) => {}
            Err(other) => panic!("unexpected error opening packet socket: {other}"),
        }
    }
}