3101345fe6
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.
184 lines
5.7 KiB
Rust
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)
|
|
}
|
|
}
|