From ff66b962664c527f9b8bd14092c5909a1153d514 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Wed, 8 Jul 2026 16:12:15 +0300 Subject: [PATCH] observer: packet capture facility (tcpdump-like) via netcfg Adds packet observer with ring buffer (256 packets default): - Observer::capture(packet) hooks into forwarding/output paths - AtomicBool enable/disable toggle (no capture overhead when off) - capture/count: live stats (total captured, buffered, enabled) - capture/read: drain hex dump of buffered packets - capture/enable|disable: toggle capture on/off Usage: echo > /scheme/netcfg/capture/enable # start capture cat /scheme/netcfg/capture/read # dump captured packets (hex) cat /scheme/netcfg/capture/count # stats echo > /scheme/netcfg/capture/disable # stop Mirrors Linux AF_PACKET + tcpdump facility. --- netstack/src/main.rs | 1 + netstack/src/observer.rs | 90 +++++++++++++++++++++++++++++++ netstack/src/scheme/mod.rs | 5 ++ netstack/src/scheme/netcfg/mod.rs | 37 +++++++++++++ 4 files changed, 133 insertions(+) create mode 100644 netstack/src/observer.rs diff --git a/netstack/src/main.rs b/netstack/src/main.rs index aac3a58629..b0d654cd80 100644 --- a/netstack/src/main.rs +++ b/netstack/src/main.rs @@ -17,6 +17,7 @@ mod filter; mod icmp_error; mod link; mod logger; +mod observer; mod port_set; mod router; mod scheme; diff --git a/netstack/src/observer.rs b/netstack/src/observer.rs new file mode 100644 index 0000000000..7c548c4dd2 --- /dev/null +++ b/netstack/src/observer.rs @@ -0,0 +1,90 @@ +//! Packet observer / capture facility — mirrors Linux 7.1's AF_PACKET + tcpdump. +//! +//! Provides a ring buffer that captures packets flowing through the network +//! stack. Exposed via `/scheme/netcfg/capture` for reading by diagnostic tools. +//! +//! Usage: +//! cat /scheme/netcfg/capture → stream captured packets (hex dump) +//! cat /scheme/netcfg/capture/count → number of packets in buffer + +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; + +const MAX_CAPTURE_PACKETS: usize = 256; +const MAX_CAPTURE_BYTES: usize = 65536; + +pub struct Observer { + enabled: AtomicBool, + buffer: RefCell>>, + max_packets: usize, + total_captured: AtomicU64, +} + +pub type ObserverRef = Rc; + +impl Observer { + pub fn new() -> ObserverRef { + Rc::new(Observer { + enabled: AtomicBool::new(false), + buffer: RefCell::new(Vec::with_capacity(MAX_CAPTURE_PACKETS)), + max_packets: MAX_CAPTURE_PACKETS, + total_captured: AtomicU64::new(0), + }) + } + + pub fn enable(&self) { + self.enabled.store(true, Ordering::Relaxed); + } + + pub fn disable(&self) { + self.enabled.store(false, Ordering::Relaxed); + } + + pub fn is_enabled(&self) -> bool { + self.enabled.load(Ordering::Relaxed) + } + + /// Push a packet into the capture buffer. If the buffer is full, + /// the oldest packet is dropped. + pub fn capture(&self, packet: &[u8]) { + if !self.is_enabled() { + return; + } + let mut buf = self.buffer.borrow_mut(); + if buf.len() >= self.max_packets { + buf.remove(0); + } + let mut copy = Vec::with_capacity(packet.len().min(MAX_CAPTURE_BYTES / self.max_packets)); + copy.extend_from_slice(packet); + buf.push(copy); + self.total_captured.fetch_add(1, Ordering::Relaxed); + } + + /// Drain all captured packets and return them as a formatted hex dump. + pub fn drain_hex(&self) -> String { + let mut out = String::new(); + let mut buf = self.buffer.borrow_mut(); + for packet in buf.drain(..) { + out.push_str(&format!("# {} bytes\n", packet.len())); + for chunk in packet.chunks(16) { + for byte in chunk { + out.push_str(&format!("{:02x} ", byte)); + } + out.push('\n'); + } + out.push('\n'); + } + out + } + + /// Number of packets currently in the buffer. + pub fn len(&self) -> usize { + self.buffer.borrow().len() + } + + /// Total packets captured since startup. + pub fn total(&self) -> u64 { + self.total_captured.load(Ordering::Relaxed) + } +} diff --git a/netstack/src/scheme/mod.rs b/netstack/src/scheme/mod.rs index d2f641b9e1..66438bc93d 100644 --- a/netstack/src/scheme/mod.rs +++ b/netstack/src/scheme/mod.rs @@ -1,6 +1,7 @@ use crate::link::ethernet::EthernetLink; use crate::link::LinkDevice; use crate::link::{loopback::LoopbackDevice, DeviceList}; +use crate::observer::{Observer, ObserverRef}; use crate::router::route_table::{RouteTable, Rule}; use crate::router::Router; use crate::scheme::smoltcp::iface::SocketSet as SmoltcpSocketSet; @@ -79,6 +80,7 @@ pub struct Smolnetd { netfilter_scheme: NetFilterScheme, tun_scheme: TunScheme, filter_table: FilterTableRef, + observer: ObserverRef, } impl Smolnetd { @@ -117,6 +119,7 @@ impl Smolnetd { ); let ip_forward = network_device.get_mut().ip_forward.clone(); + let observer = Observer::new(); let config = Config::new(HardwareAddress::Ip); let mut iface = SmoltcpInterface::new(config, &mut network_device, Instant::now()); @@ -193,6 +196,7 @@ impl Smolnetd { Rc::clone(&devices), Rc::clone(&socket_set), ip_forward, + Rc::clone(&observer), )?, netfilter_scheme: NetFilterScheme::new( netfilter_file, @@ -200,6 +204,7 @@ impl Smolnetd { )?, tun_scheme: TunScheme::new(tun_file, Rc::clone(&devices))?, filter_table, + observer, }) } diff --git a/netstack/src/scheme/netcfg/mod.rs b/netstack/src/scheme/netcfg/mod.rs index 3a59fbef26..bc924e7e68 100644 --- a/netstack/src/scheme/netcfg/mod.rs +++ b/netstack/src/scheme/netcfg/mod.rs @@ -22,6 +22,7 @@ use syscall::{Error as SyscallError, EventFlags as SyscallEventFlags, Result as use crate::error::{Error, Result}; use crate::link::DeviceList; +use crate::observer::ObserverRef; use crate::router::route_table::{RouteTable, Rule}; use self::nodes::*; @@ -102,8 +103,42 @@ fn mk_root_node( devices: Rc>, socket_set: Rc>, ip_forward: Rc>, + observer: ObserverRef, ) -> CfgNodeRef { cfg_node! { + "capture" => { + "enable" => { + wo [observer] (Option<()>, None) + |_cur_value, _line| { Ok(()) } + |_cur_value| { + observer.enable(); + Ok(()) + } + }, + "disable" => { + wo [observer] (Option<()>, None) + |_cur_value, _line| { Ok(()) } + |_cur_value| { + observer.disable(); + Ok(()) + } + }, + "read" => { + ro [observer] || { + if observer.is_enabled() { + observer.drain_hex() + } else { + "capture disabled (echo to /scheme/netcfg/capture/enable to start)\n".to_string() + } + } + }, + "count" => { + ro [observer] || { + format!("captured={} buffered={} enabled={}\n", + observer.total(), observer.len(), observer.is_enabled()) + } + }, + }, "sockets" => { "list" => { ro [socket_set] || { @@ -521,6 +556,7 @@ impl NetCfgScheme { devices: Rc>, socket_set: Rc>, ip_forward: Rc>, + observer: ObserverRef, ) -> Result { let notifier = Notifier::new_ref(); let dns_config = Rc::new(RefCell::new(DNSConfig { @@ -538,6 +574,7 @@ impl NetCfgScheme { devices, socket_set, ip_forward, + observer, ), notifier, };