CRITICAL BUGS FIXED:
1. Smolnetd startup panic on missing/malformed ip_router cfg (scheme/mod.rs:111-112)
- getcfg returned Option but was being unwrapped: getcfg('ip_router').unwrap()
- .expect('Can't parse the ip_router cfg.') on malformed IP would panic
- Fix: match on Result, fall back to 0.0.0.0 with warning log
2. Smolnetd default route panic on 0.0.0.0 gateway (scheme/mod.rs:140-142)
- iface.routes_mut().add_default_ipv4_route(0.0.0.0).expect(...) panics
- smoltcp rejects default route with 0.0.0.0 as gateway
- Fix: skip route addition when gateway is unspecified
3. TUN event loop destroyed data (scheme/tun.rs:119-133)
- Loop moved packets from dev.tx to dev.rx - stealing data userspace was
supposed to read (since dev.tx is aliased to device_rx)
- Fix: only clear stale packets from dev.tx; data transfer between
userspace and network is via TunDevice::recv() called by poller
4. ip_forward sysctl broke two-phase write/commit pattern (netcfg/mod.rs:367-389)
- write_line closure immediately called ip_forward.set()
- Inconsistent with other writable nodes
- Fix: write_line stores value in cur_value, commit applies it
5. ICMP Udp socket was non-functional (scheme/icmp.rs:217-243)
- Old code only handled EchoReply, dropped all other ICMP types
- Udp variant (IP_RECVERR-style error notification) returned nothing
- Fix: split read_buf by socket_type. Echo still only matches EchoReply.
Udp now serializes ICMP error type+code+original IP into the read buffer.
SEMANTIC BUGS FIXED:
6. UDP connected local_addr resolution was inverted (scheme/udp.rs:154-167)
- Some(specific) fell into _ branch, doing route lookup instead of using
the user's specified address
- Fix: Some(specific) returns the address directly, only None or
Some(0.0.0.0) trigger route lookup
7. claim_port_reuse lacked documentation (port_set.rs:50-53)
- Always returned true, but semantics of why it never fails was unclear
- Fix: doc comment explains the two-phase collision check (claim_port
first, claim_port_reuse only on SO_REUSEADDR path)
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.
CRITICAL BUG #1: is_syn() IPv4-only
conntrack.rs is_syn() hardcoded offset 33 (IPv4: 20+13).
For IPv6, TCP flags are at offset 53 (40+13). IPv6 SYN packets
were never detected as SYNs, so SYN flood rate limiting was
completely broken for IPv6 traffic.
Fix: pass l3num, compute tcp_offset correctly for both families.
Added ipv6_syn_detection_works regression test with IPv6 SYN packet
at offset 53 — verifies the fix.
CRITICAL BUG #2: netfilter writes all broken
open_path stored paths without leading '/' (e.g. 'rule/add')
but commit() compared against '/rule/add' (with leading '/').
Result: ALL write operations silently failed with EINVAL:
- rule/add - never added rules
- nat/add - never added NAT
- rule/del - never deleted
- nat/del - never deleted
- policy/X - never set policy
- reset - never reset
- counters/reset - never reset
Fix: align commit() paths with the stored convention (no '/').
Removed dead /rule/del/<id> REST-style path (no open_path arm existed).
Updated stored path convention comment.
Tests: 21 total (added ipv6_syn_detection_works), all passing.
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.
CRITICAL BUG FIX: is_echo_request() and track_icmp() were reading
ctx.packet[0] thinking it was the ICMP type byte. But ctx.packet is
the IP packet, so offset 0 is the IP version/IHL byte (0x45 for IPv4).
Result: ICMP echo detection NEVER matched Type 8 packets.
- ICMP rate limiting (R32) was a no-op — wrong byte read
- ICMP echo tracking in track_icmp() was checking IP version vs 8/128
Fixed: read ICMP type from offset for IPv4, or offset 40 for IPv6.
ALSO: separate TCP SYN and ICMP echo rate limit maps.
Previously both shared one BTreeMap, so a host doing 100 TCP SYNs
+ 1 ICMP echo would be over-limit. Now each has its own budget.
NEW TESTS (4 conntrack + 5 filter table = 9 total, all passing):
- syn_limit_triggers_after_threshold
- icmp_limit_triggers_after_threshold
- syn_and_icmp_have_independent_budgets
- non_syn_tcp_does_not_count_against_syn_limit
These tests caught the ICMP type bug — failed before fix, pass after.
5 new unit tests in filter::table::tests:
- drop_rule_actually_drops (R33 bug regression test)
- accept_rule_actually_accepts (R33 bug regression test)
- rule_matching_other_hook_does_not_apply (hook isolation)
- default_policy_applied_when_no_rules (fallback)
- reset_counters_clears_metrics_keeps_rules (R34 method coverage)
All 5 tests pass — proves the critical verdict bug from R33
is fixed and reset_counters from R34 works correctly.
Also adds FilterTable::chain_summary() returning:
'input: pkts=12 bytes=5678 policy=ACCEPT' (per-chain line)
for use by future netcfg summary extension.
FilterTable::evaluate() set final_verdict for matched rules but never
returned it — always returned the chain's default_policy instead.
This meant every firewall rule (ACCEPT/DROP/REJECT/LOG) was being
evaluated (correctly) but its verdict was DISCARDED.
Symptoms:
- 'DROP' rules had no effect — packets flowed through
- 'ACCEPT' rules had no effect — packets flowed through
- Only default_policy was ever applied
Fix: return final_verdict.unwrap_or(default_policy).
Now 'iptables-style' rules actually take effect:
- DROP rules block traffic
- ACCEPT rules allow traffic (before default)
- REJECT rules send ICMP unreachable
- LOG rules log and continue
This bug was introduced when the filter evaluation logic was
refactored and the return statement was left pointing at the
default policy instead of the verdict from the rule loop.
Confirmed via cargo build; warning 'value assigned to final_verdict
is never read' is now gone.
Conntrack now rate-limits ICMP/ICMPv6 echo requests per source:
- is_echo_request() detects Type 8 (ICMPv4) / Type 128 (ICMPv6)
- check_icmp_limit() enforces 20 echoes/sec per source IP
- Over-limit returns ConnState::OverLimit, increments over_limit_count
Mirrors existing SYN flood protection (100 SYN/sec per source).
Smaller threshold (20/sec) because ICMP is cheaper to generate.
Reuses same rate_limits map — TCP SYNs and ICMP echoes share the
budget. Side effect: 100 SYN + 20 echo = 120 packets/sec from one
source before any trigger, which is reasonable.
/scheme/netcfg/summary outputs:
interfaces: eth0=up lo=up
routes: 4
sockets: 7 (tcp=3 udp=4 icmp=0 raw=0)
ip_forward: 1
eth0 stats: rx=1234/16 tx=5678/12 err=0/0 drop=0/2
Single read aggregates:
- Link state of eth0 and loopback
- Total route count
- Per-protocol socket counts (TCP/UDP/ICMP/Raw)
- ip_forward toggle state
- Full MIB-II stats for eth0
Useful for at-a-glance monitoring without reading 6+ separate nodes.
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.
Reads the default gateway from the route table:
cat /scheme/netcfg/route/gateway
→ 'default via 192.168.1.1\n'
or 'no default route\n' if no default gateway configured.
Looks up the route for 0.0.0.0 (the default route CIDR) and
returns its 'via' address. Mirrors Linux 'ip route show default'.
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.
Router now captures packets flowing through the network stack:
- forward_packets(): capture all forwarded/local-delivered packets
- Observer injected via Router::new() from Smolnetd constructor
When /scheme/netcfg/capture/enable is written, all packets
traversing the router are captured into the ring buffer.
When disabled, zero overhead (AtomicBool check).
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.
TCP and UDP get_sock_opt/set_sock_opt had duplicate match arms
due to constant collisions (Linux-level values share namespace):
TCP: TCP_MAXSEG=2, IP_TTL=2, SO_REUSEADDR=2 → kept TCP_MAXSEG
UDP: IP_TTL=2, SO_REUSEADDR=2 → kept IP_TTL (more useful)
Removed unreachable arms with explanatory comments. The
collision is inherent — Linux uses different option levels
(SOL_SOCKET vs IPPROTO_TCP vs IPPROTO_IP) but Redox scheme
has a flat namespace. Applications that use multi-level
getsockopt() would need richer level dispatching.
Router gains Rc<Cell<bool>> ip_forward flag (default: true).
When false, forward_packets() returns immediately — no packets forwarded
between interfaces. Security best practice for non-router hosts.
netcfg scheme gains sysctl subtree:
/scheme/netcfg/sysctl/net/ipv4/ip_forward (rw: 0 or 1)
Read: echo /scheme/netcfg/sysctl/net/ipv4/ip_forward → 1
Write: echo 0 > /scheme/netcfg/sysctl/net/ipv4/ip_forward (disable)
Restore: echo 1 > /scheme/netcfg/sysctl/net/ipv4/ip_forward
Mirrors Linux /proc/sys/net/ipv4/ip_forward.
Shared via Rc<Cell<bool>> between Router and NetCfgScheme.
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
- UDP port allocation now falls back to claim_port_reuse() (SO_REUSEADDR)
- SO_REUSEADDR get/set added to both UDP and TCP schemes
- SO_BROADCAST getter added to UDP (always returns 1)
- IP_TTL getter/setter added to UDP (get/set hop_limit)
- TCP: SO_REUSEADDR get/set added for API completeness
- All new options return known values for application compatibility
- Revert initfs/rootfs service types from oneshot_async to blocking
Scheme/Notify/Oneshot to fix init ordering races.
- Add /scheme/pci retry loop in pcid-spawner.
- Bump redox_event to 0.4.8; use local paths for redox-ioctl and redox-rt.
- Regenerate Cargo.lock / bootstrap/Cargo.lock with only local forks.
- Update submodule origin from redbear-os-base.git to RedBear-OS.git
branch submodule/base per single-repo policy.