filter: add unit tests + chain_summary method

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.
This commit is contained in:
Red Bear OS
2026-07-08 19:47:47 +03:00
parent ecc0ef33de
commit 579a512545
+120
View File
@@ -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<u16, ParseError> {
port_str
.parse::<u16>()
.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");
}
}