nat: active SNAT binding tracking + display

NatTable gains bindings: Vec<NatBinding> 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.
This commit is contained in:
Red Bear OS
2026-07-09 11:04:25 +03:00
parent e818909712
commit 917cda18e5
2 changed files with 49 additions and 1 deletions
+41
View File
@@ -70,6 +70,7 @@ impl Default for NatBinding {
#[derive(Debug)]
pub struct NatTable {
pub rules: Vec<NatRule>,
pub bindings: Vec<NatBinding>,
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);
+8 -1
View File
@@ -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,
);
}
}
}