ferranet 0.2.0

A modern, async-first, zero-copy datalink-layer (L2) networking library
Documentation
//! Error and result types for `ferranet`.
//!
//! Every runtime failure names the operation it came from (`Send`, `Recv`, `Stats`, ...) rather
//! than arriving as a bare I/O error: "Operation would block" alone doesn't say whether the TX
//! ring or the RX path produced it. The underlying OS error stays reachable through the variant's
//! source (or uniformly via [`Error::as_io`]) for errno-level handling.

use std::borrow::Cow;
use std::io;

/// The result type returned by fallible `ferranet` operations.
pub type Result<T> = std::result::Result<T, Error>;

/// Errors that can occur while opening or operating a datalink channel.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// The named network interface could not be found.
    #[error("network interface {0:?} not found")]
    InterfaceNotFound(String),

    /// Creating the channel's backing descriptors (the `AF_PACKET`/`AF_XDP` socket, or the dummy
    /// backend's pipes) failed.
    ///
    /// Commonly caused by missing `CAP_NET_RAW`; see the crate-level docs on permissions.
    #[error("failed to open packet socket: {0}")]
    Socket(#[source] io::Error),

    /// Binding the socket to an interface failed.
    #[error("failed to bind packet socket to interface: {0}")]
    Bind(#[source] io::Error),

    /// Setting a socket option failed.
    #[error("failed to set socket option {option}: {source}")]
    SetSockOpt {
        /// The name of the option that could not be set.
        option: &'static str,
        /// The underlying OS error.
        #[source]
        source: io::Error,
    },

    /// Mapping the `PACKET_MMAP` ring buffer failed.
    #[error("failed to mmap packet ring: {0}")]
    Mmap(#[source] io::Error),

    /// A frame submitted for transmission exceeds the configured frame size.
    #[error("frame of {len} bytes exceeds maximum frame size of {max} bytes")]
    FrameTooLarge {
        /// The length of the offending frame.
        len: usize,
        /// The maximum permitted frame length.
        max: usize,
    },

    /// The supplied channel/ring configuration is invalid.
    #[error("invalid channel configuration: {0}")]
    InvalidConfig(Cow<'static, str>),

    /// Transmitting failed.
    #[error("failed to send frame: {0}")]
    Send(#[source] io::Error),

    /// Receiving failed.
    ///
    /// A receiver whose dummy [`Injector`](crate::dummy::Injector) was dropped reports
    /// end-of-stream as `Recv` with [`io::ErrorKind::UnexpectedEof`].
    #[error("failed to receive: {0}")]
    Recv(#[source] io::Error),

    /// Reading channel statistics failed.
    #[error("failed to read channel statistics: {0}")]
    Stats(#[source] io::Error),

    /// Looking up or enumerating network interfaces failed (other than a missing interface,
    /// which is [`Error::InterfaceNotFound`]).
    #[error("failed to look up network interfaces: {0}")]
    Interfaces(#[source] io::Error),

    /// Registering the channel's descriptor with the async reactor failed.
    #[cfg(feature = "tokio")]
    #[error("failed to register with the async reactor: {0}")]
    Register(#[source] io::Error),
}

impl Error {
    /// The underlying OS error, when this error wraps one.
    ///
    /// Lets callers handle errno-level conditions (e.g. [`io::ErrorKind::UnexpectedEof`] from a
    /// closed dummy network) without matching every operation variant.
    #[must_use]
    pub fn as_io(&self) -> Option<&io::Error> {
        match self {
            Error::Socket(e)
            | Error::Bind(e)
            | Error::SetSockOpt { source: e, .. }
            | Error::Mmap(e)
            | Error::Send(e)
            | Error::Recv(e)
            | Error::Stats(e)
            | Error::Interfaces(e) => Some(e),
            #[cfg(feature = "tokio")]
            Error::Register(e) => Some(e),
            Error::InterfaceNotFound(_) | Error::FrameTooLarge { .. } | Error::InvalidConfig(_) => {
                None
            }
        }
    }

    /// Builds an [`Error::InvalidConfig`] from a static or formatted message.
    pub(crate) fn invalid_config(msg: impl Into<Cow<'static, str>>) -> Self {
        Error::InvalidConfig(msg.into())
    }
}

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

    #[test]
    fn as_io_reaches_the_os_error_across_variants() {
        let e = Error::Recv(io::Error::from(io::ErrorKind::UnexpectedEof));
        assert_eq!(e.as_io().unwrap().kind(), io::ErrorKind::UnexpectedEof);
        let e = Error::Send(io::Error::from(io::ErrorKind::WouldBlock));
        assert_eq!(e.as_io().unwrap().kind(), io::ErrorKind::WouldBlock);
        let e = Error::SetSockOpt {
            option: "PACKET_FANOUT",
            source: io::Error::from_raw_os_error(libc::EINVAL),
        };
        assert_eq!(e.as_io().unwrap().raw_os_error(), Some(libc::EINVAL));
        assert!(Error::invalid_config("nope").as_io().is_none());
        assert!(Error::FrameTooLarge { len: 1, max: 0 }.as_io().is_none());
    }

    #[test]
    fn display_names_the_operation() {
        let e = Error::Send(io::Error::from(io::ErrorKind::WouldBlock));
        assert!(e.to_string().starts_with("failed to send frame"), "{e}");
        let e = Error::Stats(io::Error::from(io::ErrorKind::PermissionDenied));
        assert!(e.to_string().starts_with("failed to read channel statistics"), "{e}");
    }
}