Files
RedBear-OS/netstack/src/observer.rs
T
Red Bear OS 3101345fe6 review: NDP state corruption fix, port double-free fix, observer limits
CRITICAL/MEDIUM BUGS FIXED:

1. ethernet.rs send_ndp_solicit: state corruption via recursive call
   The function destructured self.ndp_state by value (target, tries,
   silent_until) at the start and wrote back at the end. But between
   destructuring and writing back, self.drop_waiting_packets_v6(target)
   could recursively call self.send_ndp_solicit(Instant::ZERO) for a
   different target. The recursive call updated self.ndp_state with the
   new target's state. When control returned to the original frame, the
   original write-back clobbered the recursive call's state, silently
   losing neighbor discovery for the new target.
   Fix: use scoped pattern matches to read target/tries/silent_until
   from the live state right before they are needed, so the recursive
   call's writes are preserved. This matches the pattern used by
   send_arp (which uses ref mut and is correct).

2. scheme/socket.rs on_close: port double-free
   close_file() was called before the refcount check, so the port was
   released on every close. For a dup'd socket, the second close
   tried to release the port again — double-free.
   Fix: compute the new refcount first, only call close_file and
   remove from socket_set when the count reaches 0 (last reference).

3. scheme/tcp.rs new_socket: port + socket leak on connect failure
   If get_port() succeeded but connect() later failed, the port was
   claimed and the socket was added to socket_set, but new_socket
   returned Err. The caller (open_inner) did not insert the file
   handle, so on_close was never called. Both the port and the socket
   slot leaked.
   Fix: when connect() fails, release the auto-allocated port before
   returning. Explicit user-provided ports are still released by
   on_close when the last file is dropped (preserves the existing
   on_close-based release).

4. observer.rs capture: per-packet size limit not enforced
   Vec::with_capacity only pre-allocates; extend_from_slice copies the
   full packet. The intended per-packet limit (MAX_CAPTURE_BYTES /
   max_packets = 256 bytes) was being bypassed, allowing a single
   1500-byte packet to consume the full capture buffer.
   Fix: truncate to per_packet_limit before extending.

5. observer.rs capture: short packets bypassed filter
   Packets < 20 bytes returned true (capture anyway) regardless of
   the user's filter. A filter like 'tcp port 80' would capture all
   short packets, including non-TCP.
   Fix: return false (no match) for short packets. Filter semantics
   are now consistent: if a packet can't be matched, it's not captured.

All 29 existing tests still pass.
2026-07-09 00:42:46 +03:00

184 lines
5.7 KiB
Rust

//! 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/read → drain captured packets (hex dump)
//! cat /scheme/netcfg/capture/count → stats
//! echo tcp > /scheme/netcfg/capture/filter → capture only TCP
//! echo > /scheme/netcfg/capture/enable → start
//! echo > /scheme/netcfg/capture/disable → stop
//! echo tcp port 80 > /scheme/netcfg/capture/filter → TCP port 80 only
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>>>,
filter: RefCell<CaptureFilter>,
max_packets: usize,
total_captured: AtomicU64,
}
#[derive(Debug, Clone)]
struct CaptureFilter {
proto: Option<u8>,
port: Option<u16>,
}
impl CaptureFilter {
fn new() -> Self {
Self { proto: None, port: None }
}
fn matches(&self, packet: &[u8]) -> bool {
if self.proto.is_none() && self.port.is_none() {
return true;
}
if packet.len() < 20 {
return false; // too short to determine protocol/port
}
let version = packet[0] >> 4;
let (proto, src_port_offset, dst_port_offset) = match version {
4 => {
if packet.len() < 24 { return true; }
let ihl = (packet[0] & 0x0f) as usize * 4;
if packet.len() < ihl + 4 { return true; }
(packet[9], ihl, ihl + 2)
}
6 => {
if packet.len() < 48 { return true; }
(packet[6], 40, 42)
}
_ => return true,
};
if let Some(p) = self.proto {
if proto != p { return false; }
}
if let Some(port) = self.port {
let sp = u16::from_be_bytes([packet[src_port_offset], packet[src_port_offset + 1]]);
let dp = u16::from_be_bytes([packet[dst_port_offset], packet[dst_port_offset + 1]]);
if sp != port && dp != port { return false; }
}
true
}
fn from_str(s: &str) -> Self {
let mut filter = Self::new();
let lower = s.trim().to_lowercase();
let parts: Vec<&str> = lower.split_whitespace().collect();
let mut i = 0;
while i < parts.len() {
match parts[i] {
"tcp" => filter.proto = Some(6),
"udp" => filter.proto = Some(17),
"icmp" => filter.proto = Some(1),
"port" if i + 1 < parts.len() => {
if let Ok(p) = parts[i + 1].parse::<u16>() {
filter.port = Some(p);
i += 1;
}
}
_ => {}
}
i += 1;
}
filter
}
}
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)),
filter: RefCell::new(CaptureFilter::new()),
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)
}
pub fn set_filter(&self, filter_str: &str) {
*self.filter.borrow_mut() = CaptureFilter::from_str(filter_str);
}
pub fn filter_str(&self) -> String {
let f = self.filter.borrow();
let mut s = String::new();
if let Some(p) = f.proto {
s.push_str(match p { 6 => "tcp", 17 => "udp", 1 => "icmp", _ => "?" });
}
if let Some(port) = f.port {
if !s.is_empty() { s.push(' '); }
s.push_str(&format!("port {}", port));
}
if s.is_empty() { s.push_str("any"); }
s
}
pub fn capture(&self, packet: &[u8]) {
if !self.is_enabled() {
return;
}
if !self.filter.borrow().matches(packet) {
return;
}
let mut buf = self.buffer.borrow_mut();
if buf.len() >= self.max_packets {
buf.remove(0);
}
// Truncate to per-packet size limit so a single large packet
// can't exhaust the total capture buffer.
let per_packet_limit = MAX_CAPTURE_BYTES / self.max_packets.max(1);
let truncated_len = packet.len().min(per_packet_limit);
let mut copy = Vec::with_capacity(truncated_len);
copy.extend_from_slice(&packet[..truncated_len]);
buf.push(copy);
self.total_captured.fetch_add(1, Ordering::Relaxed);
}
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
}
pub fn len(&self) -> usize {
self.buffer.borrow().len()
}
pub fn total(&self) -> u64 {
self.total_captured.load(Ordering::Relaxed)
}
}