netring 0.21.0

High-performance zero-copy packet I/O for Linux (AF_PACKET TPACKET_V3 + AF_XDP)
Documentation
//! Unified multi-protocol event surface.
//!
//! [`ProtocolEvent<K>`] is a sum-type over the lifecycle events
//! that come out of flowscope's `FlowTracker` plus the L7 messages
//! emitted by HTTP / DNS / TLS parsers. It's the canonical event
//! type for **multi-protocol anomaly correlation** — see
//! [`crate::correlate`] for the primitives that consume it.
//!
//! [`ProtocolMonitor<K>`] is the entry point: declare which
//! protocols you care about (`flow()`, `http()`, `dns()`, `tls()`),
//! the monitor orchestrates one filtered `AsyncCapture` per
//! protocol and yields events through a unified async stream.
//!
//! ```no_run
//! # #[cfg(all(feature = "tokio", feature = "http", feature = "dns"))]
//! # async fn _ex() -> Result<(), Box<dyn std::error::Error>> {
//! use futures::StreamExt;
//! use netring::flow::extract::FiveTuple;
//! use netring::protocol::{ProtocolEvent, ProtocolMessage, ProtocolMonitorBuilder};
//!
//! let mut monitor = ProtocolMonitorBuilder::new()
//!     .interface("eth0")
//!     .flow()
//!     .http()
//!     .dns()
//!     .build(FiveTuple::bidirectional())?;
//!
//! while let Some(evt) = monitor.next().await {
//!     match evt? {
//!         ProtocolEvent::FlowStarted { .. }
//!         | ProtocolEvent::FlowEnded { .. } => { /* flow lifecycle */ }
//!         ProtocolEvent::Message { parser_kind, message: ProtocolMessage::Http(_), .. } => {
//!             let _ = parser_kind;
//!         }
//!         ProtocolEvent::Message { message: ProtocolMessage::Dns(_), .. } => {
//!             /* dns query/response/unanswered */
//!         }
//!         _ => {}
//!     }
//! }
//! # Ok(()) }
//! ```

mod event;
mod monitor;

pub mod builtin;
pub mod event_typed;

pub use event::{ProtocolEvent, ProtocolMessage};
pub use event_typed::{
    AnyFlowAnomaly, Event, FlowEnded, FlowEstablished, FlowPacket, FlowStarted, FlowTick,
    ParserClosed, Side, Tick,
};
#[allow(deprecated)]
pub use monitor::{ProtocolMonitor, ProtocolMonitorBuilder};

// Re-export the 7 built-in `Protocol` markers at the protocol
// module level so `use netring::protocol::Http;` works (without
// the intermediate `::builtin::` path). The markers also live at
// `netring::protocol::builtin::*` and `netring::prelude::*` for
// users who prefer those paths.
#[cfg(feature = "dns")]
pub use builtin::Dns;
#[cfg(feature = "http")]
pub use builtin::Http;
pub use builtin::{Icmp, Tcp, Udp};
#[cfg(feature = "tls")]
pub use builtin::{Tls, TlsHandshake};

// ─── Plugin layer (netring 0.20, Phase A) ──────────────────────────────────
//
// The `Protocol` trait + supporting types define a protocol-agnostic plugin
// layer. Downstream crates implement `Protocol` for their own marker types
// and register them via the (forthcoming) `Monitor::builder().protocol::<P>()`
// API.
//
// In Phase A these types are defined but NOT yet consumed by the existing
// `ProtocolMonitorBuilder`. Phase B introduces the `Monitor` builder that
// uses them.

/// A protocol the monitor can observe.
///
/// Implementors are usually zero-sized marker types (`struct Http;`).
/// The marker is used as a type-level identifier; the runtime
/// dispatch key is its `TypeId`.
///
/// `'static` is required because dispatch is keyed by `TypeId`.
/// This forecloses lifetime-parameterized marker types — not a
/// real limitation since markers are typically ZSTs.
///
/// Built-in markers ship in [`builtin`]; downstream crates can
/// add their own without editing netring.
///
/// ## Why [`Self::register`] instead of returning a boxed parser
///
/// flowscope's `DriverBuilder::session_on_ports` (and friends)
/// require `P: SessionParser + Clone + Send + 'static`. A boxed
/// trait object (`Box<dyn SessionParser<Message = M>>`) can't
/// satisfy `Clone` and is `!Sized`, so the "give me your boxed
/// parser, I'll register it" shape doesn't compile. Instead the
/// `Protocol` impl drives the registration itself — it keeps the
/// parser as its concrete type all the way to the call site.
pub trait Protocol: Send + Sync + 'static {
    /// The typed message this protocol's parser emits. Must be
    /// `'static` (owning) — the framework downcasts via `Any`,
    /// which requires `'static`.
    type Message: Send + Sync + 'static;

    /// Stable identifier, used for metrics labels, log targets,
    /// and the `parser_kind` field on the low-level Stream API.
    /// Convention: lowercase, hyphenated. Examples: `"http/1"`,
    /// `"dns-udp"`, `"tls-handshake"`. Matches flowscope's
    /// `parser_kinds::*` constants where applicable.
    const NAME: &'static str;

    /// How packets get routed to this protocol's parser.
    fn dispatch() -> Dispatch;

    /// Register this protocol's parser with the given flowscope
    /// driver builder and return the typed drain handle.
    ///
    /// The builder is parameterised on
    /// [`flowscope::extract::FiveTuple`] — netring's canonical
    /// flow extractor. The handle yields per-parser typed
    /// messages of [`Self::Message`].
    ///
    /// Lifecycle-only markers ([`builtin::Tcp`] / [`builtin::Udp`])
    /// have no parser to register and return
    /// [`ProtocolInitError`]; the [`crate::monitor::Monitor`] builder treats
    /// [`Dispatch::AllTcp`] / [`Dispatch::AllUdp`] as "lifecycle
    /// dispatch only — central tracker handles it" and ignores
    /// the error.
    fn register(
        builder: &mut flowscope::driver::DriverBuilder<flowscope::extract::FiveTuple>,
    ) -> Result<
        flowscope::driver::SlotHandle<Self::Message, flowscope::extract::FiveTupleKey>,
        ProtocolInitError,
    >;

    /// 0.21 F.1: register this protocol's parser as a *broadcast*
    /// slot — every drain handle clone receives a copy of each
    /// emitted message, enabling
    /// [`crate::monitor::Monitor::subscribe`] for this protocol.
    ///
    /// Default returns `Err` — protocols that need broadcast must
    /// override (e.g. via flowscope's
    /// `DriverBuilder::session_on_ports_broadcast_each`). The
    /// override imposes `Message: Send + Sync + Clone + 'static`
    /// (per-subscriber clone semantics).
    ///
    /// Used by [`crate::monitor::MonitorBuilder::with_broadcast`]
    /// — calling `register_broadcast` on the builder also adds the
    /// returned handle to the monitor's slot drain (so registered
    /// handlers still see every message, in addition to user
    /// subscribers).
    fn register_broadcast(
        _builder: &mut flowscope::driver::DriverBuilder<flowscope::extract::FiveTuple>,
    ) -> Result<
        flowscope::driver::BroadcastSlotHandle<Self::Message, flowscope::extract::FiveTupleKey>,
        ProtocolInitError,
    >
    where
        Self::Message: Send + Sync + Clone + 'static,
    {
        Err(ProtocolInitError(format!(
            "{} does not support broadcast (only session-shaped L7 protocols do today)",
            Self::NAME
        )))
    }
}

/// How a protocol selects packets for its parser.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Dispatch {
    /// Match TCP flows on these ports.
    Tcp(Vec<u16>),
    /// Match UDP flows on these ports.
    Udp(Vec<u16>),
    /// Match all ICMP / ICMPv6 datagrams.
    Icmp,
    /// All TCP flows regardless of port — the L4-lifecycle case
    /// for the [`builtin::Tcp`] marker.
    AllTcp,
    /// All UDP flows regardless of port — the [`builtin::Udp`] marker.
    AllUdp,
    /// Port-agnostic dispatch via a signature function over the
    /// first ≤64 payload bytes. The function returns whether the
    /// packet matches; matching flows pin to the parser.
    Signature(fn(&[u8]) -> SignatureMatch),
}

/// Result of a signature function. `Match` pins the flow to this
/// protocol's parser; `NoMatch` skips it; `NeedMoreData` says
/// "I need more bytes" — the dispatcher keeps probing until budget
/// runs out.
///
/// Mirrors [`flowscope::detect::signatures::SignatureMatch`] —
/// the [`From`] impl converts losslessly so netring users can
/// pass flowscope signature functions directly into
/// [`Dispatch::Signature`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignatureMatch {
    /// Bytes definitively match this protocol.
    Match,
    /// Bytes definitively do not match.
    NoMatch,
    /// Not enough bytes to decide — re-check with more.
    NeedMoreData,
}

impl From<flowscope::detect::signatures::SignatureMatch> for SignatureMatch {
    fn from(s: flowscope::detect::signatures::SignatureMatch) -> Self {
        use flowscope::detect::signatures::SignatureMatch as Fs;
        match s {
            Fs::Match => SignatureMatch::Match,
            Fs::NoMatch => SignatureMatch::NoMatch,
            Fs::NeedMoreData => SignatureMatch::NeedMoreData,
        }
    }
}

/// Error type for [`Protocol::register`]. Most parsers are
/// infallible to construct; flowscope parsers that take config
/// can fail. Lifecycle-only markers (Tcp, Udp) use this to
/// indicate "no parser slot needed — handled by the central
/// flow tracker."
#[derive(Debug, thiserror::Error)]
#[error("protocol parser init failed: {0}")]
pub struct ProtocolInitError(pub String);

/// Convenience alias — the flow key produced by
/// [`flowscope::extract::FiveTuple`]. Most user code names this
/// rather than the longer fully-qualified path.
pub type FlowKey = flowscope::extract::FiveTupleKey;

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

    #[test]
    fn signature_match_from_flowscope_roundtrip() {
        use flowscope::detect::signatures::SignatureMatch as Fs;
        assert_eq!(SignatureMatch::from(Fs::Match), SignatureMatch::Match);
        assert_eq!(SignatureMatch::from(Fs::NoMatch), SignatureMatch::NoMatch);
        assert_eq!(
            SignatureMatch::from(Fs::NeedMoreData),
            SignatureMatch::NeedMoreData
        );
    }

    #[test]
    fn dispatch_is_clone_and_debug() {
        let d = Dispatch::Tcp(vec![80, 8080]);
        let _ = format!("{d:?}");
        let _ = d.clone();
    }

    #[test]
    fn protocol_init_error_displays() {
        let e = ProtocolInitError("config missing".into());
        assert!(format!("{e}").contains("config missing"));
    }
}