netring
High-performance zero-copy packet I/O for Linux, async-first.
netring provides packet capture and injection via AF_PACKET (TPACKET_V3
block-based mmap ring buffers) and AF_XDP (kernel-bypass via XDP sockets).
The recommended API is async/tokio; sync types are first-class but
mostly used as the underlying source for the async wrappers.
Quick start (async, recommended)
[]
= { = "0.21", = ["tokio"] }
// Capture: zero-copy borrowed batches via AsyncFd.
# async
// Stream-style consumption with futures::StreamExt
// (add `futures = "0.3"` to your Cargo.toml):
use StreamExt;
let mut stream = open?.into_stream;
while let Some = stream.next.await
// Inject with backpressure (awaits POLLOUT when ring is full):
# async
// AF_XDP (kernel bypass, 10M+ pps) — same shape as AsyncCapture:
#
# async
See docs/ASYNC_GUIDE.md for the full async story —
patterns, trade-offs, when to use which entry point, and Send/!Send
considerations.
Flow & session tracking
[]
= { = "0.21", = ["tokio", "flow"] }
= "0.3"
use StreamExt;
use AsyncCapture;
use FiveTuple;
use FlowEvent;
let cap = open?;
let mut stream = cap.flow_stream;
while let Some = stream.next.await
Pluggable flow keys (5-tuple, IpPair, MacPair, VLAN/MPLS/VXLAN/GTP-U
decap combinators, custom extractors), bidirectional sessions, TCP
state machine with Zeek-style history string, idle-timeout sweep,
LRU eviction, optional TCP reassembly hook (sync Reassembler or
async AsyncReassembler with channel_factory for backpressure).
The flow types live in a separate cross-platform crate
flowscope (no Linux, no
tokio, no async runtime — usable with pcap, tun-tap, embedded).
netring is the Linux capture integration; the underlying flow API
works on any source of &[u8] frames.
flowscope also ships feature-gated L7 modules: http (HTTP/1.x),
tls (TLS handshake observation, optional JA3), dns (DNS-over-UDP
parser + correlator), icmp (ICMPv4 + ICMPv6 with IcmpInner
cross-protocol correlation), and pcap (offline replay).
Declarative Monitor (recommended)
The Monitor::builder() API is a single fluent surface: typed event
handlers, a tower-style middleware chain over the anomaly sink, an
opt-in async escape hatch, detector! and pattern_detector! macros,
streaming subscribers, per-CPU sharding, offline pcap replay, and (0.22)
a high-level operations toolkit — bandwidth-by-app, ICMP-error
correlation, TCP-reset alerts, custom port labels, and a report stream.
[]
# Full app-tier experience (Monitor + all sinks + parsers):
= { = "0.22", = ["monitor-quickstart"] }
# Lean embedded build — pick what you need:
= { = "0.22", = ["monitor", "eve-sink", "metrics"] }
0.22 is a breaking release. Typed protocol roles make
on::<Tcp>andFlowStarted<Http>compile errors;FlowPacketis now flat (FlowPacket { proto, … }); the legacy 0.19ProtocolMonitor/AnomalyMonitor/AnomalyRuleAPI is removed. See docs/MIGRATING_0.21_TO_0.22.md.
Monitor is Send as of 0.21 — plain #[tokio::main] (the
multi-thread runtime) works without ceremony. The
Monitor::run_for / run_until_signal futures stay !Send
because the underlying AsyncCapture<S> borrows from the
!Sync mmap ring across awaits — tokio::spawn(monitor.run_for(d))
won't compile. Keep the run-loop future on the main task and
use tokio::select! (or ChannelSink to ship anomalies to a
spawned consumer task). flowscope's Driver<E>: Send + Sync
is unconditional upstream as of 0.13.
use Duration;
use *;
let truncated_tls = detector! ;
builder
.interface
. // FlowStarted/Ended<Tcp> lifecycle events
. // HttpMessage events from flowscope's HttpParser
. // one synthesised event per completed TLS handshake
.
.
.detect
.layer // outermost: drop Info
.layer// drop dups
.sink // innermost: stdout
.run_until_signal
.await?;
Builder surface — .interface(s) / .protocol::<P>() /
.all_l4() / .all_l7() / .on::<E>(handler) (payload only) /
.on_ctx::<E>(handler) (payload + &mut Ctx) / .on_async::<E>(handler) /
.state::<T>() / .counter::<K>(window, bucket) / .sink(s) /
.layer(L) / .tick(period, handler) / .tick_ctx(period, |ctx| …) /
.detect(handler) / .build().
0.22 high-level toolkit — one-call operational signals:
.on_bandwidth(period, |bw| …) (per-app bytes/sec → a typed
BandwidthReport), .on_icmp_error(|err, ctx| …) (unified v4/v6 ICMP
errors with the originating flow joined), .on_tcp_reset(|rst, ctx| …),
.label_table(table) (custom port labels), and .report(period, …) /
.report_to(period, build, sink) (periodic typed snapshots → a
ReportSink). The examples/monitor/net_diagnostic.rs example uses the
first three.
Run modes — run_until(Instant), run_for(Duration),
run_until_signal() (SIGINT/SIGTERM), run_until_idle(window).
Ctx accessors — handlers receive &mut Ctx<'_>:
ctx.state_mut::<T>(), ctx.counter_mut::<K>(),
ctx.sink_mut(), plus the public fields ctx.ts, ctx.flow,
ctx.source. The split_state_sink::<T>() /
split_state_counter::<T, K>() helpers project disjoint
&mut references when a handler needs simultaneous access.
5 shipped middleware —
MinSeverity,
DedupeAnomalies,
RateLimitAnomalies,
Sample,
Tee.
Compose freely; ordering is outermost-first.
4 shipped sinks — StdoutSink (text), StdoutJsonSink
(JSON, feature = "serde"), TracingSink (tracing::event! at
the matching tracing::Level), ChannelSink (tokio mpsc with
OwnedAnomaly payloads).
Zero allocations on the dispatch path. Verified by the
benches/zero_alloc.rs dhat benchmark: 100k synthetic dispatches
(state mutation, counter bump, sink emit) measure
Δ 0 bytes / 0 blocks in steady state.
Async escape hatch. on_async::<E>(handler) runs each
event through a boxed future (one allocation per event per
handler — only when explicitly registered). Async handlers
receive payload only; capture Arc<Pool> in the closure or
pair with ChannelSink to ship anomalies to an async I/O
task.
0.21 builder additions — same Monitor::builder() plus:
.with_broadcast::<P>() (enrolls a broadcast slot for
Monitor::subscribe::<P>()); .fanout(FanoutMode::Cpu, group_id)
(per-CPU sharding via ShardedRunner); .pcap_source(path) +
.pcap_speed_factor(f) (offline pcap replay); .drain_timeout(d)
(graceful drain phase after shutdown); .flow_state::<T>(idle)
(per-flow state map, ctx.flow_state_mut::<T>());
.name(s) (label surfaced through ctx.monitor_name).
Streaming subscribers — pull events out of the dispatcher
as a futures_core::Stream instead of registering a handler:
use StreamExt;
use *;
let monitor = builder
.interface
.
. // enrol the broadcast slot
.build?;
let mut stream = monitor.;
spawn;
while let Some = stream.next.await
Per-CPU sharding — fan one capture across N AF_PACKET
sockets via PACKET_FANOUT_CPU:
use Arc;
use *;
use ShardedRunner;
let runner = new?;
runner.run_until_signal.await?;
Pcap replay — same handler graph, offline:
builder
.pcap_source
.pcap_speed_factor // 10× wire speed
.
.on
.build?
.replay
.await?;
See docs/MIGRATING_0.21_TO_0.22.md for
the breaking-change recipes (typed protocol roles, flat FlowPacket,
the removed 0.19 API), and examples/monitor/ for runnable demos.
Multi-protocol monitoring + anomaly correlation
The 0.19
ProtocolMonitor/AnomalyMonitor/AnomalyRulecorrelation API was removed in 0.22.
Watch one interface for several protocols and correlate across them on
the declarative Monitor::builder() API — register protocols with
.all_l4() / .all_l7() / .protocol::<P>(), handle events with
.on::<E> / .on_ctx::<E> / detector! / pattern_detector!, share
state across handlers with .state::<T>() + ctx.state_mut::<T>(), and
emit findings via ctx.emit(kind, severity) into the layered sink chain:
use IpAddr;
use Duration;
use *;
use DnsMessage;
builder
.interface
.all_l4 // Tcp + Udp + Icmp
. // + the DNS parser
.
.
.layer
.sink
.run_until_signal
.await?;
The cross-protocol correlation primitives (TimeBucketedCounter,
KeyIndexed, RollingRate, BurstDetector, Ewma, TopK) live in
netring::correlate; examples/monitor/ ships reference detectors
(port_scan, beacon_detector, dga_query, net_diagnostic).
Anomaly<K> impls Display for one-line greppable output and
to_json_line() for production-pipeline JSON (no serde dep —
escaping is hand-rolled to RFC 8259 §7). Severity tiers
(Info/Warning/Error/Critical) port directly to flowscope's
AnomalyKind::severity() via a From impl. Reference detectors
live under examples/monitor/ on the declarative Monitor::builder()
API (port_scan, beacon_detector, dga_query, net_diagnostic,
file_hash_dfir), plus raw-primitive correlators under
examples/anomaly/ (dns_query_burst, dns_resolved_no_connection).
Pair with cargo run --example synthetic_traffic to demo on lo
without CAP_NET_RAW.
See docs/WRITING_DETECTORS.md for
the full tutorial — anatomy of an AnomalyRule, state-primitive
decision table, observe vs on_tick, cross-protocol patterns,
testing, production deployment, and MITRE ATT&CK mapping.
Stream observability
Every async stream type — FlowStream, SessionStream,
DatagramStream, DedupStream — implements the sealed
StreamCapture trait. That gives uniform access to kernel ring
stats and the underlying AsyncCapture even after the stream has
consumed it:
use ;
use FiveTuple;
let cap = open?;
let stream = cap.flow_stream;
// Kernel ring stats while the stream runs:
let stats = stream.capture_stats?;
println!;
// Atomic BPF filter swap on a running stream:
let new_filter = builder.tcp.dst_port.build?;
stream.capture.set_filter?;
Pair with with_pcap_tap(writer) on any of the four stream types
to record raw frames before the flow tracker processes them —
decoded events and a wire-faithful capture file from one invocation:
use File;
use BufWriter;
use CaptureWriter;
let writer = create?;
let stream = cap
.flow_stream
.with_pcap_tap;
TapErrorPolicy::{Continue (default), DropTap, FailStream}
controls disk-full / I/O-glitch handling.
BPF filter ergonomics
AsyncCapture::open_with_filter is the one-call sugar for the
common case:
use ;
let filter = builder.tcp.dst_port.build.unwrap;
let _cap = open_with_filter.unwrap;
For runtime filter swaps without tearing down the kernel ring:
use ;
let cap = open.unwrap;
let new = builder.tcp.dst_port.build.unwrap;
cap.set_filter.unwrap; // atomic in-kernel replacement
set_filter is gated to AF_PACKET-backed captures via the
PacketSetFilter trait; AsyncCapture<XdpSocket> doesn't expose
it (XDP filtering belongs in the XDP program).
Multi-source capture
AsyncMultiCapture fans in N captures of two shapes — multiple
interfaces, or one interface with a fanout-group worker pool — into
a single tagged stream:
use StreamExt;
use AsyncMultiCapture;
use FiveTuple;
// Multi-interface gateway:
let multi = open?;
let mut stream = multi.flow_stream;
while let Some = stream.next.await
// Worker pool scaling (FanoutMode::Cpu by default):
let workers = open_workers?;
Per-source breakdown and aggregate stats:
let agg = stream.capture_stats;
for in stream.per_source_capture_stats
See docs/scaling.md for the canonical multi-core
recipe, the FanoutMode decision matrix, and 7 anti-patterns
(including the FANOUT_HASH-on-skewed-traffic and PACKET_FANOUT
-on-lo gotchas).
Offline replay
AsyncPcapSource reads PCAP and PCAPNG files asynchronously (format
auto-detected at open) and yields OwnedPackets through a tokio
Stream. The companion PcapFlowStream bridges to the same
flowscope FlowTracker used by live capture, so the same downstream
code runs both live and offline:
use StreamExt;
use AsyncPcapSource;
use FiveTuple;
let source = open.await?;
let mut events = source.flow_events;
while let Some = events.next.await
AsyncPcapConfig controls pacing (replay_speed = 1.0 for wire
rate, 2.0 for double speed, 0.0 for as-fast-as-possible) and
loop_at_eof for stress testing.
BPF filtering
netring ships a typed classic-BPF builder — no shelling out to
tcpdump -dd, no native-library deps:
use ;
let filter = builder
.tcp
.dst_port
.or
.build
.unwrap;
let cap = builder
.interface
.bpf_filter
.build
.unwrap;
Vocabulary: eth_type / ipv4 / ipv6 / arp, vlan / vlan_id,
ip_proto / tcp / udp / icmp, src_host / dst_host / host,
src_net / dst_net / net, src_port / dst_port / port,
plus negate() and or(|b| ...). See
examples/bpf_filter.rs for a runnable
demo. The escape hatch BpfFilter::new(insns) still accepts raw
bytecode from tcpdump -dd or any other source.
BpfFilter::matches(&[u8]) -> bool runs the bytecode in pure Rust
for offline validation against pcap data.
Sync API
The sync types power the async wrappers and are also usable directly:
// Flat iterator — simplest path.
let mut cap = open.unwrap;
for pkt in cap.packets.take
// Batch processing with sequence-gap detection.
use Capture;
use Duration;
let mut cap = builder
.interface
.block_size
.build
.unwrap;
while let Some = cap.next_batch_blocking.unwrap
Features
| Feature | Default | Description |
|---|---|---|
tokio |
off | Async wrappers (AsyncCapture, AsyncInjector, AsyncXdpSocket, PacketStream) |
af-xdp |
off | AF_XDP kernel-bypass packet I/O (pure Rust, no native deps) |
xdp-loader |
off | Built-in redirect-all XDP program loader for AF_XDP via aya. Implies af-xdp. See async_xdp_self_loaded example. |
channel |
off | Thread + bounded channel adapter (runtime-agnostic) |
parse |
off | Packet header parsing via etherparse |
pcap |
off | Stream packets to PCAP files |
metrics |
off | metrics crate counters (netring_capture_*_total) |
flow |
off | Pluggable flow & session tracking (pulls flowscope, see Flow & session tracking above) |
Public API
| Concept | Sync type | Async wrapper |
|---|---|---|
| AF_PACKET RX | Capture |
AsyncCapture<Capture> |
| AF_PACKET TX | Injector |
AsyncInjector |
| AF_XDP (RX + TX) | XdpSocket |
AsyncXdpSocket |
| Bridge two interfaces | Bridge |
Bridge::run_async |
| Channel adapter | — | ChannelCapture (sync threads) |
Every type has a ::open(iface) shortcut for the simple case and a
::builder() for full configuration.
Default Configuration
| Parameter | Default | Description |
|---|---|---|
block_size |
4 MiB | Ring buffer block size |
block_count |
64 | Number of blocks (256 MiB total) |
frame_size |
2048 | Minimum frame size |
block_timeout_ms |
60 | Block retirement timeout |
fill_rxhash |
true | Kernel fills RX flow hash |
Performance Tuning
| Profile | block_size | block_count | timeout_ms | Notes |
|---|---|---|---|---|
| High throughput | 4 MiB | 128–256 | 60 | + FanoutMode::Cpu + thread pinning |
| Low latency | 256 KiB | 64 | 1–10 | + busy_poll_us(50).prefer_busy_poll(true).busy_poll_budget(64) (kernel ≥ 5.11) |
| Memory-constrained | 1 MiB | 16 | 100 | 16 MiB total ring |
| Jumbo frames | 4 MiB | 64 | 60 | frame_size(65536) |
See docs/TUNING_GUIDE.md for detailed tuning advice.
Fanout Modes
Distribute packets across multiple sockets for multi-threaded capture:
| Mode | Strategy |
|---|---|
Hash |
Flow hash (same flow → same socket) |
Cpu |
Route to CPU that received the NIC interrupt |
LoadBalance |
Round-robin |
Rollover |
Fill one socket, overflow to next |
Random |
Random distribution |
QueueMapping |
NIC hardware queue mapping |
use ;
let cap = builder
.interface
.fanout
.fanout_flags
.build
.unwrap;
Statistics
# let cap = open.unwrap;
let stats = cap.stats.unwrap;
println!;
Reading stats resets the kernel counters — call periodically for rate calculation.
System Requirements
- Linux kernel 3.2+ (for TPACKET_V3), 5.4+ (for AF_XDP)
- Rust 1.95+ (edition 2024)
Capabilities
| Capability | Required For |
|---|---|
CAP_NET_RAW |
Creating AF_PACKET / AF_XDP sockets |
CAP_IPC_LOCK |
MAP_LOCKED (or sufficient RLIMIT_MEMLOCK) |
CAP_NET_ADMIN |
Promiscuous mode |
# Recommended: use justfile (sudo only once for setcap)
# Manual alternative
Examples
# 0.13.0 — stream observability, BPF ergonomics, multi-source, offline replay:
# 0.13.1 — async sibling of stats_monitor (StreamCapture::capture_stats demo):
# 0.14.0 — flowscope 0.4 ergonomics: one-step pcap-to-sessions + on_tick parsers:
# 0.15.0+ — real-life L7 monitors using flowscope's HTTP / DNS parsers:
# "watch everything" recipe — use the declarative Monitor (0.22):
# 0.21 — declarative Monitor (subscribe / pcap replay / pattern detectors):
Examples are organized by topic under
examples/ — basic/, async_basics/,
filter/, scaling/, xdp/, flow/, l7/, pcap/. See
examples/README.md for a per-category
index with the right --features flags.
Documentation
- Scaling capture across cores —
FanoutModedecision matrix, multi-worker recipe, anti-patterns (added 0.13.0) - Architecture — system design, lifetime model, ring layout
- API Overview — all types, methods, and configuration
- Async / tokio guide — async patterns, Send rules, Monitor on multi-thread
- Writing detectors — detector! / pattern_detector! tutorial
- Migrating 0.22 → 0.23 — breaking: spawnable
Sendrun-loop future;on_asynchandlers must returnSendfutures - Migrating 0.21 → 0.22 — breaking: typed protocol roles, flat
FlowPacket, ops toolkit, report stream, legacy 0.19 API removed - Migrating 0.20 → 0.21 — Send Monitor, key narrowing, subscribers, sharding
- Migrating 0.19 → 0.20 — legacy → declarative Monitor
- Tuning Guide — performance profiles, system tuning, monitoring
- Troubleshooting — common errors and fixes
License
Licensed under either of Apache License, Version 2.0 or MIT License at your option.