Add the missing tcp_flags(), is_fin(), is_rst() helpers and upgrade
advance_entry_state from a 3-parameter stub to a 4-parameter function
that takes &PacketContext. This completes the WIP conntrack state
machine with proper TCP tracking (SynSent, SynRecv, Established,
FinWait, TimeWait, Close) and RST/session-timeout handling.
Reference: Linux 7.x net/netfilter/nf_conntrack_proto_tcp.c
(tcp_packet, nf_conntrack_tcp_packet state machine).
parse_port now rejects port ranges ('1024:65535') with a clear
error message instead of silently using only the first number.
FilterRule uses a single u16 field and cannot represent ranges.
infer_context now documents the hardcoded 40-byte IPv6 header
limitation: smoltcp's next_header() chases extension headers to
return the final protocol, but the transport offset is not
computed. Port extraction silently returns None/None for packets
with extension headers, which is safe but means port-based
filter rules won't match for such packets.
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 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.
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.