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.
This commit is contained in:
Red Bear OS
2026-07-08 16:12:15 +03:00
parent f6313ae5c4
commit ff66b96266
4 changed files with 133 additions and 0 deletions
+1
View File
@@ -17,6 +17,7 @@ mod filter;
mod icmp_error;
mod link;
mod logger;
mod observer;
mod port_set;
mod router;
mod scheme;
+90
View File
@@ -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<Vec<Vec<u8>>>,
max_packets: usize,
total_captured: AtomicU64,
}
pub type ObserverRef = Rc<Observer>;
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)
}
}
+5
View File
@@ -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,
})
}
+37
View File
@@ -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<RefCell<DeviceList>>,
socket_set: Rc<RefCell<SocketSet>>,
ip_forward: Rc<Cell<bool>>,
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<RefCell<DeviceList>>,
socket_set: Rc<RefCell<SocketSet>>,
ip_forward: Rc<Cell<bool>>,
observer: ObserverRef,
) -> Result<NetCfgScheme> {
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,
};