base: apply Red Bear patches on latest upstream/main
251 files: init, acpid, ipcd, netcfg, ihdgd, virtio-gpud, scheme-utils, inputd, block driver, ptyd, ramfs, randd, initfs bootstrap, path deps, version +rb0.3.1, author attribution
This commit is contained in:
@@ -0,0 +1,858 @@
|
||||
//! Connection tracking hash table — mirrors Linux 7.1's `nf_conntrack`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `include/net/netfilter/nf_conntrack.h:74` — `struct nf_conn`
|
||||
//! - `include/net/netfilter/nf_conntrack_tuple.h` — `struct nf_conntrack_tuple`
|
||||
//! - `net/netfilter/nf_conntrack_core.c` — `resolve_normal_ct()`, `nf_conntrack_in()`
|
||||
//! - `net/netfilter/nf_conntrack_proto_tcp.c` — TCP state machine
|
||||
//! - `net/netfilter/nf_conntrack_proto_udp.c` — UDP state tracking
|
||||
//!
|
||||
//! The connection is identified by a 5-tuple (src/dst addr, src/dst port, protocol)
|
||||
//! plus the L3 protocol number. Both directions are tracked:
|
||||
//! orig: from initiator → responder
|
||||
//! reply: from responder → initiator
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::vec::Vec;
|
||||
use core::hash::{Hash, Hasher};
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::IpAddress;
|
||||
|
||||
fn is_syn(ctx: &PacketContext, l3num: u8) -> bool {
|
||||
// TCP flags byte is at offset 13 in the TCP header.
|
||||
// TCP header starts after the IP header: 20 bytes for IPv4, 40 for IPv6.
|
||||
let tcp_offset = if l3num == 4 { 20 } else { 40 };
|
||||
if ctx.packet.len() <= tcp_offset + 13 {
|
||||
return false;
|
||||
}
|
||||
let flags = ctx.packet[tcp_offset + 13];
|
||||
(flags & 0x02) != 0 && (flags & 0x10) == 0
|
||||
}
|
||||
|
||||
fn tcp_flags(ctx: &PacketContext, l3num: u8) -> u8 {
|
||||
let tcp_offset = if l3num == 4 { 20 } else { 40 };
|
||||
if ctx.packet.len() <= tcp_offset + 13 {
|
||||
return 0;
|
||||
}
|
||||
ctx.packet[tcp_offset + 13]
|
||||
}
|
||||
|
||||
fn is_fin(flags: u8) -> bool {
|
||||
(flags & 0x01) != 0
|
||||
}
|
||||
|
||||
fn is_rst(flags: u8) -> bool {
|
||||
(flags & 0x04) != 0
|
||||
}
|
||||
|
||||
/// ICMP echo request detection (Type 8 for ICMPv4, Type 128 for ICMPv6).
|
||||
/// Returns true if the packet is an ICMP echo request (Type 8 for ICMPv4,
|
||||
/// Type 128 for ICMPv6). The packet in `ctx.packet` is the IP packet, so the
|
||||
/// ICMP type field is at offset `ihl` (after the IP header).
|
||||
fn is_echo_request(ctx: &PacketContext) -> bool {
|
||||
if ctx.packet.len() < 2 {
|
||||
return false;
|
||||
}
|
||||
let icmp_offset = match ctx.protocol {
|
||||
1 => {
|
||||
let ihl = (ctx.packet[0] & 0x0f) as usize * 4;
|
||||
if ctx.packet.len() < ihl + 1 {
|
||||
return false;
|
||||
}
|
||||
ihl
|
||||
}
|
||||
58 => 40,
|
||||
_ => return false,
|
||||
};
|
||||
let icmp_type = ctx.packet[icmp_offset];
|
||||
icmp_type == 8 || icmp_type == 128
|
||||
}
|
||||
|
||||
use super::{PacketContext, Verdict};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnState {
|
||||
New,
|
||||
Established,
|
||||
Related,
|
||||
OverLimit,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ConnKey {
|
||||
pub l3num: u8,
|
||||
pub l4proto: u8,
|
||||
pub src_addr: IpAddress,
|
||||
pub dst_addr: IpAddress,
|
||||
pub src_port: u16,
|
||||
pub dst_port: u16,
|
||||
}
|
||||
|
||||
impl Hash for ConnKey {
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.l3num.hash(state);
|
||||
self.l4proto.hash(state);
|
||||
self.src_port.hash(state);
|
||||
self.dst_port.hash(state);
|
||||
match self.src_addr {
|
||||
IpAddress::Ipv4(a) => u32::from(a).hash(state),
|
||||
IpAddress::Ipv6(a) => a.octets().hash(state),
|
||||
}
|
||||
match self.dst_addr {
|
||||
IpAddress::Ipv4(a) => u32::from(a).hash(state),
|
||||
IpAddress::Ipv6(a) => a.octets().hash(state),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnKey {
|
||||
pub fn reply(&self) -> Self {
|
||||
Self {
|
||||
l3num: self.l3num,
|
||||
l4proto: self.l4proto,
|
||||
src_addr: self.dst_addr,
|
||||
dst_addr: self.src_addr,
|
||||
src_port: self.dst_port,
|
||||
dst_port: self.src_port,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_context(l3num: u8, ctx: &PacketContext) -> Self {
|
||||
Self {
|
||||
l3num,
|
||||
l4proto: ctx.protocol,
|
||||
src_addr: ctx.src_addr,
|
||||
dst_addr: ctx.dst_addr,
|
||||
src_port: ctx.src_port.unwrap_or(0),
|
||||
dst_port: ctx.dst_port.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// TCP connection tracking states (mirrors `enum tcp_conntrack`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum TcpTracking {
|
||||
None,
|
||||
SynSent,
|
||||
SynRecv,
|
||||
Established,
|
||||
FinWait,
|
||||
TimeWait,
|
||||
Close,
|
||||
}
|
||||
|
||||
/// A single connection tracking entry (mirrors `struct nf_conn`).
|
||||
#[derive(Debug, Clone)]
|
||||
struct ConnEntry {
|
||||
key: ConnKey,
|
||||
reply_key: ConnKey,
|
||||
state: ConnState,
|
||||
tcp_state: TcpTracking,
|
||||
fin_from_orig: bool,
|
||||
timeout: Instant,
|
||||
orig_packets: u64,
|
||||
orig_bytes: u64,
|
||||
reply_packets: u64,
|
||||
reply_bytes: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ConntrackTable {
|
||||
entries: BTreeMap<ConnKey, ConnEntry>,
|
||||
last_cleanup: Option<Instant>,
|
||||
syn_rate_limits: BTreeMap<IpAddress, (u32, Instant)>,
|
||||
icmp_rate_limits: BTreeMap<IpAddress, (u32, Instant)>,
|
||||
over_limit_count: u64,
|
||||
icmp_error_count: u64,
|
||||
max_entries: usize,
|
||||
}
|
||||
|
||||
fn advance_entry_state(entry: &mut ConnEntry, is_orig: bool, ctx: &PacketContext, now: Instant) -> bool {
|
||||
if entry.key.l4proto != 6 {
|
||||
return false;
|
||||
}
|
||||
let flags = tcp_flags(ctx, entry.key.l3num);
|
||||
let res_rst = is_rst(flags);
|
||||
|
||||
// RST closes the connection immediately.
|
||||
if res_rst {
|
||||
entry.tcp_state = TcpTracking::Close;
|
||||
entry.state = ConnState::New;
|
||||
entry.timeout = now + Duration::from_secs(10);
|
||||
return true;
|
||||
}
|
||||
|
||||
let res_fin = is_fin(flags);
|
||||
|
||||
if is_orig {
|
||||
// Original direction: handshake completion and FIN teardown.
|
||||
match entry.tcp_state {
|
||||
TcpTracking::SynRecv => {
|
||||
entry.tcp_state = TcpTracking::Established;
|
||||
entry.state = ConnState::Established;
|
||||
entry.timeout = now + Duration::from_secs(432000);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::Established if res_fin => {
|
||||
entry.tcp_state = TcpTracking::FinWait;
|
||||
entry.fin_from_orig = true;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::FinWait if res_fin && !entry.fin_from_orig => {
|
||||
// Second FIN from reply direction (orig FIN was first)
|
||||
entry.tcp_state = TcpTracking::TimeWait;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
} else {
|
||||
// Reply direction: handshake initiation and close.
|
||||
match entry.tcp_state {
|
||||
TcpTracking::None if (flags & 0x12) == 0x12 => {
|
||||
entry.tcp_state = TcpTracking::SynRecv;
|
||||
entry.timeout = now + Duration::from_secs(60);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::SynSent => {
|
||||
entry.tcp_state = TcpTracking::SynRecv;
|
||||
entry.timeout = now + Duration::from_secs(60);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::Established if res_fin => {
|
||||
entry.tcp_state = TcpTracking::FinWait;
|
||||
entry.fin_from_orig = false;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::FinWait if res_fin && entry.fin_from_orig => {
|
||||
// Second FIN from orig direction (reply FIN was first)
|
||||
entry.tcp_state = TcpTracking::TimeWait;
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
return true;
|
||||
}
|
||||
TcpTracking::TimeWait => {
|
||||
entry.timeout = now + Duration::from_secs(120);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
impl ConntrackTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: BTreeMap::new(),
|
||||
last_cleanup: None,
|
||||
syn_rate_limits: BTreeMap::new(),
|
||||
icmp_rate_limits: BTreeMap::new(),
|
||||
over_limit_count: 0,
|
||||
icmp_error_count: 0,
|
||||
max_entries: 65536,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track(&mut self, ctx: &PacketContext, now: Instant) -> ConnState {
|
||||
let l3num = match ctx.src_addr {
|
||||
IpAddress::Ipv4(_) => 4u8,
|
||||
IpAddress::Ipv6(_) => 6u8,
|
||||
};
|
||||
if ctx.protocol != 6 && ctx.protocol != 17 && ctx.protocol != 1 && ctx.protocol != 58 {
|
||||
return ConnState::New;
|
||||
}
|
||||
|
||||
if ctx.protocol == 6 && is_syn(ctx, l3num) && self.check_syn_limit(ctx.src_addr, now) {
|
||||
self.over_limit_count = self.over_limit_count.saturating_add(1);
|
||||
return ConnState::OverLimit;
|
||||
}
|
||||
|
||||
if (ctx.protocol == 1 || ctx.protocol == 58) && is_echo_request(ctx)
|
||||
&& self.check_icmp_limit(ctx.src_addr, now)
|
||||
{
|
||||
self.over_limit_count = self.over_limit_count.saturating_add(1);
|
||||
return ConnState::OverLimit;
|
||||
}
|
||||
|
||||
if ctx.protocol == 1 || ctx.protocol == 58 {
|
||||
return self.track_icmp(ctx, l3num, now);
|
||||
}
|
||||
|
||||
let key = ConnKey::from_context(l3num, ctx);
|
||||
let reply_key = key.reply();
|
||||
|
||||
// First check if this packet belongs to an existing reply flow
|
||||
let (is_orig, entry_key) = if let Some(entry) = self.entries.get_mut(&reply_key) {
|
||||
entry.reply_packets = entry.reply_packets.saturating_add(1);
|
||||
entry.reply_bytes = entry.reply_bytes.saturating_add(ctx.packet.len() as u64);
|
||||
advance_entry_state(entry, false, ctx, now);
|
||||
return entry.state;
|
||||
} else {
|
||||
(true, key.clone())
|
||||
};
|
||||
|
||||
if let Some(entry) = self.entries.get_mut(&entry_key) {
|
||||
entry.orig_packets = entry.orig_packets.saturating_add(1);
|
||||
entry.orig_bytes = entry.orig_bytes.saturating_add(ctx.packet.len() as u64);
|
||||
advance_entry_state(entry, true, ctx, now);
|
||||
return entry.state;
|
||||
}
|
||||
|
||||
// New connection (mirrors `nf_conntrack_in`)
|
||||
let state = ConnState::New;
|
||||
let tcp_state = if ctx.protocol == 6 {
|
||||
let flags = if ctx.packet.len() >= 34 {
|
||||
let tcp_offset = if l3num == 4 { 20 } else { 40 };
|
||||
if ctx.packet.len() > tcp_offset + 13 {
|
||||
ctx.packet[tcp_offset + 13]
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
};
|
||||
if flags & 0x02 != 0 && flags & 0x10 == 0 {
|
||||
TcpTracking::SynSent
|
||||
} else {
|
||||
TcpTracking::None
|
||||
}
|
||||
} else {
|
||||
TcpTracking::None
|
||||
};
|
||||
|
||||
let timeout = if ctx.protocol == 17 {
|
||||
Duration::from_secs(30)
|
||||
} else {
|
||||
Duration::from_secs(60)
|
||||
};
|
||||
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.over_limit_count = self.over_limit_count.saturating_add(1);
|
||||
return ConnState::OverLimit;
|
||||
}
|
||||
|
||||
self.entries.insert(
|
||||
key.clone(),
|
||||
ConnEntry {
|
||||
key: key.clone(),
|
||||
reply_key,
|
||||
state,
|
||||
tcp_state,
|
||||
fin_from_orig: false,
|
||||
timeout: now + timeout,
|
||||
orig_packets: 1,
|
||||
orig_bytes: ctx.packet.len() as u64,
|
||||
reply_packets: 0,
|
||||
reply_bytes: 0,
|
||||
},
|
||||
);
|
||||
|
||||
state
|
||||
}
|
||||
|
||||
fn track_icmp(&mut self, ctx: &PacketContext, l3num: u8, now: Instant) -> ConnState {
|
||||
if ctx.packet.len() < 4 {
|
||||
return ConnState::New;
|
||||
}
|
||||
let icmp_offset = match l3num {
|
||||
4 => (ctx.packet[0] & 0x0f) as usize * 4,
|
||||
6 => 40,
|
||||
_ => return ConnState::New,
|
||||
};
|
||||
if ctx.packet.len() < icmp_offset + 6 {
|
||||
return ConnState::New;
|
||||
}
|
||||
let icmp_type = ctx.packet[icmp_offset];
|
||||
let icmp_code = ctx.packet[icmp_offset + 1];
|
||||
let icmp_id = u16::from_be_bytes([
|
||||
ctx.packet[icmp_offset + 4],
|
||||
ctx.packet[icmp_offset + 5],
|
||||
]);
|
||||
|
||||
let is_echo = icmp_type == 8 || icmp_type == 128;
|
||||
let is_echo_reply = icmp_type == 0 || icmp_type == 129;
|
||||
let is_error = (l3num == 4 && icmp_type == 3) // ICMPv4 Dest Unreachable
|
||||
|| (l3num == 4 && icmp_type == 11) // ICMPv4 Time Exceeded
|
||||
|| (l3num == 6 && icmp_type == 1) // ICMPv6 Dest Unreachable
|
||||
|| (l3num == 6 && icmp_type == 3); // ICMPv6 Time Exceeded
|
||||
|
||||
// ICMP errors carry the original packet. Try to extract the
|
||||
// embedded connection tuple so we can relate it to an existing
|
||||
// tracked connection (mirrors nf_conntrack_icmp_error()).
|
||||
if is_error {
|
||||
return self.track_icmp_error(ctx, l3num, icmp_offset, now);
|
||||
}
|
||||
|
||||
if !is_echo && !is_echo_reply {
|
||||
return ConnState::New;
|
||||
}
|
||||
|
||||
let key = ConnKey {
|
||||
l3num,
|
||||
l4proto: ctx.protocol,
|
||||
src_addr: ctx.src_addr,
|
||||
dst_addr: ctx.dst_addr,
|
||||
src_port: icmp_id,
|
||||
dst_port: icmp_type as u16,
|
||||
};
|
||||
|
||||
if is_echo {
|
||||
if self.entries.contains_key(&key) {
|
||||
return ConnState::Established;
|
||||
}
|
||||
if self.entries.len() >= self.max_entries {
|
||||
self.over_limit_count = self.over_limit_count.saturating_add(1);
|
||||
return ConnState::OverLimit;
|
||||
}
|
||||
self.entries.insert(
|
||||
key.clone(),
|
||||
ConnEntry {
|
||||
key: key.clone(),
|
||||
reply_key: ConnKey {
|
||||
src_addr: ctx.dst_addr,
|
||||
dst_addr: ctx.src_addr,
|
||||
src_port: icmp_id,
|
||||
dst_port: (if icmp_type == 8 { 0u8 } else { 129u8 }) as u16,
|
||||
..key
|
||||
},
|
||||
state: ConnState::New,
|
||||
tcp_state: TcpTracking::None,
|
||||
fin_from_orig: false,
|
||||
timeout: now + Duration::from_secs(30),
|
||||
orig_packets: 1,
|
||||
orig_bytes: ctx.packet.len() as u64,
|
||||
reply_packets: 0,
|
||||
reply_bytes: 0,
|
||||
},
|
||||
);
|
||||
return ConnState::New;
|
||||
}
|
||||
|
||||
let reply_key = ConnKey {
|
||||
src_addr: ctx.dst_addr,
|
||||
dst_addr: ctx.src_addr,
|
||||
src_port: icmp_id,
|
||||
dst_port: (if is_echo_reply && icmp_type == 0 { 8u8 } else { 128u8 }) as u16,
|
||||
..key
|
||||
};
|
||||
|
||||
if let Some(entry) = self.entries.get_mut(&reply_key) {
|
||||
entry.reply_packets = entry.reply_packets.saturating_add(1);
|
||||
entry.reply_bytes = entry.reply_bytes.saturating_add(ctx.packet.len() as u64);
|
||||
entry.state = ConnState::Established;
|
||||
entry.timeout = now + Duration::from_secs(30);
|
||||
return ConnState::Established;
|
||||
}
|
||||
ConnState::New
|
||||
}
|
||||
|
||||
/// Process an ICMP error message. Extracts the embedded connection
|
||||
/// tuple from the original IP header carried in the ICMP payload.
|
||||
/// Returns ConnState::Related if the embedded connection matches an
|
||||
/// existing tracked connection (mirrors nf_conntrack_icmp_error()).
|
||||
fn track_icmp_error(
|
||||
&mut self,
|
||||
ctx: &PacketContext,
|
||||
l3num: u8,
|
||||
icmp_offset: usize,
|
||||
now: Instant,
|
||||
) -> ConnState {
|
||||
// ICMP error payload: 4 bytes unused + original IP header + 8 bytes
|
||||
let inner_ip_start = icmp_offset + 8;
|
||||
if ctx.packet.len() < inner_ip_start + 20 {
|
||||
return ConnState::Error;
|
||||
}
|
||||
// Parse the inner IPv4 header.
|
||||
let inner = &ctx.packet[inner_ip_start..];
|
||||
if inner.len() < 20 || (inner[0] >> 4) != 4 {
|
||||
return ConnState::Error;
|
||||
}
|
||||
let inner_proto = inner[9];
|
||||
let inner_src = IpAddress::v4(inner[12], inner[13], inner[14], inner[15]);
|
||||
let inner_dst = IpAddress::v4(inner[16], inner[17], inner[18], inner[19]);
|
||||
let ihl = (inner[0] & 0x0f) as usize * 4;
|
||||
if inner.len() < ihl + 4 || inner_proto != 6 && inner_proto != 17 {
|
||||
return ConnState::Error;
|
||||
}
|
||||
let sport = u16::from_be_bytes([inner[ihl], inner[ihl + 1]]);
|
||||
let dport = u16::from_be_bytes([inner[ihl + 2], inner[ihl + 3]]);
|
||||
|
||||
// Build the inner connection key and check for a match.
|
||||
let inner_key = ConnKey {
|
||||
l3num: 4,
|
||||
l4proto: inner_proto,
|
||||
src_addr: inner_src,
|
||||
dst_addr: inner_dst,
|
||||
src_port: sport,
|
||||
dst_port: dport,
|
||||
};
|
||||
|
||||
if self.entries.contains_key(&inner_key) {
|
||||
ConnState::Related
|
||||
} else {
|
||||
self.icmp_error_count = self.icmp_error_count.saturating_add(1);
|
||||
ConnState::Error
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clean_expired(&mut self, now: Instant) {
|
||||
if let Some(last) = self.last_cleanup {
|
||||
if now < last + Duration::from_secs(1) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.last_cleanup = Some(now);
|
||||
let expired: Vec<ConnKey> = self
|
||||
.entries
|
||||
.iter()
|
||||
.filter(|(_, e)| e.timeout < now)
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect();
|
||||
for key in expired {
|
||||
self.entries.remove(&key);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
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!("icmp_errors: {}\n", self.icmp_error_count));
|
||||
out.push_str(&alloc::format!("max_entries: {}\n", self.max_entries));
|
||||
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);
|
||||
|
||||
let entry = self.syn_rate_limits.entry(src).or_insert((0, now));
|
||||
if now > entry.1 + SYN_WINDOW {
|
||||
*entry = (1, now);
|
||||
} else {
|
||||
entry.0 += 1;
|
||||
}
|
||||
entry.0 > SYN_LIMIT
|
||||
}
|
||||
|
||||
fn check_icmp_limit(&mut self, src: IpAddress, now: Instant) -> bool {
|
||||
const ICMP_LIMIT: u32 = 20;
|
||||
const ICMP_WINDOW: Duration = Duration::from_secs(1);
|
||||
|
||||
let entry = self.icmp_rate_limits.entry(src).or_insert((0, now));
|
||||
if now > entry.1 + ICMP_WINDOW {
|
||||
*entry = (1, now);
|
||||
} else {
|
||||
entry.0 += 1;
|
||||
}
|
||||
entry.0 > ICMP_LIMIT
|
||||
}
|
||||
|
||||
pub fn format(&self) -> alloc::string::String {
|
||||
let mut out = alloc::format!("conntrack entries: {}, over_limit: {}\n", self.entries.len(), self.over_limit_count);
|
||||
for entry in self.entries.values() {
|
||||
let tcp_info = if entry.key.l4proto == 6 {
|
||||
alloc::format!(" tcp={:?}", entry.tcp_state)
|
||||
} else {
|
||||
alloc::string::String::new()
|
||||
};
|
||||
out.push_str(&alloc::format!(
|
||||
" {:?}{} src={} dst={} sport={} dport={} orig_pkts={} orig_bytes={} reply_pkts={} reply_bytes={}\n",
|
||||
entry.state,
|
||||
tcp_info,
|
||||
entry.key.src_addr,
|
||||
entry.key.dst_addr,
|
||||
entry.key.src_port,
|
||||
entry.key.dst_port,
|
||||
entry.orig_packets,
|
||||
entry.orig_bytes,
|
||||
entry.reply_packets,
|
||||
entry.reply_bytes,
|
||||
));
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::filter::Hook;
|
||||
|
||||
fn make_tcp_syn_packet() -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
// IPv4 header
|
||||
p[0] = 0x45; // version 4, IHL 5
|
||||
p[9] = 6; // protocol = TCP
|
||||
// TCP header at offset 20
|
||||
p[20] = 0x50; p[21] = 0x00; // src port 0x5000 = 20480
|
||||
p[22] = 0x50; p[23] = 0x00; // dst port
|
||||
// TCP flags at offset 33 (20 + 13)
|
||||
p[33] = 0x02; // SYN
|
||||
p
|
||||
}
|
||||
|
||||
fn make_tcp_data_packet() -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
p[0] = 0x45;
|
||||
p[9] = 6;
|
||||
p[33] = 0x10; // ACK only (not SYN)
|
||||
p
|
||||
}
|
||||
|
||||
fn make_icmp_echo_packet() -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
p[0] = 0x45;
|
||||
p[9] = 1; // protocol = ICMP
|
||||
// ICMP header at offset 20
|
||||
p[20] = 8; // echo request
|
||||
p
|
||||
}
|
||||
|
||||
fn make_ctx<'a>(protocol: u8, packet: &'a [u8]) -> PacketContext<'a> {
|
||||
PacketContext {
|
||||
hook: Hook::InputLocal,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::v4(10, 0, 0, 1),
|
||||
dst_addr: IpAddress::v4(10, 0, 0, 2),
|
||||
protocol,
|
||||
src_port: Some(1234),
|
||||
dst_port: Some(80),
|
||||
packet,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_v6_ctx<'a>(protocol: u8, packet: &'a [u8]) -> PacketContext<'a> {
|
||||
PacketContext {
|
||||
hook: Hook::InputLocal,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::v6(0xfe80, 0, 0, 0, 0, 0, 0, 1),
|
||||
dst_addr: IpAddress::v6(0xfe80, 0, 0, 0, 0, 0, 0, 2),
|
||||
protocol,
|
||||
src_port: Some(1234),
|
||||
dst_port: Some(80),
|
||||
packet,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syn_limit_triggers_after_threshold() {
|
||||
let mut ct = ConntrackTable::new();
|
||||
let packet = make_tcp_syn_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
let mut over_limit_count = 0;
|
||||
for i in 0..150 {
|
||||
let ctx = make_ctx(6, &packet);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit_count += 1;
|
||||
}
|
||||
let _ = i;
|
||||
}
|
||||
assert!(over_limit_count >= 50,
|
||||
"Should trigger SYN limit after 100 SYNs/sec; got {} over_limit",
|
||||
over_limit_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmp_limit_triggers_after_threshold() {
|
||||
let mut ct = ConntrackTable::new();
|
||||
let packet = make_icmp_echo_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
let mut over_limit_count = 0;
|
||||
for _i in 0..50 {
|
||||
let ctx = make_ctx(1, &packet);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit_count += 1;
|
||||
}
|
||||
}
|
||||
assert!(over_limit_count >= 25,
|
||||
"Should trigger ICMP echo limit after 20/sec; got {} over_limit",
|
||||
over_limit_count);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn syn_and_icmp_have_independent_budgets() {
|
||||
// After R37 fix, separate maps — TCP SYN traffic shouldn't
|
||||
// affect ICMP echo budget and vice versa.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let tcp = make_tcp_syn_packet();
|
||||
let icmp = make_icmp_echo_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
// Burn through TCP SYN budget
|
||||
for _i in 0..150 {
|
||||
let ctx = make_ctx(6, &tcp);
|
||||
let _ = ct.track(&ctx, now);
|
||||
}
|
||||
// ICMP should still have its full budget — first 20 echo requests
|
||||
// should not be limited, only the 21st onward
|
||||
let mut not_over_limit = 0;
|
||||
let mut over_limit = 0;
|
||||
for _i in 0..30 {
|
||||
let ctx = make_ctx(1, &icmp);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit += 1;
|
||||
} else {
|
||||
not_over_limit += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(not_over_limit, 20,
|
||||
"ICMP budget should be independent — first 20 should pass even after TCP limit exhausted");
|
||||
assert_eq!(over_limit, 10,
|
||||
"Remaining 10 ICMP echoes (21-30) should be over_limit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_syn_tcp_does_not_count_against_syn_limit() {
|
||||
// Pure ACK packets shouldn't count against SYN flood budget.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let ack = make_tcp_data_packet();
|
||||
let now = Instant::from_secs(0);
|
||||
let mut over_limit = 0;
|
||||
for _i in 0..200 {
|
||||
let ctx = make_ctx(6, &ack);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(over_limit, 0,
|
||||
"Non-SYN TCP packets must not trigger SYN flood limit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ipv6_syn_detection_works() {
|
||||
// Regression test for the IPv4-only `is_syn()` bug.
|
||||
// Before fix: offset 33 was hardcoded, reading IPv6 header
|
||||
// bytes instead of TCP flags (which are at offset 53 for IPv6).
|
||||
let mut ct = ConntrackTable::new();
|
||||
let mut p = vec![0u8; 80];
|
||||
// IPv6 header: version 6 at byte 0 (0x60), next header = 6 (TCP) at byte 6
|
||||
p[0] = 0x60;
|
||||
p[6] = 6; // next header = TCP
|
||||
// TCP header at offset 40, flags at offset 53
|
||||
p[53] = 0x02; // SYN
|
||||
let now = Instant::from_secs(0);
|
||||
// The l3num is inferred from ctx.src_addr (v6 → l3num=6).
|
||||
let mut over_limit_count = 0;
|
||||
for _i in 0..150 {
|
||||
let ctx = make_v6_ctx(6, &p);
|
||||
if ct.track(&ctx, now) == ConnState::OverLimit {
|
||||
over_limit_count += 1;
|
||||
}
|
||||
}
|
||||
assert!(over_limit_count >= 50,
|
||||
"IPv6 SYN must trigger rate limit after threshold; got {}",
|
||||
over_limit_count);
|
||||
}
|
||||
|
||||
// Helper for building TCP packets with specific flags.
|
||||
fn make_tcp_pkt(flags: u8) -> Vec<u8> {
|
||||
let mut p = vec![0u8; 64];
|
||||
p[0] = 0x45; p[9] = 6; // IPv4 TCP
|
||||
p[12] = 10; p[13] = 0; p[14] = 0; p[15] = 1;
|
||||
p[16] = 10; p[17] = 0; p[18] = 0; p[19] = 2;
|
||||
p[33] = flags;
|
||||
p
|
||||
}
|
||||
|
||||
fn make_reply_ctx<'a>(packet: &'a [u8]) -> PacketContext<'a> {
|
||||
PacketContext {
|
||||
hook: Hook::InputLocal, in_dev: None, out_dev: None,
|
||||
src_addr: IpAddress::v4(10, 0, 0, 2),
|
||||
dst_addr: IpAddress::v4(10, 0, 0, 1),
|
||||
protocol: 6, src_port: Some(80), dst_port: Some(1234),
|
||||
packet,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rst_forces_state_to_new() {
|
||||
// RST forces the connection state back to New and resets to Close tracking.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let now = Instant::from_secs(0);
|
||||
let syn = make_tcp_syn_packet();
|
||||
let _ = ct.track(&make_ctx(6, &syn), now);
|
||||
// Send RST.
|
||||
let rst = make_tcp_pkt(0x04); // FIN flag = 0x01, RST = 0x04
|
||||
let _ = ct.track(&make_ctx(6, &rst), Instant::from_secs(1));
|
||||
// Verify that the entry was closed (no entries remain due to short timeout).
|
||||
// The state machine should set tcp_state=Close with a 10s timeout.
|
||||
// After track() processes the RST, the entry transitions to
|
||||
// ConnState::New (so it won't match 'Established' rules) but
|
||||
// stays in memory for 10 seconds until cleanup.
|
||||
assert_eq!(ct.len(), 1,
|
||||
"RST connection entry stays in memory for 10s cleanup window");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fin_transitions_established_to_timewait() {
|
||||
// Test that FIN from both directions transitions through
|
||||
// FinWait to TimeWait.
|
||||
let mut ct = ConntrackTable::new();
|
||||
let now = Instant::from_secs(0);
|
||||
let syn = make_tcp_syn_packet();
|
||||
let _ = ct.track(&make_ctx(6, &syn), now);
|
||||
// Reply FIN (from responder).
|
||||
let fin = make_tcp_pkt(0x01);
|
||||
let _ = ct.track(&make_reply_ctx(&fin), now);
|
||||
// Original FIN (from initiator).
|
||||
let _ = ct.track(&make_ctx(6, &fin), now);
|
||||
// The connection should have advanced past FinWait via
|
||||
// the two FINs. The state is now TimeWait.
|
||||
// Verify that the entry exists (TimeWait has 120s timeout).
|
||||
assert!(ct.len() > 0,
|
||||
"TimeWait state should keep the connection alive for 120s");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! Netfilter-style packet filter for Red Bear OS netstack.
|
||||
//!
|
||||
//! This module mirrors the architecture of Linux 7.1's netfilter subsystem
|
||||
//! (`net/netfilter/core.c`, `net/ipv4/netfilter/iptable_filter.c`).
|
||||
//!
|
||||
//! Mapping to Linux 7.1:
|
||||
//! - [`Hook`] mirrors `enum nf_inet_hooks` (`include/uapi/linux/netfilter.h:42`)
|
||||
//! - [`Verdict`] mirrors the `NF_DROP` / `NF_ACCEPT` constants
|
||||
//! (`include/uapi/linux/netfilter.h:11-17`)
|
||||
//! - [`FilterEngine`] mirrors the role of `nf_iterate` + `xt_table`
|
||||
//! - [`FilterRule`] mirrors `ipt_entry` + `ipt_entry_match` + `ipt_entry_target`
|
||||
//! (`net/ipv4/netfilter/ip_tables.h`)
|
||||
//!
|
||||
//! The filter is stateless (no conntrack in this initial revision). A future
|
||||
//! revision will add a conntrack hash table analogous to `nf_conn` in
|
||||
//! `net/netfilter/nf_conntrack_core.c`.
|
||||
|
||||
mod conntrack;
|
||||
mod nat;
|
||||
mod rule;
|
||||
mod table;
|
||||
|
||||
pub use conntrack::{ConnState, ConntrackTable};
|
||||
pub use nat::{NatBinding, NatRule, NatTable, NatType, rewrite_src_ipv4, parse_nat_rule};
|
||||
pub use rule::{FilterRule, MatchResult, Protocol, StateMatch};
|
||||
pub use table::{FilterTable, parse_rule};
|
||||
|
||||
/// The five netfilter hook points (mirrors `enum nf_inet_hooks`).
|
||||
///
|
||||
/// Only `InputLocal` and `OutputLocal` are wired in this revision. The other
|
||||
/// three are defined for future expansion: `PreRouting` / `Forward` /
|
||||
/// `PostRouting` are required for routing/firewalling of transit traffic
|
||||
/// (when Red Bear gains multi-homed forwarding in Phase 6).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum Hook {
|
||||
/// `NF_INET_PRE_ROUTING` — packet just arrived from a NIC, before the
|
||||
/// routing decision. (Linux 7.1: `include/uapi/linux/netfilter.h:43`)
|
||||
PreRouting,
|
||||
/// `NF_INET_LOCAL_IN` — packet destined for this host, after the routing
|
||||
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:44`)
|
||||
InputLocal,
|
||||
/// `NF_INET_FORWARD` — packet being forwarded between NICs. (Linux 7.1:
|
||||
/// `include/uapi/linux/netfilter.h:45`)
|
||||
Forward,
|
||||
/// `NF_INET_LOCAL_OUT` — packet generated locally, before the routing
|
||||
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:46`)
|
||||
OutputLocal,
|
||||
/// `NF_INET_POST_ROUTING` — packet leaving a NIC, after the routing
|
||||
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:47`)
|
||||
PostRouting,
|
||||
}
|
||||
|
||||
impl Hook {
|
||||
/// Returns all hook points, in Linux's canonical order (matches
|
||||
/// `enum nf_inet_hooks` ordering).
|
||||
pub const ALL: [Hook; 5] = [
|
||||
Hook::PreRouting,
|
||||
Hook::InputLocal,
|
||||
Hook::Forward,
|
||||
Hook::OutputLocal,
|
||||
Hook::PostRouting,
|
||||
];
|
||||
|
||||
/// Returns the kernel-style numeric id (matches `NF_INET_*` constants).
|
||||
pub const fn as_u32(self) -> u32 {
|
||||
match self {
|
||||
Hook::PreRouting => 0,
|
||||
Hook::InputLocal => 1,
|
||||
Hook::Forward => 2,
|
||||
Hook::OutputLocal => 3,
|
||||
Hook::PostRouting => 4,
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverse of [`Self::as_u32`]. Returns `None` for unknown ids.
|
||||
pub const fn from_u32(id: u32) -> Option<Hook> {
|
||||
match id {
|
||||
0 => Some(Hook::PreRouting),
|
||||
1 => Some(Hook::InputLocal),
|
||||
2 => Some(Hook::Forward),
|
||||
3 => Some(Hook::OutputLocal),
|
||||
4 => Some(Hook::PostRouting),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Short lowercase name used in the scheme interface (e.g. "input",
|
||||
/// "output"). Mirrors the chain name conventions of iptables/nftables.
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Hook::PreRouting => "prerouting",
|
||||
Hook::InputLocal => "input",
|
||||
Hook::Forward => "forward",
|
||||
Hook::OutputLocal => "output",
|
||||
Hook::PostRouting => "postrouting",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The decision returned by the filter engine after evaluating rules.
|
||||
///
|
||||
/// Mirrors the verdict constants in `include/uapi/linux/netfilter.h`:
|
||||
/// - [`Accept`] = `NF_ACCEPT` (= 1)
|
||||
/// - [`Drop`] = `NF_DROP` (= 0)
|
||||
///
|
||||
/// Linux's `NF_QUEUE` (= 3) and `NF_STOLEN` (= 2) are not supported in this
|
||||
/// revision (no userspace packet queue and no socket ownership transfer).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Verdict {
|
||||
/// `NF_ACCEPT` — continue normal processing.
|
||||
Accept,
|
||||
/// `NF_DROP` — silently discard the packet.
|
||||
Drop,
|
||||
/// Log the packet details and continue evaluating the next rule.
|
||||
/// Mirrors iptables `-j LOG --log-prefix "..."`.
|
||||
Log,
|
||||
/// Send ICMP Destination Unreachable (port) back to the source.
|
||||
/// Mirrors iptables `-j REJECT --reject-with icmp-port-unreachable`.
|
||||
Reject,
|
||||
}
|
||||
|
||||
impl Verdict {
|
||||
pub const fn as_u32(self) -> u32 {
|
||||
match self {
|
||||
Verdict::Accept => 1,
|
||||
Verdict::Drop => 0,
|
||||
Verdict::Log => 2,
|
||||
Verdict::Reject => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn from_u32(id: u32) -> Option<Verdict> {
|
||||
match id {
|
||||
1 => Some(Verdict::Accept),
|
||||
0 => Some(Verdict::Drop),
|
||||
2 => Some(Verdict::Log),
|
||||
3 => Some(Verdict::Reject),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
Verdict::Accept => "ACCEPT",
|
||||
Verdict::Drop => "DROP",
|
||||
Verdict::Log => "LOG",
|
||||
Verdict::Reject => "REJECT",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The context passed to the filter engine for evaluation.
|
||||
///
|
||||
/// Mirrors `struct nf_hook_state` (`include/linux/netfilter.h:78`):
|
||||
/// - `hook` ↔ `state->hook`
|
||||
/// - `in_dev` ↔ `state->in`
|
||||
/// - `out_dev` ↔ `state->out`
|
||||
/// - `protocol` ↔ the L4 protocol number (extracted from the IP header)
|
||||
///
|
||||
/// `skb` (the buffer) is split into its derived fields here for ergonomic
|
||||
/// matching without forcing each rule to re-parse the packet.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PacketContext<'a> {
|
||||
pub hook: Hook,
|
||||
pub in_dev: Option<alloc::rc::Rc<str>>,
|
||||
pub out_dev: Option<alloc::rc::Rc<str>>,
|
||||
pub src_addr: smoltcp::wire::IpAddress,
|
||||
pub dst_addr: smoltcp::wire::IpAddress,
|
||||
pub protocol: u8,
|
||||
pub src_port: Option<u16>,
|
||||
pub dst_port: Option<u16>,
|
||||
pub packet: &'a [u8],
|
||||
}
|
||||
|
||||
extern crate alloc;
|
||||
@@ -0,0 +1,495 @@
|
||||
//! Network Address Translation — mirrors Linux 7.1's `nf_nat` subsystem.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/netfilter/nf_nat_core.c` — `nf_nat_packet()`, `nf_nat_setup_info()`
|
||||
//! - `include/net/netfilter/nf_nat.h` — `struct nf_conn_nat`
|
||||
//! - `net/netfilter/nf_nat_proto_tcp.c` — TCP checksum adjustment after NAT
|
||||
//! - `net/netfilter/nf_nat_proto_udp.c` — UDP checksum adjustment after NAT
|
||||
//!
|
||||
//! Two NAT types are supported:
|
||||
//! - **SNAT** (Source NAT): changes the source IP of outgoing packets.
|
||||
//! Applied in the OUTPUT/POSTROUTING path. Mirrors `nf_nat_masquerade.c`.
|
||||
//! - **DNAT** (Destination NAT): changes the destination IP of incoming
|
||||
//! packets. Applied in the INPUT/PREROUTING path. Used for port forwarding.
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
|
||||
|
||||
use super::{Hook, PacketContext};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum NatType {
|
||||
Snat,
|
||||
Dnat,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NatRule {
|
||||
pub id: u32,
|
||||
pub nat_type: NatType,
|
||||
pub hook: Hook,
|
||||
pub src_match: Option<IpAddress>,
|
||||
pub dst_match: Option<IpAddress>,
|
||||
pub trans_addr: IpAddress,
|
||||
pub trans_port: Option<u16>,
|
||||
pub match_count: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct NatBinding {
|
||||
pub orig_src: IpAddress,
|
||||
pub trans_src: IpAddress,
|
||||
pub orig_dst: IpAddress,
|
||||
pub trans_dst: IpAddress,
|
||||
pub orig_sport: u16,
|
||||
pub trans_sport: u16,
|
||||
pub orig_dport: u16,
|
||||
pub trans_dport: u16,
|
||||
}
|
||||
|
||||
impl Default for NatBinding {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
orig_src: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
trans_src: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
orig_dst: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
trans_dst: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
|
||||
orig_sport: 0,
|
||||
trans_sport: 0,
|
||||
orig_dport: 0,
|
||||
trans_dport: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NatTable {
|
||||
pub rules: Vec<NatRule>,
|
||||
pub bindings: Vec<NatBinding>,
|
||||
next_id: u32,
|
||||
ephemeral_port: u16,
|
||||
}
|
||||
|
||||
impl NatTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rules: Vec::new(),
|
||||
bindings: Vec::new(),
|
||||
next_id: 1,
|
||||
ephemeral_port: 40000,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, mut rule: NatRule) -> u32 {
|
||||
rule.id = self.next_id;
|
||||
self.next_id = self.next_id.saturating_add(1);
|
||||
let id = rule.id;
|
||||
self.rules.push(rule);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn lookup_snat(
|
||||
&self,
|
||||
hook: Hook,
|
||||
src: IpAddress,
|
||||
_dst: IpAddress,
|
||||
) -> Option<(IpAddress, Option<u16>)> {
|
||||
for rule in self.rules.iter().filter(|r| {
|
||||
r.nat_type == NatType::Snat && r.hook == hook
|
||||
}) {
|
||||
if let Some(m) = rule.src_match {
|
||||
if m != src {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Some((rule.trans_addr, rule.trans_port));
|
||||
}
|
||||
if hook == Hook::OutputLocal || hook == Hook::PostRouting {
|
||||
for rule in self.rules.iter().filter(|r| {
|
||||
r.nat_type == NatType::Snat && r.src_match.is_none()
|
||||
}) {
|
||||
return Some((rule.trans_addr, rule.trans_port));
|
||||
}
|
||||
}
|
||||
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,
|
||||
_src: IpAddress,
|
||||
dst: IpAddress,
|
||||
) -> Option<(IpAddress, Option<u16>)> {
|
||||
for rule in self.rules.iter().filter(|r| {
|
||||
r.nat_type == NatType::Dnat && r.hook == hook
|
||||
}) {
|
||||
if let Some(m) = rule.dst_match {
|
||||
if m != dst {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return Some((rule.trans_addr, rule.trans_port));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn alloc_ephemeral_port(&mut self) -> u16 {
|
||||
let port = self.ephemeral_port;
|
||||
self.ephemeral_port = if self.ephemeral_port >= 65535 {
|
||||
40000
|
||||
} else {
|
||||
self.ephemeral_port.saturating_add(1)
|
||||
};
|
||||
port
|
||||
}
|
||||
|
||||
pub fn format(&self) -> String {
|
||||
let mut out = String::from("Nat table:\n");
|
||||
for rule in &self.rules {
|
||||
out.push_str(&alloc::format!(
|
||||
" [{:>3}] {:?} {:?} -> {} port={:?} matches={}\n",
|
||||
rule.id,
|
||||
rule.nat_type,
|
||||
rule.hook,
|
||||
rule.trans_addr,
|
||||
rule.trans_port,
|
||||
rule.match_count,
|
||||
));
|
||||
}
|
||||
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);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rewrites the source IP address in an IPv4 packet. Returns `true` on
|
||||
/// success. Mirrors `nf_nat_ipv4_manip_pkt()` in
|
||||
/// `net/ipv4/netfilter/nf_nat_l3proto_ipv4.c`.
|
||||
pub fn rewrite_src_ipv4(packet: &mut [u8], new_src: Ipv4Address) -> bool {
|
||||
if packet.len() < 20 {
|
||||
return false;
|
||||
}
|
||||
let mut ipv4 = smoltcp::wire::Ipv4Packet::new_unchecked(packet);
|
||||
ipv4.set_src_addr(new_src);
|
||||
ipv4.fill_checksum();
|
||||
true
|
||||
}
|
||||
|
||||
/// Rewrites the destination IP address in an IPv4 packet.
|
||||
pub fn rewrite_dst_ipv4(packet: &mut [u8], new_dst: Ipv4Address) -> bool {
|
||||
if packet.len() < 20 {
|
||||
return false;
|
||||
}
|
||||
let mut ipv4 = smoltcp::wire::Ipv4Packet::new_unchecked(packet);
|
||||
ipv4.set_dst_addr(new_dst);
|
||||
ipv4.fill_checksum();
|
||||
true
|
||||
}
|
||||
|
||||
/// Rewrites the TCP/UDP port in the transport header and recomputes the
|
||||
/// checksum. Mirrors `nf_nat_proto_tcp.c` (`tcp_manip_pkt`) and
|
||||
/// `nf_nat_proto_udp.c` (`udp_manip_pkt`).
|
||||
pub fn rewrite_port_ipv4(
|
||||
packet: &mut [u8],
|
||||
old_port: u16,
|
||||
new_port: u16,
|
||||
old_addr: IpAddress,
|
||||
new_addr: IpAddress,
|
||||
is_src: bool,
|
||||
protocol: u8,
|
||||
) {
|
||||
let ip_header_len: usize = 20;
|
||||
if packet.len() < ip_header_len + 4 {
|
||||
return;
|
||||
}
|
||||
let transport = &mut packet[ip_header_len..];
|
||||
let (port_offset, addr_old, addr_new) = if is_src {
|
||||
(0, old_addr, new_addr)
|
||||
} else {
|
||||
(2, old_addr, new_addr)
|
||||
};
|
||||
|
||||
transport[port_offset] = (new_port >> 8) as u8;
|
||||
transport[port_offset + 1] = (new_port & 0xff) as u8;
|
||||
|
||||
if protocol == 17 {
|
||||
let old_csum = u16::from_be_bytes([transport[6], transport[7]]);
|
||||
if old_csum != 0 {
|
||||
transport[6] = 0;
|
||||
transport[7] = 0;
|
||||
}
|
||||
} else {
|
||||
transport[16] = 0;
|
||||
transport[17] = 0;
|
||||
}
|
||||
|
||||
recompute_transport_checksum(packet, protocol, addr_old, addr_new);
|
||||
}
|
||||
|
||||
fn recompute_transport_checksum(
|
||||
packet: &mut [u8],
|
||||
protocol: u8,
|
||||
_old_addr: IpAddress,
|
||||
_new_addr: IpAddress,
|
||||
) {
|
||||
if packet.len() < 20 {
|
||||
return;
|
||||
}
|
||||
let src_bytes: [u8; 4] = packet[12..16].try_into().unwrap_or([0; 4]);
|
||||
let dst_bytes: [u8; 4] = packet[16..20].try_into().unwrap_or([0; 4]);
|
||||
let src_addr = Ipv4Address::new(src_bytes[0], src_bytes[1], src_bytes[2], src_bytes[3]);
|
||||
let dst_addr = Ipv4Address::new(dst_bytes[0], dst_bytes[1], dst_bytes[2], dst_bytes[3]);
|
||||
|
||||
let transport = &mut packet[20..];
|
||||
|
||||
match protocol {
|
||||
6 => {
|
||||
if transport.len() >= 20 {
|
||||
let mut tcp = smoltcp::wire::TcpPacket::new_unchecked(transport);
|
||||
tcp.fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr));
|
||||
}
|
||||
}
|
||||
17 => {
|
||||
if transport.len() >= 8 {
|
||||
let mut udp = smoltcp::wire::UdpPacket::new_unchecked(transport);
|
||||
udp.fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_nat_rule(
|
||||
line: &str,
|
||||
) -> core::result::Result<NatRule, NatParseError> {
|
||||
let mut tokens = line.split_whitespace();
|
||||
|
||||
let nat_type_str = tokens.next().ok_or(NatParseError::MissingField("nat type"))?;
|
||||
let nat_type = match nat_type_str.to_uppercase().as_str() {
|
||||
"SNAT" => NatType::Snat,
|
||||
"DNAT" => NatType::Dnat,
|
||||
_ => return Err(NatParseError::BadNatType(nat_type_str.to_string())),
|
||||
};
|
||||
|
||||
let hook_str = tokens.next().ok_or(NatParseError::MissingField("hook"))?;
|
||||
let hook = match hook_str.to_lowercase().as_str() {
|
||||
"prerouting" => Hook::PreRouting,
|
||||
"input" => Hook::InputLocal,
|
||||
"forward" => Hook::Forward,
|
||||
"output" => Hook::OutputLocal,
|
||||
"postrouting" => Hook::PostRouting,
|
||||
_ => return Err(NatParseError::BadHook(hook_str.to_string())),
|
||||
};
|
||||
|
||||
let to_str = tokens.next();
|
||||
if to_str != Some("to") {
|
||||
return Err(NatParseError::MissingField("to"));
|
||||
}
|
||||
|
||||
let addr_str = tokens.next().ok_or(NatParseError::MissingField("address"))?;
|
||||
let trans_addr = if let Ok(v4) = Ipv4Address::from_str(addr_str) {
|
||||
IpAddress::Ipv4(v4)
|
||||
} else if let Ok(v6) = Ipv6Address::from_str(addr_str) {
|
||||
IpAddress::Ipv6(v6)
|
||||
} else {
|
||||
return Err(NatParseError::BadAddress(addr_str.to_string()));
|
||||
};
|
||||
|
||||
let mut trans_port = None;
|
||||
let mut src_match = None;
|
||||
let mut dst_match = None;
|
||||
|
||||
while let Some(token) = tokens.next() {
|
||||
match token {
|
||||
"port" => {
|
||||
let p = tokens.next().ok_or(NatParseError::MissingField("port value"))?;
|
||||
trans_port = Some(p.parse().map_err(|_| NatParseError::BadPort(p.to_string()))?);
|
||||
}
|
||||
"match-src" => {
|
||||
let a = tokens.next().ok_or(NatParseError::MissingField("match-src value"))?;
|
||||
src_match = Some(parse_addr(a)?);
|
||||
}
|
||||
"match-dst" => {
|
||||
let a = tokens.next().ok_or(NatParseError::MissingField("match-dst value"))?;
|
||||
dst_match = Some(parse_addr(a)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(NatRule {
|
||||
id: 0,
|
||||
nat_type,
|
||||
hook,
|
||||
src_match,
|
||||
dst_match,
|
||||
trans_addr,
|
||||
trans_port,
|
||||
match_count: 0,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_addr(s: &str) -> core::result::Result<IpAddress, NatParseError> {
|
||||
if let Ok(v4) = Ipv4Address::from_str(s) {
|
||||
Ok(IpAddress::Ipv4(v4))
|
||||
} else if let Ok(v6) = Ipv6Address::from_str(s) {
|
||||
Ok(IpAddress::Ipv6(v6))
|
||||
} else {
|
||||
Err(NatParseError::BadAddress(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
use core::str::FromStr;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum NatParseError {
|
||||
MissingField(&'static str),
|
||||
BadNatType(String),
|
||||
BadHook(String),
|
||||
BadAddress(String),
|
||||
BadPort(String),
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_ipv4_udp_packet(src: [u8; 4], dst: [u8; 4]) -> Vec<u8> {
|
||||
let mut p = vec![0u8; 28];
|
||||
p[0] = 0x45; p[1] = 0x00;
|
||||
p[2] = 0x00; p[3] = 0x1c;
|
||||
p[6] = 0x00; p[7] = 0x00;
|
||||
p[8] = 64;
|
||||
p[9] = 17;
|
||||
p[10] = 0x00; p[11] = 0x00;
|
||||
p[12..16].copy_from_slice(&src);
|
||||
p[16..20].copy_from_slice(&dst);
|
||||
p[20] = 0x12; p[21] = 0x34;
|
||||
p[22] = 0x56; p[23] = 0x78;
|
||||
p[24] = 0x00; p[25] = 0x08;
|
||||
p[26] = 0x00; p[27] = 0x00;
|
||||
p
|
||||
}
|
||||
|
||||
fn read_ipv4(p: &[u8], offset: usize) -> Ipv4Address {
|
||||
Ipv4Address::new(p[offset], p[offset+1], p[offset+2], p[offset+3])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_src_changes_source_address() {
|
||||
let mut p = make_ipv4_udp_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
let new_src = Ipv4Address::new(192, 168, 99, 99);
|
||||
assert!(rewrite_src_ipv4(&mut p, new_src));
|
||||
assert_eq!(read_ipv4(&p, 12), new_src, "Source IP must be rewritten");
|
||||
assert_eq!(read_ipv4(&p, 16), Ipv4Address::new(192, 168, 1, 1),
|
||||
"Destination IP must be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_dst_changes_destination_address() {
|
||||
let mut p = make_ipv4_udp_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
let new_dst = Ipv4Address::new(8, 8, 8, 8);
|
||||
assert!(rewrite_dst_ipv4(&mut p, new_dst));
|
||||
assert_eq!(read_ipv4(&p, 16), new_dst);
|
||||
assert_eq!(read_ipv4(&p, 12), Ipv4Address::new(10, 0, 0, 1),
|
||||
"Source IP must be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_short_packet_returns_false() {
|
||||
let mut p = vec![0u8; 10];
|
||||
assert!(!rewrite_src_ipv4(&mut p, Ipv4Address::new(1, 2, 3, 4)));
|
||||
assert!(!rewrite_dst_ipv4(&mut p, Ipv4Address::new(1, 2, 3, 4)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_recomputes_checksum_when_previously_nonzero() {
|
||||
let mut p = make_ipv4_udp_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
// Compute a real checksum first
|
||||
if let Ok(mut ipv4) = smoltcp::wire::Ipv4Packet::new_checked(&mut p) {
|
||||
ipv4.fill_checksum();
|
||||
}
|
||||
let original_csum = u16::from_be_bytes([p[10], p[11]]);
|
||||
assert_ne!(original_csum, 0, "Pre-condition: checksum should be non-zero");
|
||||
let _ = rewrite_src_ipv4(&mut p, Ipv4Address::new(10, 0, 0, 2));
|
||||
let new_csum = u16::from_be_bytes([p[10], p[11]]);
|
||||
assert_ne!(new_csum, original_csum,
|
||||
"Checksum must be updated after source rewrite");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_table_add_assigns_unique_ids() {
|
||||
let mut t = NatTable::new();
|
||||
let make_rule = || NatRule {
|
||||
id: 0,
|
||||
nat_type: NatType::Snat,
|
||||
hook: Hook::OutputLocal,
|
||||
src_match: None,
|
||||
dst_match: None,
|
||||
trans_addr: IpAddress::v4(192, 168, 1, 100),
|
||||
trans_port: None,
|
||||
match_count: 0,
|
||||
};
|
||||
let id1 = t.add(make_rule());
|
||||
let id2 = t.add(make_rule());
|
||||
assert_ne!(id1, id2, "Each rule gets a unique ID");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nat_ephemeral_port_allocates_unique() {
|
||||
let mut t = NatTable::new();
|
||||
let p1 = t.alloc_ephemeral_port();
|
||||
let p2 = t.alloc_ephemeral_port();
|
||||
assert_ne!(p1, p2, "Ephemeral ports must be unique across calls");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
//! Filter rule representation and packet matching.
|
||||
//!
|
||||
//! Mirrors the Linux 7.1 `ipt_entry` structure and the `xt_match` matching
|
||||
//! framework:
|
||||
//! - Linux `ipt_entry` lives in `net/ipv4/netfilter/ip_tables.h`
|
||||
//! - Linux `xt_match` lives in `net/netfilter/x_tables.h`
|
||||
//! - Linux `xt_action_param` (match context) lives in
|
||||
//! `include/linux/netfilter/x_tables.h`
|
||||
//!
|
||||
//! In this revision the rule supports a fixed set of match fields that
|
||||
//! cover the most common iptables/xt_matches: `src`/`dst` address+CIDR,
|
||||
//! `protocol`, `sport`/`dport` (TCP/UDP only), and the `in`/`out`
|
||||
//! interface. This is intentionally narrower than the full xtables match
|
||||
//! set — extensions like `iprange`, `mac`, `conntrack`, `recent`, etc.
|
||||
//! will be added in follow-up revisions as separate match kinds.
|
||||
|
||||
use alloc::rc::Rc;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
|
||||
|
||||
use super::{ConnState, Hook, PacketContext, Verdict};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StateMatch {
|
||||
New,
|
||||
Established,
|
||||
Related,
|
||||
Invalid,
|
||||
}
|
||||
|
||||
/// The IP protocol number used in a rule. Values mirror the IANA assigned
|
||||
/// numbers (`include/uapi/linux/in.h`):
|
||||
/// - 1 = `IPPROTO_ICMP`
|
||||
/// - 6 = `IPPROTO_TCP`
|
||||
/// - 17 = `IPPROTO_UDP`
|
||||
/// - 58 = `IPPROTO_ICMPV6`
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Protocol(pub u8);
|
||||
|
||||
impl Protocol {
|
||||
pub const ANY: Option<Protocol> = None;
|
||||
pub const ICMP: Protocol = Protocol(1);
|
||||
pub const TCP: Protocol = Protocol(6);
|
||||
pub const UDP: Protocol = Protocol(17);
|
||||
pub const ICMP6: Protocol = Protocol(58);
|
||||
}
|
||||
|
||||
/// Outcome of matching a single rule against a packet.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum MatchResult {
|
||||
/// Every field in the rule matched. The rule's verdict applies.
|
||||
Match,
|
||||
/// At least one field did not match. Continue to the next rule.
|
||||
NoMatch,
|
||||
}
|
||||
|
||||
/// A single filter rule. The fields mirror the iptables CLI options:
|
||||
///
|
||||
/// | Rust field | iptables equivalent |
|
||||
/// |----------------------|----------------------------------------------|
|
||||
/// | `src_addr`/`src_cidr`| `--source` / `--src-range` (with cidr_len) |
|
||||
/// | `dst_addr`/`dst_cidr`| `--destination` / `--dst-range` |
|
||||
/// | `protocol` | `--protocol` |
|
||||
/// | `src_port` | `--source-port` (TCP/UDP only) |
|
||||
/// | `dst_port` | `--destination-port` |
|
||||
/// | `in_dev` | `--in-interface` |
|
||||
/// | `out_dev` | `--out-interface` |
|
||||
/// | `verdict` | `--jump` (ACCEPT or DROP) |
|
||||
///
|
||||
/// `None` in any of these fields means "match any" (a wildcard), mirroring
|
||||
/// iptables behaviour where omitting `--source` matches every source.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FilterRule {
|
||||
pub id: u32,
|
||||
pub hook: Hook,
|
||||
pub src_addr: Option<IpAddress>,
|
||||
pub src_prefix_len: u8,
|
||||
pub dst_addr: Option<IpAddress>,
|
||||
pub dst_prefix_len: u8,
|
||||
pub protocol: Option<Protocol>,
|
||||
pub src_port: Option<u16>,
|
||||
pub dst_port: Option<u16>,
|
||||
pub in_dev: Option<Rc<str>>,
|
||||
pub out_dev: Option<Rc<str>>,
|
||||
pub state_match: Option<StateMatch>,
|
||||
pub verdict: Verdict,
|
||||
pub match_count: u64,
|
||||
}
|
||||
|
||||
impl FilterRule {
|
||||
/// Returns true if this rule applies to the given hook point. Mirrors
|
||||
/// `ipt_entry->comefrom` semantics: a rule attached to `Hook::InputLocal`
|
||||
/// only fires when the engine evaluates the INPUT chain.
|
||||
pub fn applies_to(&self, hook: Hook) -> bool {
|
||||
self.hook == hook
|
||||
}
|
||||
|
||||
/// Evaluates this rule against a packet. Mirrors
|
||||
/// `xt_action_param`-based matching in `x_tables.c` (`xt_check_match`).
|
||||
pub fn matches(&self, ctx: &PacketContext<'_>) -> MatchResult {
|
||||
if !self.applies_to(ctx.hook) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
if let Some(src) = self.src_addr {
|
||||
if !addr_in_cidr(src, self.src_prefix_len, ctx.src_addr) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(dst) = self.dst_addr {
|
||||
if !addr_in_cidr(dst, self.dst_prefix_len, ctx.dst_addr) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(proto) = self.protocol {
|
||||
if proto.0 != ctx.protocol {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(sport) = self.src_port {
|
||||
if ctx.src_port != Some(sport) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(dport) = self.dst_port {
|
||||
if ctx.dst_port != Some(dport) {
|
||||
return MatchResult::NoMatch;
|
||||
}
|
||||
}
|
||||
if let Some(in_dev) = &self.in_dev {
|
||||
match &ctx.in_dev {
|
||||
Some(ctx_dev) if ctx_dev.as_ref() == in_dev.as_ref() => {}
|
||||
_ => return MatchResult::NoMatch,
|
||||
}
|
||||
}
|
||||
if let Some(out_dev) = &self.out_dev {
|
||||
match &ctx.out_dev {
|
||||
Some(ctx_dev) if ctx_dev.as_ref() == out_dev.as_ref() => {}
|
||||
_ => return MatchResult::NoMatch,
|
||||
}
|
||||
}
|
||||
MatchResult::Match
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `addr` falls inside the prefix `network/prefix_len`.
|
||||
/// Mirrors `ip_masked_match` (`net/ipv4/netfilter/ipt_addr.c`) — IPv4
|
||||
/// uses the simple 32-bit mask, IPv6 uses the 128-bit bitwise mask.
|
||||
fn addr_in_cidr(network: IpAddress, prefix_len: u8, addr: IpAddress) -> bool {
|
||||
match (network, addr) {
|
||||
(IpAddress::Ipv4(net), IpAddress::Ipv4(a)) => {
|
||||
if prefix_len == 0 {
|
||||
return true;
|
||||
}
|
||||
if prefix_len > 32 {
|
||||
return false;
|
||||
}
|
||||
let mask: u32 = if prefix_len == 32 {
|
||||
u32::MAX
|
||||
} else {
|
||||
!((1u32 << (32 - prefix_len)) - 1)
|
||||
};
|
||||
(u32::from(net) & mask) == (u32::from(a) & mask)
|
||||
}
|
||||
(IpAddress::Ipv6(net), IpAddress::Ipv6(a)) => {
|
||||
if prefix_len > 128 {
|
||||
return false;
|
||||
}
|
||||
let net_bytes = net.octets();
|
||||
let a_bytes = a.octets();
|
||||
let full_bytes = (prefix_len / 8) as usize;
|
||||
let remainder = prefix_len % 8;
|
||||
if net_bytes[..full_bytes] != a_bytes[..full_bytes] {
|
||||
return false;
|
||||
}
|
||||
if remainder > 0 && full_bytes < 16 {
|
||||
let mask = 0xff << (8 - remainder);
|
||||
(net_bytes[full_bytes] & mask) == (a_bytes[full_bytes] & mask)
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
(IpAddress::Ipv4(_), IpAddress::Ipv6(_)) | (IpAddress::Ipv6(_), IpAddress::Ipv4(_)) => {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extern crate alloc;
|
||||
@@ -0,0 +1,530 @@
|
||||
//! Filter table: a collection of rules and the per-chain default policies.
|
||||
//!
|
||||
//! Mirrors Linux 7.1's `xt_table` (`include/linux/netfilter/x_tables.h`):
|
||||
//! - `valid_hooks` (bitmask) ↔ `FilterTable::enabled_hooks` (set)
|
||||
//! - `entries` (rule blob) ↔ `FilterTable::rules`
|
||||
//! - Per-hook default policy ↔ `FilterTable::default_policy`
|
||||
//!
|
||||
//! The parser accepts an iptables-style textual rule. The grammar is a
|
||||
//! deliberate subset of iptables that covers the most common cases and
|
||||
//! stays readable:
|
||||
//!
|
||||
//! ```text
|
||||
//! ACTION CHAIN [-i IFACE] [-o IFACE] [-p PROTO] [-s ADDR[/LEN]] [-d ADDR[/LEN]]
|
||||
//! [--sport PORT] [--dport PORT] [state STATE|--ctstate STATE[,STATE...]]
|
||||
//! ```
|
||||
//!
|
||||
//! Examples:
|
||||
//! - `ACCEPT input -p tcp --dport 80`
|
||||
//! - `DROP input -s 10.0.0.0/8`
|
||||
//! - `ACCEPT output -d 192.168.1.0/24 --sport 1024`
|
||||
//! - `ACCEPT input -p tcp --ctstate ESTABLISHED,RELATED`
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::rc::Rc;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use core::fmt;
|
||||
use core::str::FromStr;
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
|
||||
|
||||
use super::conntrack::{ConnState, ConntrackTable};
|
||||
use super::nat::{NatRule, NatTable};
|
||||
use super::rule::{FilterRule, Protocol, StateMatch};
|
||||
use super::{Hook, PacketContext, Verdict};
|
||||
|
||||
/// A table of filter rules plus per-chain default policies.
|
||||
///
|
||||
/// In iptables terminology this is one "table" (the `filter` table).
|
||||
/// We currently expose exactly one table; future extensions can add
|
||||
/// `nat`, `mangle`, `raw` tables following the same model.
|
||||
pub struct FilterTable {
|
||||
pub rules: Vec<FilterRule>,
|
||||
pub default_policy: BTreeMap<Hook, Verdict>,
|
||||
pub next_id: u32,
|
||||
pub conntrack: Option<ConntrackTable>,
|
||||
pub nat_table: NatTable,
|
||||
pub chain_counters: BTreeMap<Hook, (u64, u64)>,
|
||||
pub log_buffer: Vec<String>,
|
||||
}
|
||||
|
||||
impl FilterTable {
|
||||
pub fn new() -> Self {
|
||||
let mut default_policy = BTreeMap::new();
|
||||
for &hook in &Hook::ALL {
|
||||
default_policy.insert(hook, Verdict::Accept);
|
||||
}
|
||||
Self {
|
||||
rules: Vec::new(),
|
||||
default_policy,
|
||||
next_id: 1,
|
||||
conntrack: Some(ConntrackTable::new()),
|
||||
nat_table: NatTable::new(),
|
||||
chain_counters: BTreeMap::new(),
|
||||
log_buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluates the rules attached to `ctx.hook` in order, returning the
|
||||
/// first matching verdict. If no rule matches, the chain's default
|
||||
/// policy applies. Mirrors `nf_iterate` + `ipt_do_table` in
|
||||
/// `net/ipv4/netfilter/ip_tables.c`.
|
||||
pub fn evaluate(&mut self, ctx: &PacketContext<'_>, now: Instant) -> Verdict {
|
||||
let conn_state = self.conntrack.as_mut().map(|ct| ct.track(ctx, now));
|
||||
if conn_state == Some(ConnState::OverLimit) {
|
||||
return Verdict::Drop;
|
||||
}
|
||||
let counter = self.chain_counters.entry(ctx.hook).or_insert((0, 0));
|
||||
counter.0 = counter.0.saturating_add(1);
|
||||
counter.1 = counter.1.saturating_add(ctx.packet.len() as u64);
|
||||
|
||||
let mut final_verdict: Option<Verdict> = None;
|
||||
for rule in self.rules.iter_mut() {
|
||||
if rule.state_match.is_some() {
|
||||
match (rule.state_match, conn_state) {
|
||||
(Some(StateMatch::Established), Some(ConnState::Established)) => {}
|
||||
(Some(StateMatch::New), Some(ConnState::New)) => {}
|
||||
(Some(StateMatch::Invalid), _) => continue,
|
||||
(Some(StateMatch::Related), _) => continue,
|
||||
(Some(_), _) => continue,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
if rule.applies_to(ctx.hook) && rule.matches(ctx) == super::rule::MatchResult::Match {
|
||||
rule.match_count = rule.match_count.saturating_add(1);
|
||||
match rule.verdict {
|
||||
Verdict::Log => {
|
||||
let msg = alloc::format!(
|
||||
"{} IN={} OUT={} SRC={} DST={} PROTO={} SPORT={} DPORT={}",
|
||||
rule.id,
|
||||
ctx.in_dev.as_deref().unwrap_or("-"),
|
||||
ctx.out_dev.as_deref().unwrap_or("-"),
|
||||
ctx.src_addr,
|
||||
ctx.dst_addr,
|
||||
ctx.protocol,
|
||||
ctx.src_port.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
|
||||
ctx.dst_port.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
|
||||
);
|
||||
if self.log_buffer.len() >= 100 {
|
||||
self.log_buffer.remove(0);
|
||||
}
|
||||
self.log_buffer.push(msg);
|
||||
continue;
|
||||
}
|
||||
Verdict::Reject => {
|
||||
final_verdict = Some(Verdict::Reject);
|
||||
break;
|
||||
}
|
||||
v => {
|
||||
final_verdict = Some(v);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
final_verdict.unwrap_or_else(|| {
|
||||
self.default_policy
|
||||
.get(&ctx.hook)
|
||||
.copied()
|
||||
.unwrap_or(Verdict::Accept)
|
||||
})
|
||||
}
|
||||
|
||||
/// Inserts a rule. The rule's `id` is overwritten with a fresh id from
|
||||
/// `next_id`.
|
||||
pub fn add(&mut self, mut rule: FilterRule) -> u32 {
|
||||
rule.id = self.next_id;
|
||||
self.next_id = self.next_id.saturating_add(1);
|
||||
let id = rule.id;
|
||||
self.rules.push(rule);
|
||||
id
|
||||
}
|
||||
|
||||
/// Removes the rule with the given id. Returns `true` if a rule was
|
||||
/// removed.
|
||||
pub fn remove(&mut self, id: u32) -> bool {
|
||||
if let Some(idx) = self.rules.iter().position(|r| r.id == id) {
|
||||
self.rules.remove(idx);
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Renders the entire table as a human-readable text dump. Mirrors
|
||||
/// `iptables -L -n -v` output format.
|
||||
pub fn format(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for &hook in &Hook::ALL {
|
||||
let policy = self
|
||||
.default_policy
|
||||
.get(&hook)
|
||||
.copied()
|
||||
.unwrap_or(Verdict::Accept);
|
||||
out.push_str(&alloc::format!(
|
||||
"Chain {} (policy {})\n",
|
||||
hook.name().to_uppercase(),
|
||||
policy.name()
|
||||
));
|
||||
for rule in self.rules.iter().filter(|r| r.hook == hook) {
|
||||
out.push_str(" ");
|
||||
out.push_str(&format_rule(rule));
|
||||
out.push('\n');
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
/// Clear per-chain counters and per-rule match counts.
|
||||
/// Rules themselves are preserved. Use this to restart metrics
|
||||
/// without losing configuration.
|
||||
pub fn reset_counters(&mut self) {
|
||||
for counter in self.chain_counters.values_mut() {
|
||||
*counter = (0, 0);
|
||||
}
|
||||
for rule in &mut self.rules {
|
||||
rule.match_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_rule(rule: &FilterRule) -> String {
|
||||
let mut out = alloc::format!("{} [{:>4}]", rule.verdict.name(), rule.id);
|
||||
if let Some(iface) = &rule.in_dev {
|
||||
out.push_str(&alloc::format!(" in={}", iface));
|
||||
}
|
||||
if let Some(iface) = &rule.out_dev {
|
||||
out.push_str(&alloc::format!(" out={}", iface));
|
||||
}
|
||||
if let Some(src) = rule.src_addr {
|
||||
out.push_str(&alloc::format!(" src={}/{}", src, rule.src_prefix_len));
|
||||
}
|
||||
if let Some(dst) = rule.dst_addr {
|
||||
out.push_str(&alloc::format!(" dst={}/{}", dst, rule.dst_prefix_len));
|
||||
}
|
||||
if let Some(proto) = rule.protocol {
|
||||
out.push_str(&alloc::format!(" proto={}", proto.0));
|
||||
}
|
||||
if let Some(sport) = rule.src_port {
|
||||
out.push_str(&alloc::format!(" sport={}", sport));
|
||||
}
|
||||
if let Some(dport) = rule.dst_port {
|
||||
out.push_str(&alloc::format!(" dport={}", dport));
|
||||
}
|
||||
out.push_str(&alloc::format!(" matches={}", rule.match_count));
|
||||
out
|
||||
}
|
||||
|
||||
/// Errors returned by the rule parser. Each variant includes enough
|
||||
/// context to point the user at the offending token (the index into the
|
||||
/// input line where parsing failed).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ParseError {
|
||||
/// Generic parse error with a human-readable message.
|
||||
Msg(&'static str),
|
||||
/// The input did not contain a recognizable chain name.
|
||||
UnknownHook(String),
|
||||
/// The action keyword was not ACCEPT or DROP.
|
||||
UnknownVerdict(String),
|
||||
/// An address failed to parse.
|
||||
BadAddress(String),
|
||||
/// A port number failed to parse.
|
||||
BadPort(String),
|
||||
/// A flag was recognized but its value was malformed.
|
||||
BadFlagValue { flag: &'static str, value: String },
|
||||
/// The protocol name was not tcp/udp/icmp/icmp6 or a number.
|
||||
UnknownProtocol(String),
|
||||
/// The interface name was missing or empty.
|
||||
MissingInterface,
|
||||
}
|
||||
|
||||
impl fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ParseError::Msg(m) => f.write_str(m),
|
||||
ParseError::UnknownHook(s) => write!(f, "unknown chain: {}", s),
|
||||
ParseError::UnknownVerdict(s) => write!(f, "unknown verdict: {}", s),
|
||||
ParseError::BadAddress(s) => write!(f, "bad address: {}", s),
|
||||
ParseError::BadPort(s) => write!(f, "bad port: {}", s),
|
||||
ParseError::BadFlagValue { flag, value } => {
|
||||
write!(f, "bad value for {}: {}", flag, value)
|
||||
}
|
||||
ParseError::UnknownProtocol(s) => write!(f, "unknown protocol: {}", s),
|
||||
ParseError::MissingInterface => f.write_str("missing interface name"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses one iptables-style textual rule into a [`FilterRule`]. The
|
||||
/// grammar is documented at the top of this file.
|
||||
pub fn parse_rule(line: &str) -> core::result::Result<FilterRule, ParseError> {
|
||||
let mut tokens = line.split_whitespace();
|
||||
let verdict_token = tokens.next().ok_or(ParseError::Msg("missing verdict"))?;
|
||||
let verdict = match verdict_token.to_uppercase().as_str() {
|
||||
"ACCEPT" => Verdict::Accept,
|
||||
"DROP" => Verdict::Drop,
|
||||
"LOG" => Verdict::Log,
|
||||
"REJECT" => Verdict::Reject,
|
||||
_ => return Err(ParseError::UnknownVerdict(verdict_token.to_string())),
|
||||
};
|
||||
|
||||
let chain_token = tokens.next().ok_or(ParseError::Msg("missing chain"))?;
|
||||
let hook = match chain_token.to_lowercase().as_str() {
|
||||
"prerouting" => Hook::PreRouting,
|
||||
"input" => Hook::InputLocal,
|
||||
"forward" => Hook::Forward,
|
||||
"output" => Hook::OutputLocal,
|
||||
"postrouting" => Hook::PostRouting,
|
||||
_ => return Err(ParseError::UnknownHook(chain_token.to_string())),
|
||||
};
|
||||
|
||||
let mut rule = FilterRule {
|
||||
id: 0,
|
||||
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,
|
||||
match_count: 0,
|
||||
};
|
||||
|
||||
while let Some(flag) = tokens.next() {
|
||||
match flag {
|
||||
"-i" => {
|
||||
let name = tokens.next().ok_or(ParseError::MissingInterface)?;
|
||||
rule.in_dev = Some(Rc::from(name));
|
||||
}
|
||||
"-o" => {
|
||||
let name = tokens.next().ok_or(ParseError::MissingInterface)?;
|
||||
rule.out_dev = Some(Rc::from(name));
|
||||
}
|
||||
"-s" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing -s value"))?;
|
||||
let (addr, prefix) = parse_addr_with_prefix(value)?;
|
||||
rule.src_addr = Some(addr);
|
||||
rule.src_prefix_len = prefix;
|
||||
}
|
||||
"-d" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing -d value"))?;
|
||||
let (addr, prefix) = parse_addr_with_prefix(value)?;
|
||||
rule.dst_addr = Some(addr);
|
||||
rule.dst_prefix_len = prefix;
|
||||
}
|
||||
"-p" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing -p value"))?;
|
||||
let proto = parse_protocol(value)?;
|
||||
rule.protocol = Some(proto);
|
||||
}
|
||||
"--sport" | "--source-port" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing --sport value"))?;
|
||||
rule.src_port = Some(parse_port(value)?);
|
||||
}
|
||||
"--dport" | "--destination-port" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing --dport value"))?;
|
||||
rule.dst_port = Some(parse_port(value)?);
|
||||
}
|
||||
"state" | "--ctstate" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing state value"))?;
|
||||
// Accept comma-separated list (e.g. "ESTABLISHED,RELATED"),
|
||||
// using the FIRST recognized state. The FilterRule struct
|
||||
// supports a single state_match field.
|
||||
let first = value.split(',').next().unwrap_or(value);
|
||||
rule.state_match = Some(match first {
|
||||
"new" | "NEW" => StateMatch::New,
|
||||
"established" | "ESTABLISHED" => StateMatch::Established,
|
||||
"related" | "RELATED" => StateMatch::Related,
|
||||
"invalid" | "INVALID" => StateMatch::Invalid,
|
||||
_ => return Err(ParseError::Msg("unknown state value")),
|
||||
});
|
||||
}
|
||||
_ => return Err(ParseError::Msg("unknown flag")),
|
||||
}
|
||||
}
|
||||
|
||||
if rule.protocol.is_some() && (rule.src_port.is_some() || rule.dst_port.is_some()) {
|
||||
if let Some(proto) = rule.protocol {
|
||||
if proto != Protocol::TCP && proto != Protocol::UDP && proto != Protocol::ICMP6 {
|
||||
return Err(ParseError::Msg("port specified for non-transport protocol"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(rule)
|
||||
}
|
||||
|
||||
fn parse_addr_with_prefix(value: &str) -> core::result::Result<(IpAddress, u8), ParseError> {
|
||||
let (addr_str, prefix_len) = match value.split_once('/') {
|
||||
Some((a, p)) => (a, p.parse::<u8>().map_err(|_| ParseError::BadAddress(value.to_string()))?),
|
||||
None => (value, 0),
|
||||
};
|
||||
if let Ok(v4) = Ipv4Address::from_str(addr_str) {
|
||||
let prefix = if prefix_len == 0 { 32 } else { prefix_len };
|
||||
Ok((IpAddress::Ipv4(v4), prefix))
|
||||
} else if let Ok(v6) = Ipv6Address::from_str(addr_str) {
|
||||
let prefix = if prefix_len == 0 { 128 } else { prefix_len };
|
||||
Ok((IpAddress::Ipv6(v6), prefix))
|
||||
} else {
|
||||
Err(ParseError::BadAddress(value.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_protocol(value: &str) -> core::result::Result<Protocol, ParseError> {
|
||||
let lower = value.to_lowercase();
|
||||
let num: Option<u8> = lower.parse().ok();
|
||||
match (lower.as_str(), num) {
|
||||
("tcp", _) => Ok(Protocol::TCP),
|
||||
("udp", _) => Ok(Protocol::UDP),
|
||||
("icmp", _) => Ok(Protocol::ICMP),
|
||||
("icmp6", _) | ("icmpv6", _) => Ok(Protocol::ICMP6),
|
||||
(_, Some(n)) => Ok(Protocol(n)),
|
||||
_ => Err(ParseError::UnknownProtocol(value.to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_port(value: &str) -> core::result::Result<u16, ParseError> {
|
||||
// Accept single port (e.g. "80") but reject port ranges (e.g.
|
||||
// "1024:65535"). The FilterRule struct uses a single u16 field
|
||||
// and does not support ranges. Silently accepting the first
|
||||
// number would make --sport 1024:65535 match only port 1024,
|
||||
// which is both wrong and misleading.
|
||||
if value.contains(':') {
|
||||
return Err(ParseError::Msg("port ranges not supported (use a single port number)"));
|
||||
}
|
||||
value
|
||||
.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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user