conntrack: per-protocol and per-state statistics

ConntrackTable gains stats() method returning per-protocol breakdown:
  tcp_entries: N (est=N syn=N syn_recv=N fin=N tw=N close=N)
  udp_entries: N
  icmp_entries: N
  over_limit: N
  total_entries: N

TCP states counted: None, SynSent, SynRecv, Established, FinWait,
TimeWait, Close. Mirrors Linux /proc/net/stat/nf_conntrack.

Exposed via three channels:
- /scheme/netfilter/conntrack/stats  (new dedicated node)
- /scheme/netfilter/stats (existing, now includes per-state)
- /scheme/netcfg/summary (includes conntrack stats block)

All 31 tests pass.
This commit is contained in:
Red Bear OS
2026-07-09 01:24:16 +03:00
parent b27515d583
commit b0e3f8e9ad
3 changed files with 60 additions and 0 deletions
+49
View File
@@ -440,6 +440,55 @@ impl ConntrackTable {
self.entries.len()
}
/// Per-protocol and per-state connection breakdown.
/// Returns lines like:
/// tcp_entries: N (est=N syn=N syn_recv=N fin=N tw=N close=N)
/// udp_entries: N
/// icmp_entries: N
/// over_limit: N
/// total_entries: N
pub fn stats(&self) -> alloc::string::String {
let mut tcp = 0u32;
let mut tcp_est = 0u32;
let mut tcp_syn = 0u32;
let mut tcp_syn_recv = 0u32;
let mut tcp_fin = 0u32;
let mut tcp_tw = 0u32;
let mut tcp_close = 0u32;
let mut udp = 0u32;
let mut icmp = 0u32;
for entry in self.entries.values() {
match entry.key.l4proto {
6 => {
tcp += 1;
match entry.tcp_state {
TcpTracking::None => {}
TcpTracking::SynSent => tcp_syn += 1,
TcpTracking::SynRecv => tcp_syn_recv += 1,
TcpTracking::Established => tcp_est += 1,
TcpTracking::FinWait => tcp_fin += 1,
TcpTracking::TimeWait => tcp_tw += 1,
TcpTracking::Close => tcp_close += 1,
}
}
17 => udp += 1,
1 | 58 => icmp += 1,
_ => {}
}
}
let mut out = alloc::format!(
"tcp_entries: {} (est={} syn={} syn_recv={} fin={} tw={} close={})\n",
tcp, tcp_est, tcp_syn, tcp_syn_recv, tcp_fin, tcp_tw, tcp_close
);
out.push_str(&alloc::format!("udp_entries: {}\n", udp));
out.push_str(&alloc::format!("icmp_entries: {}\n", icmp));
out.push_str(&alloc::format!("over_limit: {}\n", self.over_limit_count));
out.push_str(&alloc::format!("total_entries: {}\n", self.entries.len()));
out
}
fn check_syn_limit(&mut self, src: IpAddress, now: Instant) -> bool {
const SYN_LIMIT: u32 = 100;
const SYN_WINDOW: Duration = Duration::from_secs(1);
+4
View File
@@ -140,6 +140,10 @@ fn mk_root_node(
}
out.push_str("filter chains:\n");
out.push_str(&filter_table.borrow().chain_summary());
if let Some(ref ct) = filter_table.borrow().conntrack {
out.push_str("conntrack stats:\n");
out.push_str(&ct.stats());
}
out
}
},
+7
View File
@@ -173,6 +173,12 @@ impl NetFilterSchemeInner {
.unwrap_or_else(|| b"conntrack: not enabled\n".to_vec()),
true, false,
),
["conntrack", "stats"] => (
self.table.borrow().conntrack.as_ref()
.map(|ct| ct.stats().into_bytes())
.unwrap_or_else(|| b"conntrack: not enabled\n".to_vec()),
true, false,
),
["log"] => {
let buf = &self.table.borrow().log_buffer;
let mut out = String::new();
@@ -210,6 +216,7 @@ impl NetFilterSchemeInner {
table.rules.len(), snat_count, dnat_count));
if let Some(ref ct) = table.conntrack {
out.push_str(&format!("conntrack: {}\n", ct.len()));
out.push_str(&ct.stats());
}
(out.into_bytes(), true, false)
},