diff --git a/netstack/src/filter/table.rs b/netstack/src/filter/table.rs index a85aec85de..722bf3df7a 100644 --- a/netstack/src/filter/table.rs +++ b/netstack/src/filter/table.rs @@ -177,6 +177,25 @@ impl FilterTable { out } + /// Compact per-chain summary for netcfg summary node. + /// Returns lines like: + /// input: pkts=12 bytes=5678 policy=ACCEPT + pub fn chain_summary(&self) -> String { + let mut out = String::new(); + for &hook in &Hook::ALL { + let (pkts, bytes) = self.chain_counters.get(&hook).copied().unwrap_or((0, 0)); + let policy = self.default_policy.get(&hook).copied().unwrap_or(Verdict::Accept); + out.push_str(&alloc::format!( + "{}: pkts={} bytes={} policy={}\n", + hook.name(), + pkts, + bytes, + policy.name() + )); + } + out + } + pub fn set_default_policy(&mut self, hook: Hook, verdict: Verdict) { self.default_policy.insert(hook, verdict); } @@ -395,4 +414,105 @@ fn parse_port(value: &str) -> core::result::Result { port_str .parse::() .map_err(|_| ParseError::BadPort(value.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::rc::Rc; + use smoltcp::wire::IpAddress; + use smoltcp::time::Instant; + + fn make_ctx(hook: Hook, src: IpAddress, dst: IpAddress) -> PacketContext<'static> { + PacketContext { + hook, + in_dev: None, + out_dev: None, + src_addr: src, + dst_addr: dst, + protocol: 6, + src_port: Some(1234), + dst_port: Some(80), + packet: &[], + } + } + + fn make_drop_rule(hook: Hook) -> FilterRule { + FilterRule { + id: 1, + hook, + src_addr: None, + src_prefix_len: 0, + dst_addr: None, + dst_prefix_len: 0, + protocol: None, + src_port: None, + dst_port: None, + in_dev: None, + out_dev: None, + state_match: None, + verdict: Verdict::Drop, + match_count: 0, + } + } + + fn make_accept_rule(hook: Hook) -> FilterRule { + let mut r = make_drop_rule(hook); + r.id = 2; + r.verdict = Verdict::Accept; + r + } + + #[test] + fn drop_rule_actually_drops() { + let mut t = FilterTable::new(); + t.rules.push(make_drop_rule(Hook::InputLocal)); + let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1)); + assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Drop, + "DROP rule must drop the packet (regression test for R33 bug fix)"); + } + + #[test] + fn accept_rule_actually_accepts() { + let mut t = FilterTable::new(); + t.rules.push(make_accept_rule(Hook::InputLocal)); + let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1)); + assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Accept, + "ACCEPT rule must accept the packet (regression test)"); + } + + #[test] + fn rule_matching_other_hook_does_not_apply() { + let mut t = FilterTable::new(); + t.rules.push(make_drop_rule(Hook::OutputLocal)); + let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1)); + assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Accept, + "Rule for OUTPUT must not affect INPUT chain — should fall through to default policy"); + } + + #[test] + fn default_policy_applied_when_no_rules() { + let mut t = FilterTable::new(); + t.set_default_policy(Hook::InputLocal, Verdict::Drop); + let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1)); + assert_eq!(t.evaluate(&ctx, Instant::from_millis(0)), Verdict::Drop, + "Default policy applies when no matching rule"); + } + + #[test] + fn reset_counters_clears_metrics_keeps_rules() { + let mut t = FilterTable::new(); + t.rules.push(make_drop_rule(Hook::InputLocal)); + let ctx = make_ctx(Hook::InputLocal, IpAddress::v4(10, 0, 0, 1), IpAddress::v4(192, 168, 1, 1)); + let _ = t.evaluate(&ctx, Instant::from_millis(0)); + assert_eq!(t.rules[0].match_count, 1); + let (_p, _b) = t.chain_counters.get(&Hook::InputLocal).copied().unwrap_or((0, 0)); + t.reset_counters(); + assert_eq!(t.rules[0].match_count, 0, + "reset_counters must clear rule.match_count"); + assert_eq!(t.chain_counters.get(&Hook::InputLocal).copied().unwrap_or((0, 0)), (0, 0), + "reset_counters must clear chain_counters"); + assert_eq!(t.rules.len(), 1, + "reset_counters must preserve rules"); + } } \ No newline at end of file