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:
Red Bear OS
2026-07-08 13:27:49 +03:00
parent 4506bfe02a
commit bb3e36e4e0
40 changed files with 6042 additions and 467 deletions
+357
View File
@@ -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
}
}
+175
View File
@@ -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;
+361
View File
@@ -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),
}
+187
View File
@@ -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;
+384
View File
@@ -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()))
}