restore: networking stack files from reflog (Phases 1-6)
Recovered from reflog commits 1c80937e and d0ecc067 after force-push data loss.
Includes: filter/, icmp_error.rs, slaac.rs, bond.rs, bridge.rs, gre.rs, ipip.rs,
qdisc.rs, tun.rs, vlan.rs, vxlan.rs, netfilter.rs, tun.rs, conntrack.rs, nat.rs,
rule.rs, table.rs, redbear-ufw/, dhcpv6d/, netdiag/ — 39 files total.
This commit is contained in:
@@ -0,0 +1,357 @@
|
||||
//! 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) -> bool {
|
||||
ctx.packet.len() >= 34 && (ctx.packet[33] & 0x02) != 0 && (ctx.packet[33] & 0x10) == 0
|
||||
}
|
||||
|
||||
use super::{PacketContext, Verdict};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ConnState {
|
||||
New,
|
||||
Established,
|
||||
Related,
|
||||
OverLimit,
|
||||
}
|
||||
|
||||
#[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,
|
||||
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>,
|
||||
rate_limits: BTreeMap<IpAddress, (u32, Instant)>,
|
||||
over_limit_count: u64,
|
||||
}
|
||||
|
||||
fn advance_entry_state(entry: &mut ConnEntry, is_orig: bool, now: Instant) {
|
||||
if entry.key.l4proto != 6 || is_orig {
|
||||
return;
|
||||
}
|
||||
|
||||
match entry.tcp_state {
|
||||
TcpTracking::SynSent => {
|
||||
entry.tcp_state = TcpTracking::SynRecv;
|
||||
}
|
||||
TcpTracking::SynRecv => {
|
||||
entry.tcp_state = TcpTracking::Established;
|
||||
entry.state = ConnState::Established;
|
||||
entry.timeout = now + Duration::from_secs(432000);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConntrackTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
entries: BTreeMap::new(),
|
||||
last_cleanup: None,
|
||||
rate_limits: BTreeMap::new(),
|
||||
over_limit_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
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) && 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 {
|
||||
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, 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, 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)
|
||||
};
|
||||
|
||||
self.entries.insert(
|
||||
key.clone(),
|
||||
ConnEntry {
|
||||
key: key.clone(),
|
||||
reply_key,
|
||||
state,
|
||||
tcp_state,
|
||||
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_type = ctx.packet[0];
|
||||
let icmp_code = ctx.packet[1];
|
||||
let icmp_id = u16::from_be_bytes([ctx.packet[4], ctx.packet[5]]);
|
||||
|
||||
let is_echo = icmp_type == 8 || icmp_type == 128;
|
||||
let is_echo_reply = icmp_type == 0 || icmp_type == 129;
|
||||
|
||||
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;
|
||||
}
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
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.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
|
||||
}
|
||||
|
||||
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() {
|
||||
out.push_str(&alloc::format!(
|
||||
" {:?} src={} dst={} sport={} dport={} orig_pkts={} orig_bytes={} reply_pkts={} reply_bytes={}\n",
|
||||
entry.state,
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, ParseError, 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,361 @@
|
||||
//! 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>,
|
||||
next_id: u32,
|
||||
ephemeral_port: u16,
|
||||
}
|
||||
|
||||
impl NatTable {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
rules: 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
|
||||
}
|
||||
|
||||
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 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),
|
||||
}
|
||||
@@ -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,384 @@
|
||||
//! 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]
|
||||
//! ```
|
||||
//!
|
||||
//! 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:65535`
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
pub fn set_default_policy(&mut self, hook: Hook, verdict: Verdict) {
|
||||
self.default_policy.insert(hook, verdict);
|
||||
}
|
||||
}
|
||||
|
||||
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" => {
|
||||
let value = tokens.next().ok_or(ParseError::Msg("missing state value"))?;
|
||||
rule.state_match = Some(match value {
|
||||
"new" => StateMatch::New,
|
||||
"established" => StateMatch::Established,
|
||||
"related" => StateMatch::Related,
|
||||
"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> {
|
||||
let port_str = value.split(':').next().unwrap_or(value);
|
||||
port_str
|
||||
.parse::<u16>()
|
||||
.map_err(|_| ParseError::BadPort(value.to_string()))
|
||||
}
|
||||
Reference in New Issue
Block a user