From 917cda18e5788eaa6bcd791f1e21c9a2c127d28c Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Thu, 9 Jul 2026 11:04:25 +0300 Subject: [PATCH] nat: active SNAT binding tracking + display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NatTable gains bindings: Vec field. record_snat() called after successful SNAT rewrite captures original→translated src_addr/port mapping. Bindings capped at 1024 entries (FIFO). format_bindings() returns: Active SNAT bindings: 3 10.0.0.1:1234 -> 192.168.1.100:40001 10.0.0.2:5678 -> 192.168.1.100:40002 Useful for diagnosing NAT issues and seeing which internal connections are being source-NAT'd. Mirrors Linux /proc/net/ip_conntrack display. All 31 tests pass. --- netstack/src/filter/nat.rs | 41 ++++++++++++++++++++++++++++++++++++++ netstack/src/router/mod.rs | 9 ++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/netstack/src/filter/nat.rs b/netstack/src/filter/nat.rs index b0e5e0a8ed..b56bcd63e3 100644 --- a/netstack/src/filter/nat.rs +++ b/netstack/src/filter/nat.rs @@ -70,6 +70,7 @@ impl Default for NatBinding { #[derive(Debug)] pub struct NatTable { pub rules: Vec, + pub bindings: Vec, next_id: u32, ephemeral_port: u16, } @@ -78,6 +79,7 @@ impl NatTable { pub fn new() -> Self { Self { rules: Vec::new(), + bindings: Vec::new(), next_id: 1, ephemeral_port: 40000, } @@ -117,6 +119,31 @@ impl NatTable { None } + /// Record an active SNAT binding for display purposes. + /// Called after a successful SNAT rewrite. + pub fn record_snat( + &mut self, + orig_src: IpAddress, + trans_src: IpAddress, + orig_sport: u16, + trans_sport: u16, + ) { + // Cap bindings to prevent unbounded growth. + if self.bindings.len() >= 1024 { + self.bindings.remove(0); + } + self.bindings.push(NatBinding { + orig_src, + trans_src, + orig_dst: IpAddress::v4(0, 0, 0, 0), + trans_dst: IpAddress::v4(0, 0, 0, 0), + orig_sport, + trans_sport, + orig_dport: 0, + trans_dport: 0, + }); + } + pub fn lookup_dnat( &self, hook: Hook, @@ -162,6 +189,20 @@ impl NatTable { out } + pub fn format_bindings(&self) -> String { + if self.bindings.is_empty() { + return "no active bindings\n".to_string(); + } + let mut out = format!("Active SNAT bindings: {}\n", self.bindings.len()); + for b in &self.bindings { + out.push_str(&alloc::format!( + " {}:{} -> {}:{}\n", + b.orig_src, b.orig_sport, b.trans_src, b.trans_sport, + )); + } + out + } + pub fn remove(&mut self, id: u32) -> bool { if let Some(idx) = self.rules.iter().position(|r| r.id == id) { self.rules.remove(idx); diff --git a/netstack/src/router/mod.rs b/netstack/src/router/mod.rs index 884b7e6091..476d5a6642 100644 --- a/netstack/src/router/mod.rs +++ b/netstack/src/router/mod.rs @@ -157,13 +157,20 @@ impl Router { debug!("filter: dropped FORWARD packet"); continue; } - if let Some((trans_addr, _)) = table.nat_table.lookup_snat( + if let Some((trans_addr, trans_port)) = table.nat_table.lookup_snat( Hook::Forward, IpAddress::Ipv4(ipv4.src_addr()), dst, ) { let IpAddress::Ipv4(new_src) = trans_addr else { continue; }; let _ = crate::filter::rewrite_src_ipv4(&mut buf, new_src); + if let Some(port) = trans_port { + table.nat_table.record_snat( + IpAddress::Ipv4(ipv4.src_addr()), + IpAddress::Ipv4(new_src), + 0, port, + ); + } } }