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:
@@ -17,6 +17,7 @@ mod filter;
|
||||
mod icmp_error;
|
||||
mod link;
|
||||
mod logger;
|
||||
mod observer;
|
||||
mod port_set;
|
||||
mod router;
|
||||
mod scheme;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user