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.
CRITICAL BUGS FIXED:
1. router/mod.rs: ICMP errors for Unreachable/Prohibit went to rx_buffer
(infinite loop back to input) instead of tx_buffer (sent to source).
Combined Unreachable/Prohibit arms; both now use tx_buffer.
2. scheme/socket.rs dup(): refcount leak in update_with branch.
OLD code: new_handle.socket_handle() got +2 when update_with was
Some, but only decremented once on close. Net: SH1 over-counted (+1),
SH2 (new listening socket from update_with) never tracked at all.
FIX: in update_with branch, increment refcount of 'socket_handle'
(the new SH), not new_handle.socket_handle(). The always-run
increment at the bottom covers new_handle. Both increments serve
different purposes and are now distinct.
3. scheme/udp.rs: 4 .expect() panic vectors in bind/send/recv.
'Can't bind', 'Can't send', 'Can't receive', 'Can't recieve' all
panicked the daemon. Now return EIO via ? operator.
4. link/vlan.rs (and vxlan/gre/ipip already partially fixed in R42):
send() pushed tagged packets into self.recv_queue, creating a
self-loop where packets were never delivered. Now drops packets
with a debug log since no parent device reference exists.
5. link/qdisc.rs: TokenBucket token_add could overflow u64 on long
elapsed durations. Changed to saturating_mul.
DOC FIX:
6. filter/table.rs docstring example used --sport 1024:65535 (port
range) but parse_port only accepts single port. Changed example to
use single port value. Range support is a future enhancement.
All 29 existing tests still pass.
CRITICAL BUGS FIXED:
1. STP build_bpdu runtime panic (link/stp.rs:121-123)
- Duration::total_millis() returns i64, to_be_bytes() = [u8; 8]
- But destination buffers (buf[29..31], buf[31..33], buf[33..35]) are 2 bytes
- copy_from_slice panics on length mismatch
- Triggered by every root bridge hello BPDU — daemon crash
- Fix: convert to ticks (1s = 256 ticks per IEEE 802.1D) as u16
2. SLAAC PIO off-by-2 byte parsing (slaac.rs:155-180)
- parse_router_advertisement used opt_data[2] for prefix length
- After pos += 2 consumed type+length, opt_data[0] is the prefix length
- All PIO fields were off by 2 — SLAAC got wrong prefix length, flags,
valid_lifetime (read Preferred Lifetime), preferred_lifetime (read Reserved2)
- Fix: use opt_data[0] for prefix length, [1] for flags, [2..6] for valid,
[6..10] for preferred, [14..30] for prefix bytes
3. ICMPv4 error wrong IHL extraction (icmp_error.rs:25, 62)
- (ipv4.version() & 0x0f) * 4 always computed 16
- smoltcp's version() returns 4 (version), not combined byte
- Fix: use ipv4.header_len()
4. UDP can_recv panic on port-only endpoint (scheme/udp.rs:54)
- data.addr.unwrap() panics if addr is None (e.g. udp/:53)
- is_specified() returns true when port is non-zero, even if addr is None
- Fix: use let-else pattern, accept all packets if addr is None
5. TCP .expect() calls crash daemon (scheme/tcp.rs)
- 5 .expect() calls in connect/listen/send/recv paths
- Any socket error panics the entire netstack daemon
- Fix: replace with .map_err() returning EIO
6. ICMP .unwrap() on malformed packets (scheme/icmp.rs:217-220)
- recv().expect() panics, Icmpv4Repr::parse().unwrap() panics
- Crafted/malformed ICMP packets would crash the daemon
- Fix: use match, drop unparseable packets
NEW TESTS (6 added, 29 total now passing):
- icmp_error::icmpv4_short_packet_returns_none
- icmp_error::icmpv4_preserves_destination_address (regression for bug 3)
- icmp_error::icmpv4_with_ip_options_includes_extended_header
- slaac::ra_with_pio_64_parses_correctly (regression for bug 2)
- link::stp::bpdu_minimal_parses
- link::stp::bpdu_short_returns_none
- link::stp::bpdu_wrong_protocol_returns_none
- link::stp::build_bpdu_does_not_panic (regression for bug 1)
The SLAAC test would have failed before the off-by-2 fix.
The STP build_bpdu test would have panicked before the ticks fix.
The ICMP tests verify the full IP header (incl. IHL=6 options) is preserved.
Bridge MAC learning tests:
- learn_stores_unicast_mapping: stored MAC resolves on correct port
- learn_ignores_multicast: broadcast/multicast never enters FDB
- learn_replaces_existing_entry_on_new_port: MAC moves update port
- age_entries_removes_expired_macs: 300s timeout respected
- lookup_returns_none_for_unknown_mac: unknown MAC returns None
Total tests across netstack: 20 (5 table + 4 conntrack + 6 nat + 5 bridge)
All passing.
LinkDevice trait gains is_promiscuous() and set_promiscuous().
EthernetLink stores promiscuous flag (default: false).
Receive path bypasses MAC check when promiscuous enabled —
captures all frames regardless of destination MAC.
netcfg/ifaces/eth0/promiscuous rw:
cat /scheme/netcfg/ifaces/eth0/promiscuous → 'off' or 'on'
echo on > .../promiscuous → enable promiscuous mode
echo off > .../promiscuous → disable
Accepts: on/off/1/0/yes/no/true/false.
Mirrors Linux 'ip link set dev promisc on/off'.
Required for packet capture tools that need to see all traffic.
All 4 RFC 1213 counters now wired to real events:
- rx_errors: malformed Ethernet frame or ARP packet
- tx_errors: network_file.write_all() failed
- rx_dropped: ARP/NDP neighbor resolution failed or queue full
- tx_dropped: qdisc returned None (rate-limit kicked in)
Stats output now reflects actual error/drop conditions.
Mirrors Linux's ifconfig/sar 'errors' and 'dropped' fields.
The new rx_dropped counter is now incremented in real drop paths:
- drop_waiting_packets_v4: ARP/NDP failed after MAX_TRIES, packets dropped
- drop_waiting_packets_v6: same for IPv6 neighbor discovery
- handle_missing_neighbor: waiting queue full, incoming packet dropped
Net effect: stats now reflect actual packet loss conditions visible
via /scheme/netcfg/ifaces/eth0/stats:
rx_dropped=N (was always 0 before)
tx_dropped was already incremented by qdisc drops (R24).
LinkDevice trait gains is_enabled() and set_enabled() with defaults.
EthernetLink stores enabled flag (default: true). When disabled,
link_state() reports 'down' regardless of hardware_address presence.
netcfg/ifaces/eth0/enabled is rw:
cat /scheme/netcfg/ifaces/eth0/enabled → 'up' or 'down'
echo up > .../enabled → bring interface up
echo down > .../enabled → bring interface down
echo on/off/1/0/yes/no → also accepted
Mirrors Linux 'ip link set dev <state>'.
Pre-existing link state (hardware_address Some/None) unchanged;
the enabled flag adds a software override layer that can be
toggled without restarting the netstack.
Bridge: statistics() aggregates rx/tx bytes+packets from all member ports.
arp_stats() delegates to each port, showing per-port breakdown.
Bond: statistics() aggregates from all slaves (active+standby).
arp_stats() delegates to each slave.
TUN: statistics() now returns live counters tracked during send()/recv().
rx_bytes, rx_packets, tx_bytes, tx_packets increment per-packet.
Previously these devices returned Stats::default() (all zeros).
Now netcfg /ifaces/*/stats shows real data for bridge, bond, tun.
BridgeDevice STP hardening:
- send(): check STP blocking before unicast forwarding (host-originated)
- recv(): check STP blocking before unicast forwarding (switched frames)
- Previously only flood() checked STP; unicast forwarding bypassed it.
A blocked port must never forward any traffic — STP semantics now correct.
netcfg stats enhancement:
- Per-interface stats now include mtu= and link= fields alongside counters
- Applies to both eth0 and loopback
- Enables bandwidth monitoring tools to self-discover MTU/link state