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:
+4
-1
@@ -20,14 +20,17 @@ scheme-utils = { path = "../scheme-utils" }
|
||||
|
||||
[dependencies.log]
|
||||
workspace = true
|
||||
default-features = false
|
||||
features = ["release_max_level_warn"]
|
||||
|
||||
[dependencies.smoltcp]
|
||||
version = "0.13.1"
|
||||
version = "0.12.0"
|
||||
default-features = false
|
||||
features = [
|
||||
"std",
|
||||
"medium-ethernet", "medium-ip",
|
||||
"proto-ipv4",
|
||||
"proto-ipv6",
|
||||
"socket-raw", "socket-icmp", "socket-udp", "socket-tcp", "socket-tcp-cubic",
|
||||
"iface-max-addr-count-8",
|
||||
"log"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
//! ICMP error message generation — mirrors Linux 7.1's `icmp_send`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/ipv4/icmp.c:802` — `__icmp_send()` — builds ICMPv4 error messages
|
||||
//! - `net/ipv6/icmp.c:icmpv6_send()` — ICMPv6 error messages
|
||||
//! - `include/uapi/linux/icmp.h` — ICMP type/code constants
|
||||
//!
|
||||
//! ICMP error messages contain: ICMP header + original IP header + 8 bytes
|
||||
//! of original transport payload (RFC 792 §3.1, RFC 4443 §2.4).
|
||||
|
||||
use smoltcp::wire::{
|
||||
Icmpv4Packet, Icmpv4Repr, Icmpv6Packet, Icmpv6Repr, IpAddress, Ipv4Address, Ipv4Packet,
|
||||
Ipv4Repr, Ipv6Address, Ipv6Packet, Ipv6Repr,
|
||||
};
|
||||
|
||||
/// Builds an ICMPv4 Destination Unreachable error message (Type 3).
|
||||
/// Code 3 = Port Unreachable (mirrors `ICMP_PORT_UNREACH` in icmp.c).
|
||||
pub fn build_icmpv4_port_unreachable(
|
||||
original_packet: &[u8],
|
||||
) -> Option<Vec<u8>> {
|
||||
let ipv4 = Ipv4Packet::new_checked(original_packet).ok()?;
|
||||
let src = ipv4.src_addr();
|
||||
let dst = ipv4.dst_addr();
|
||||
|
||||
let ip_header_len = ipv4.header_len() as usize;
|
||||
let total_len = original_packet.len().min(ip_header_len + 8);
|
||||
|
||||
let mut payload = alloc::vec![0u8; 4 + total_len];
|
||||
payload[0] = 0;
|
||||
payload[1] = 0;
|
||||
payload[2] = 0;
|
||||
payload[3] = 0;
|
||||
payload[4..4 + total_len].copy_from_slice(&original_packet[..total_len]);
|
||||
|
||||
let icmp_repr = Icmpv4Repr::DstUnreachable {
|
||||
reason: smoltcp::wire::Icmpv4DstUnreachable::PortUnreachable,
|
||||
header: Ipv4Repr {
|
||||
src_addr: dst,
|
||||
dst_addr: src,
|
||||
next_header: smoltcp::wire::IpProtocol::Icmp,
|
||||
hop_limit: 64,
|
||||
payload_len: 4 + total_len,
|
||||
},
|
||||
data: &payload,
|
||||
};
|
||||
|
||||
let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()];
|
||||
let mut pkt = Icmpv4Packet::new_unchecked(&mut buf);
|
||||
icmp_repr.emit(&mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
/// Builds an ICMPv4 Time Exceeded error message (Type 11, Code 0).
|
||||
/// Mirrors `ICMP_TIME_EXCEEDED` / `ICMP_EXC_TTL` in `icmp.c:1168`.
|
||||
pub fn build_icmpv4_time_exceeded(
|
||||
original_packet: &[u8],
|
||||
) -> Option<Vec<u8>> {
|
||||
let ipv4 = Ipv4Packet::new_checked(original_packet).ok()?;
|
||||
let src = ipv4.src_addr();
|
||||
let dst = ipv4.dst_addr();
|
||||
|
||||
let ip_header_len = ipv4.header_len() as usize;
|
||||
let total_len = original_packet.len().min(ip_header_len + 8);
|
||||
|
||||
let mut payload = alloc::vec![0u8; 4 + total_len];
|
||||
payload[4..4 + total_len].copy_from_slice(&original_packet[..total_len]);
|
||||
|
||||
let icmp_repr = Icmpv4Repr::TimeExceeded {
|
||||
reason: smoltcp::wire::Icmpv4TimeExceeded::TtlExpired,
|
||||
header: Ipv4Repr {
|
||||
src_addr: dst,
|
||||
dst_addr: src,
|
||||
next_header: smoltcp::wire::IpProtocol::Icmp,
|
||||
hop_limit: 64,
|
||||
payload_len: 4 + total_len,
|
||||
},
|
||||
data: &payload,
|
||||
};
|
||||
|
||||
let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()];
|
||||
let mut pkt = Icmpv4Packet::new_unchecked(&mut buf);
|
||||
icmp_repr.emit(&mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
/// Builds an ICMPv6 Destination Unreachable error (Type 1, Code 4).
|
||||
pub fn build_icmpv6_port_unreachable(
|
||||
original_packet: &[u8],
|
||||
) -> Option<Vec<u8>> {
|
||||
let ipv6 = Ipv6Packet::new_checked(original_packet).ok()?;
|
||||
let src = ipv6.src_addr();
|
||||
let _dst = ipv6.dst_addr();
|
||||
|
||||
let total_len = original_packet.len().min(48);
|
||||
let mut data = alloc::vec![0u8; 4 + total_len];
|
||||
data[4..4 + total_len].copy_from_slice(&original_packet[..total_len]);
|
||||
|
||||
let icmp_repr = Icmpv6Repr::DstUnreachable {
|
||||
reason: smoltcp::wire::Icmpv6DstUnreachable::PortUnreachable,
|
||||
header: Ipv6Repr {
|
||||
src_addr: _dst,
|
||||
dst_addr: src,
|
||||
next_header: smoltcp::wire::IpProtocol::Icmpv6,
|
||||
hop_limit: 64,
|
||||
payload_len: 4 + total_len,
|
||||
},
|
||||
data: &data,
|
||||
};
|
||||
|
||||
let mut buf = alloc::vec![0u8; icmp_repr.buffer_len()];
|
||||
let mut pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
icmp_repr.emit(&_dst, &src, &mut pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_standard_packet(src: [u8; 4], dst: [u8; 4]) -> Vec<u8> {
|
||||
// 20-byte standard IPv4 header + 8-byte UDP header.
|
||||
let mut p = vec![0u8; 28];
|
||||
p[0] = 0x45; // version 4, IHL 5
|
||||
p[2] = 0; p[3] = 28;
|
||||
p[8] = 64;
|
||||
p[9] = 17; // UDP
|
||||
p[10] = 0;
|
||||
p[11] = 0;
|
||||
p[12..16].copy_from_slice(&src);
|
||||
p[16..20].copy_from_slice(&dst);
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmpv4_short_packet_returns_none() {
|
||||
let p = vec![0u8; 10];
|
||||
assert!(build_icmpv4_port_unreachable(&p).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmpv4_preserves_destination_address() {
|
||||
// Regression: the previous code computed IHL as
|
||||
// (ipv4.version() & 0x0f) * 4 = 4*4 = 16 (WRONG).
|
||||
// smoltcp's version() returns 4 (the version number), not the
|
||||
// combined version/IHL byte. The correct IHL for a standard
|
||||
// 20-byte header is 5. With the bug, the ICMP error would
|
||||
// include only 16+8=24 bytes of original, truncating the
|
||||
// destination IP. With the fix, all 20+8=28 bytes included.
|
||||
let original = make_standard_packet([10, 0, 0, 1], [192, 168, 1, 1]);
|
||||
let icmp = build_icmpv4_port_unreachable(&original).expect("should build");
|
||||
// ICMP DstUnreachable layout:
|
||||
// bytes 0-3: type(1) + code(1) + checksum(2)
|
||||
// bytes 4-7: unused(4)
|
||||
// bytes 8-31: ICMP-wrapped IP header (src=orig_dst, dst=orig_src)
|
||||
// bytes 32+: original IP packet (and L4 data)
|
||||
// The original IP's dst IP first byte is at offset 32+16=48.
|
||||
assert_eq!(icmp[48], 192, "dst[0] must be 192, was {}", icmp[48]);
|
||||
assert_eq!(icmp[49], 168);
|
||||
assert_eq!(icmp[50], 1);
|
||||
assert_eq!(icmp[51], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn icmpv4_with_ip_options_includes_extended_header() {
|
||||
// IHL=6 means 24-byte header (20 + 4 bytes options).
|
||||
// The bug would compute IHL=4 (16 bytes) and truncate.
|
||||
let mut p = vec![0u8; 32];
|
||||
p[0] = 0x46; // version 4, IHL 6
|
||||
p[2] = 0; p[3] = 32;
|
||||
p[8] = 64;
|
||||
p[9] = 17;
|
||||
p[10] = 0;
|
||||
p[11] = 0;
|
||||
p[12] = 10; p[13] = 0; p[14] = 0; p[15] = 1; // src
|
||||
p[16] = 192; p[17] = 168; p[18] = 1; p[19] = 1; // dst
|
||||
let icmp = build_icmpv4_port_unreachable(&p).expect("should build");
|
||||
// Source IP of the original packet is at offset 32+12=44.
|
||||
// (See comment above for the full ICMP layout.)
|
||||
assert_eq!(icmp[44], 10, "src[0] must be 10 with IHL=6");
|
||||
assert_eq!(icmp[45], 0);
|
||||
assert_eq!(icmp[46], 0);
|
||||
assert_eq!(icmp[47], 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
//! Bonding (Link Aggregation) — mirrors Linux 7.1's `drivers/net/bonding/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `drivers/net/bonding/bond_main.c:1589` — `bond_should_deliver` (frame handling)
|
||||
//! - `drivers/net/bonding/bond_main.c:328` — mode selection (round-robin vs active-backup)
|
||||
//! - `drivers/net/bonding/bond_3ad.c` — LACP (not yet implemented)
|
||||
//!
|
||||
//! Two modes are supported:
|
||||
//! - **ActiveBackup**: one slave is active, others standby. If active fails,
|
||||
//! the next standby takes over. Mirrors `BOND_MODE_ACTIVEBACKUP`.
|
||||
//! - **RoundRobin**: packets are distributed across slaves in sequence.
|
||||
//! Mirrors `BOND_MODE_ROUNDROBIN`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
|
||||
|
||||
use super::LinkDevice;
|
||||
use super::Stats;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BondMode {
|
||||
ActiveBackup,
|
||||
RoundRobin,
|
||||
}
|
||||
|
||||
struct Slave {
|
||||
dev: Box<dyn LinkDevice>,
|
||||
up: bool,
|
||||
}
|
||||
|
||||
pub struct BondDevice {
|
||||
name: Rc<str>,
|
||||
slaves: RefCell<Vec<Slave>>,
|
||||
active_slave: RefCell<usize>,
|
||||
mode: BondMode,
|
||||
rr_counter: RefCell<usize>,
|
||||
recv_buffer: Vec<u8>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl BondDevice {
|
||||
pub fn new(name: &str, mode: BondMode) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
slaves: RefCell::new(Vec::new()),
|
||||
active_slave: RefCell::new(0),
|
||||
mode,
|
||||
rr_counter: RefCell::new(0),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_slave<T: LinkDevice + 'static>(&self, dev: T) {
|
||||
self.slaves.borrow_mut().push(Slave {
|
||||
dev: Box::new(dev),
|
||||
up: true,
|
||||
});
|
||||
}
|
||||
|
||||
fn select_slave(&self) -> Option<usize> {
|
||||
let slaves = self.slaves.borrow();
|
||||
match self.mode {
|
||||
BondMode::ActiveBackup => {
|
||||
let active = *self.active_slave.borrow();
|
||||
if active < slaves.len() && slaves[active].up {
|
||||
Some(active)
|
||||
} else {
|
||||
for (idx, slave) in slaves.iter().enumerate() {
|
||||
if slave.up {
|
||||
*self.active_slave.borrow_mut() = idx;
|
||||
return Some(idx);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
BondMode::RoundRobin => {
|
||||
let count = slaves.len();
|
||||
if count == 0 {
|
||||
return None;
|
||||
}
|
||||
let idx = *self.rr_counter.borrow() % count;
|
||||
*self.rr_counter.borrow_mut() = idx.wrapping_add(1);
|
||||
if !slaves[idx].up {
|
||||
for (i, slave) in slaves.iter().enumerate() {
|
||||
if slave.up {
|
||||
return Some(i);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
Some(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for BondDevice {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
if let Some(idx) = self.select_slave() {
|
||||
let mut slaves = self.slaves.borrow_mut();
|
||||
if let Some(slave) = slaves.get_mut(idx) {
|
||||
slave.dev.send(next_hop, packet, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, now: Instant) -> Option<&[u8]> {
|
||||
let mut slaves = self.slaves.borrow_mut();
|
||||
for slave in slaves.iter_mut() {
|
||||
if !slave.up {
|
||||
continue;
|
||||
}
|
||||
if let Some(buf) = slave.dev.recv(now) {
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(buf);
|
||||
return Some(&self.recv_buffer);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
self.slaves.borrow().iter().any(|s| s.up && s.dev.can_recv())
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
self.slaves.borrow().first().and_then(|s| s.dev.mac_address())
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
for slave in self.slaves.borrow_mut().iter_mut() {
|
||||
slave.dev.set_mac_address(addr);
|
||||
}
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
let active = *self.active_slave.borrow();
|
||||
let slaves = self.slaves.borrow();
|
||||
if slaves.is_empty() { return "down"; }
|
||||
if active < slaves.len() && slaves[active].up { "up" } else { "degraded" }
|
||||
}
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
let slaves = self.slaves.borrow();
|
||||
let active = *self.active_slave.borrow();
|
||||
let mut out = format!("Bond mode: {:?}, active slave: {}\n",
|
||||
self.mode, if active < slaves.len() { slaves[active].dev.name().as_ref() } else { "none" });
|
||||
for (i, s) in slaves.iter().enumerate() {
|
||||
out.push_str(&format!(" slave {}: {} ({})\n", i, s.dev.name(), if s.up { "up" } else { "down" }));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
let mut total = Stats::default();
|
||||
for s in self.slaves.borrow().iter() {
|
||||
let stats = s.dev.statistics();
|
||||
total.rx_bytes += stats.rx_bytes;
|
||||
total.rx_packets += stats.rx_packets;
|
||||
total.tx_bytes += stats.tx_bytes;
|
||||
total.tx_packets += stats.tx_packets;
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let slaves = self.slaves.borrow();
|
||||
for (i, s) in slaves.iter().enumerate() {
|
||||
let stats = s.dev.arp_stats();
|
||||
if !stats.is_empty() {
|
||||
out.push_str(&format!("slave{}: {}", i, stats));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
//! 802.1D MAC Learning Bridge — mirrors Linux 7.1's `net/bridge/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/bridge/br.c` — bridge core (`br_add_bridge`, `br_del_bridge`)
|
||||
//! - `net/bridge/br_fdb.c` — forwarding database (MAC learning + aging)
|
||||
//! - `net/bridge/br_forward.c` — frame forwarding logic
|
||||
//! - `net/bridge/br_input.c` — ingress frame handling
|
||||
//! - `net/bridge/br_device.c` — bridge as a `net_device`
|
||||
//!
|
||||
//! The bridge composes multiple link-layer devices and forwards Ethernet
|
||||
//! frames between them based on a dynamically-learned MAC→port table.
|
||||
//! Unknown destinations are flooded to all other ports, mirroring Linux's
|
||||
//! `br_flood()` in `br_forward.c`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::BTreeMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, IpAddress, IpCidr,
|
||||
};
|
||||
|
||||
use super::stp::{self, BPDU_MAC, PortState, StpState};
|
||||
use super::LinkDevice;
|
||||
use super::Stats;
|
||||
|
||||
const MAC_AGE_TIMEOUT: Duration = Duration::from_secs(300);
|
||||
|
||||
struct MacEntry {
|
||||
port: usize,
|
||||
last_seen: Instant,
|
||||
}
|
||||
|
||||
pub struct BridgeDevice {
|
||||
name: Rc<str>,
|
||||
ports: RefCell<Vec<Box<dyn LinkDevice>>>,
|
||||
mac_table: RefCell<BTreeMap<EthernetAddress, MacEntry>>,
|
||||
stp: RefCell<Option<StpState>>,
|
||||
recv_buffer: Vec<u8>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl BridgeDevice {
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
ports: RefCell::new(Vec::new()),
|
||||
mac_table: RefCell::new(BTreeMap::new()),
|
||||
stp: RefCell::new(None),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_port<T: LinkDevice + 'static>(&self, dev: T) {
|
||||
let mac = dev.mac_address();
|
||||
self.ports.borrow_mut().push(Box::new(dev));
|
||||
if let Some(stp) = self.stp.borrow_mut().as_mut() {
|
||||
stp.port_states.push(PortState::Forwarding);
|
||||
}
|
||||
let _ = mac;
|
||||
}
|
||||
|
||||
/// Enable STP with the given bridge priority and MAC.
|
||||
/// Must be called after all ports are added.
|
||||
pub fn enable_stp(&self, priority: u16, bridge_mac: EthernetAddress) {
|
||||
let port_count = self.ports.borrow().len();
|
||||
*self.stp.borrow_mut() = Some(StpState::new(priority, bridge_mac, port_count));
|
||||
}
|
||||
|
||||
fn learn(&self, mac: EthernetAddress, port: usize, now: Instant) {
|
||||
if !mac.is_unicast() {
|
||||
return;
|
||||
}
|
||||
self.mac_table.borrow_mut().insert(
|
||||
mac,
|
||||
MacEntry {
|
||||
port,
|
||||
last_seen: now,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn age_entries(&self, now: Instant) {
|
||||
self.mac_table
|
||||
.borrow_mut()
|
||||
.retain(|_, e| now < e.last_seen + MAC_AGE_TIMEOUT);
|
||||
}
|
||||
|
||||
fn lookup(&self, mac: EthernetAddress) -> Option<usize> {
|
||||
self.mac_table.borrow().get(&mac).map(|e| e.port)
|
||||
}
|
||||
|
||||
fn flood(&self, packet: &[u8], now: Instant, except_port: Option<usize>) {
|
||||
for (idx, port) in self.ports.borrow_mut().iter_mut().enumerate() {
|
||||
if Some(idx) == except_port {
|
||||
continue;
|
||||
}
|
||||
if self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(idx)) {
|
||||
continue;
|
||||
}
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), packet, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for BridgeDevice {
|
||||
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
if packet.len() < 14 {
|
||||
return;
|
||||
}
|
||||
let frame = EthernetFrame::new_unchecked(packet);
|
||||
let Ok(repr) = EthernetRepr::parse(&frame) else {
|
||||
return;
|
||||
};
|
||||
let dst_mac = repr.dst_addr;
|
||||
if repr.dst_addr.is_broadcast() || repr.dst_addr.is_multicast() {
|
||||
self.flood(packet, now, None);
|
||||
} else if let Some(port_idx) = self.lookup(dst_mac) {
|
||||
if self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(port_idx)) {
|
||||
return;
|
||||
}
|
||||
if let Some(port) = self.ports.borrow_mut().get_mut(port_idx) {
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), packet, now);
|
||||
}
|
||||
} else {
|
||||
self.flood(packet, now, None);
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, now: Instant) -> Option<&[u8]> {
|
||||
self.age_entries(now);
|
||||
|
||||
// STP hello timer — send periodic BPDUs if we're the root bridge
|
||||
if self.stp.borrow_mut().as_mut().is_some_and(|s| s.send_hello(now)) {
|
||||
let bpdu = {
|
||||
let stp = self.stp.borrow();
|
||||
stp.as_ref().map(|s| s.build_bpdu())
|
||||
};
|
||||
if let Some(bpdu) = bpdu {
|
||||
for port in self.ports.borrow_mut().iter_mut() {
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &bpdu, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut received: Option<(usize, Vec<u8>, EthernetAddress, EthernetProtocol)> = None;
|
||||
{
|
||||
let mut ports = self.ports.borrow_mut();
|
||||
for (port_idx, port) in ports.iter_mut().enumerate() {
|
||||
if let Some(buf) = port.recv(now) {
|
||||
let frame = EthernetFrame::new_unchecked(buf);
|
||||
let Ok(repr) = EthernetRepr::parse(&frame) else {
|
||||
continue;
|
||||
};
|
||||
self.learn(repr.src_addr, port_idx, now);
|
||||
received = Some((port_idx, buf.to_vec(), repr.dst_addr, repr.ethertype));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((port_idx, packet, dst_mac, ethertype)) = received {
|
||||
// BPDU: process via STP, don't forward
|
||||
if dst_mac == BPDU_MAC {
|
||||
let response = self.stp.borrow_mut().as_mut()
|
||||
.and_then(|s| s.process_bpdu(port_idx, &packet, now));
|
||||
if let Some(rsp) = response {
|
||||
if let Some(port) = self.ports.borrow_mut().get_mut(port_idx) {
|
||||
port.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &rsp, now);
|
||||
}
|
||||
}
|
||||
return None;
|
||||
}
|
||||
|
||||
if ethertype == EthernetProtocol::Arp
|
||||
|| ethertype == EthernetProtocol::Ipv4
|
||||
|| ethertype == EthernetProtocol::Ipv6
|
||||
{
|
||||
if dst_mac.is_broadcast() || dst_mac.is_multicast() {
|
||||
self.recv_buffer = packet.clone();
|
||||
self.flood(&self.recv_buffer, now, Some(port_idx));
|
||||
return Some(&self.recv_buffer);
|
||||
} else if let Some(dst_port_idx) = self.lookup(dst_mac) {
|
||||
if dst_port_idx != port_idx
|
||||
&& !self.stp.borrow().as_ref().is_some_and(|s| s.is_blocked(dst_port_idx))
|
||||
{
|
||||
let mut ports = self.ports.borrow_mut();
|
||||
if let Some(target) = ports.get_mut(dst_port_idx) {
|
||||
target.send(IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED), &packet, now);
|
||||
}
|
||||
return None;
|
||||
}
|
||||
}
|
||||
self.recv_buffer = packet;
|
||||
return Some(&self.recv_buffer);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
self.ports.borrow().iter().any(|p| p.can_recv())
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
let mut out = String::from("Bridge FDB:\n");
|
||||
for (mac, entry) in self.mac_table.borrow().iter() {
|
||||
out.push_str(&format!(" {} port={} last_seen={}\n", mac, entry.port, entry.last_seen));
|
||||
}
|
||||
if self.mac_table.borrow().is_empty() {
|
||||
out.push_str(" (empty)\n");
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
if self.ports.borrow().is_empty() { "down" } else { "up" }
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
let mut total = Stats::default();
|
||||
for port in self.ports.borrow().iter() {
|
||||
let s = port.statistics();
|
||||
total.rx_bytes += s.rx_bytes;
|
||||
total.rx_packets += s.rx_packets;
|
||||
total.tx_bytes += s.tx_bytes;
|
||||
total.tx_packets += s.tx_packets;
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for (i, port) in self.ports.borrow().iter().enumerate() {
|
||||
let s = port.arp_stats();
|
||||
if !s.is_empty() {
|
||||
out.push_str(&format!("port{}: {}", i, s));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
|
||||
fn mac(a: u8, b: u8, c: u8, d: u8, e: u8, f: u8) -> EthernetAddress {
|
||||
EthernetAddress([a, b, c, d, e, f])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn learn_stores_unicast_mapping() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let m = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
|
||||
b.learn(m, 2, Instant::from_secs(0));
|
||||
assert_eq!(b.lookup(m), Some(2),
|
||||
"Learned MAC must be found on port 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn learn_ignores_multicast() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let multicast = EthernetAddress::BROADCAST;
|
||||
b.learn(multicast, 1, Instant::from_secs(0));
|
||||
assert_eq!(b.lookup(multicast), None,
|
||||
"Multicast MACs must not be learned");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn learn_replaces_existing_entry_on_new_port() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let m = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
|
||||
b.learn(m, 1, Instant::from_secs(0));
|
||||
assert_eq!(b.lookup(m), Some(1));
|
||||
b.learn(m, 2, Instant::from_secs(10));
|
||||
assert_eq!(b.lookup(m), Some(2),
|
||||
"MAC move from port 1 to port 2 must update FDB");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn age_entries_removes_expired_macs() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
let m1 = mac(0x00, 0x11, 0x22, 0x33, 0x44, 0x55);
|
||||
let m2 = mac(0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE);
|
||||
b.learn(m1, 1, Instant::from_secs(0));
|
||||
b.learn(m2, 2, Instant::from_secs(MAC_AGE_TIMEOUT.secs() as i64)); // Age boundary
|
||||
// Age just past the 300s timeout
|
||||
b.age_entries(Instant::from_secs(MAC_AGE_TIMEOUT.secs() as i64 + 1));
|
||||
assert_eq!(b.lookup(m1), None, "m1 (learned at 0) must be aged out");
|
||||
assert_eq!(b.lookup(m2), Some(2),
|
||||
"m2 (learned at 300s) must still be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_returns_none_for_unknown_mac() {
|
||||
let b = BridgeDevice::new("br0");
|
||||
assert_eq!(b.lookup(mac(0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0x01)), None);
|
||||
}
|
||||
}
|
||||
+649
-47
@@ -4,14 +4,19 @@ use std::fs::File;
|
||||
use std::io::{ErrorKind, Read, Write};
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::phy::ChecksumCapabilities;
|
||||
use smoltcp::storage::PacketMetadata;
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::{
|
||||
ArpOperation, ArpPacket, ArpRepr, EthernetAddress, EthernetFrame, EthernetProtocol,
|
||||
EthernetRepr, IpAddress, IpCidr, Ipv4Address, Ipv4Cidr,
|
||||
EthernetRepr, Icmpv6Packet, Icmpv6Repr, IpAddress, IpCidr, IpProtocol, Ipv4Address,
|
||||
Ipv6Address, Ipv6Cidr, Ipv6Packet, NdiscNeighborFlags, NdiscPrefixInfoFlags, NdiscRepr,
|
||||
RawHardwareAddress,
|
||||
};
|
||||
|
||||
use super::LinkDevice;
|
||||
use crate::link::qdisc::QdiscConfig;
|
||||
use super::{LinkDevice, Stats};
|
||||
use crate::slaac;
|
||||
|
||||
struct Neighbor {
|
||||
hardware_address: EthernetAddress,
|
||||
@@ -29,20 +34,66 @@ enum ArpState {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum NdpState {
|
||||
#[default]
|
||||
Discovered,
|
||||
Discovering {
|
||||
target: Ipv6Address,
|
||||
tries: u32,
|
||||
silent_until: Instant,
|
||||
},
|
||||
}
|
||||
|
||||
type PacketBuffer = smoltcp::storage::PacketBuffer<'static, IpAddress>;
|
||||
|
||||
const EMPTY_MAC: EthernetAddress = EthernetAddress([0; 6]);
|
||||
const NDP_MAX_TRIES: u32 = 3;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
enum SlaacState {
|
||||
#[default]
|
||||
Idle,
|
||||
LinkLocalOnly,
|
||||
RsSent {
|
||||
silent_until: Instant,
|
||||
},
|
||||
Configured,
|
||||
}
|
||||
|
||||
fn solicited_node_multicast(target: &Ipv6Address) -> Ipv6Address {
|
||||
let o = target.octets();
|
||||
Ipv6Address::new(
|
||||
0xff02, 0, 0, 0, 0, 1, 0xff00 | (o[13] as u16), ((o[14] as u16) << 8) | (o[15] as u16),
|
||||
)
|
||||
}
|
||||
|
||||
fn multicast_mac(addr: &Ipv6Address) -> EthernetAddress {
|
||||
let o = addr.octets();
|
||||
EthernetAddress([0x33, 0x33, o[12], o[13], o[14], o[15]])
|
||||
}
|
||||
|
||||
pub struct EthernetLink {
|
||||
name: Rc<str>,
|
||||
neighbor_cache: BTreeMap<IpAddress, Neighbor>,
|
||||
arp_state: ArpState,
|
||||
ndp_state: NdpState,
|
||||
slaac_state: SlaacState,
|
||||
waiting_packets: PacketBuffer,
|
||||
input_buffer: Vec<u8>,
|
||||
output_buffer: Vec<u8>,
|
||||
network_file: File,
|
||||
hardware_address: Option<EthernetAddress>,
|
||||
ip_address: Option<Ipv4Cidr>,
|
||||
ip_address: Option<IpCidr>,
|
||||
qdisc_config: QdiscConfig,
|
||||
mtu: usize,
|
||||
enabled: bool,
|
||||
promiscuous: bool,
|
||||
stats: Stats,
|
||||
arp_requests: u64,
|
||||
arp_replies: u64,
|
||||
arp_cache_hits: u64,
|
||||
arp_cache_misses: u64,
|
||||
}
|
||||
|
||||
impl EthernetLink {
|
||||
@@ -53,6 +104,26 @@ impl EthernetLink {
|
||||
|
||||
const NEIGHBOR_LIVE_TIME: Duration = Duration::from_secs(60);
|
||||
const ARP_SILENCE_TIME: Duration = Duration::from_secs(1);
|
||||
const ARP_CACHE_MAX: usize = 1024;
|
||||
|
||||
fn insert_neighbor(&mut self, ip: IpAddress, hw: EthernetAddress, now: Instant) {
|
||||
if self.neighbor_cache.len() >= Self::ARP_CACHE_MAX
|
||||
&& !self.neighbor_cache.contains_key(&ip)
|
||||
{
|
||||
// Remove the entry with the earliest expiration time (LRU-style).
|
||||
if let Some((&oldest_ip, _)) = self.neighbor_cache
|
||||
.iter()
|
||||
.min_by_key(|(_, n)| n.expires_at)
|
||||
{
|
||||
self.neighbor_cache.remove(&oldest_ip);
|
||||
}
|
||||
}
|
||||
self.neighbor_cache.insert(ip, Neighbor {
|
||||
hardware_address: hw,
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
});
|
||||
}
|
||||
const NDP_SILENCE_TIME: Duration = Duration::from_secs(1);
|
||||
|
||||
pub fn new(name: &str, network_file: File) -> Self {
|
||||
let waiting_packets = PacketBuffer::new(
|
||||
@@ -69,7 +140,18 @@ impl EthernetLink {
|
||||
input_buffer: vec![0u8; Self::MTU],
|
||||
output_buffer: Vec::with_capacity(Self::MTU),
|
||||
arp_state: Default::default(),
|
||||
ndp_state: Default::default(),
|
||||
slaac_state: Default::default(),
|
||||
qdisc_config: QdiscConfig::default(),
|
||||
mtu: Self::MTU,
|
||||
enabled: true,
|
||||
promiscuous: false,
|
||||
stats: Stats::default(),
|
||||
neighbor_cache: Default::default(),
|
||||
arp_requests: 0,
|
||||
arp_replies: 0,
|
||||
arp_cache_hits: 0,
|
||||
arp_cache_misses: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +177,7 @@ impl EthernetLink {
|
||||
f(frame.payload_mut());
|
||||
|
||||
if let Err(_) = self.network_file.write_all(&self.output_buffer) {
|
||||
self.stats.tx_errors += 1;
|
||||
error!(
|
||||
"Dropped outboud packet on {} (failed to write to network file)",
|
||||
self.name
|
||||
@@ -107,13 +190,14 @@ impl EthernetLink {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(ip_addr) = self.ip_address else {
|
||||
let Some(IpCidr::Ipv4(ip_addr)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(repr) = ArpPacket::new_checked(packet).and_then(|packet| ArpRepr::parse(&packet))
|
||||
else {
|
||||
debug!("Dropped incomming arp packet on {} (Malformed)", self.name);
|
||||
self.stats.rx_errors += 1;
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -149,13 +233,12 @@ impl EthernetLink {
|
||||
return;
|
||||
}
|
||||
|
||||
self.neighbor_cache.insert(
|
||||
self.insert_neighbor(
|
||||
IpAddress::Ipv4(source_protocol_addr),
|
||||
Neighbor {
|
||||
hardware_address: source_hardware_addr,
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
source_hardware_addr,
|
||||
now,
|
||||
);
|
||||
self.arp_replies += 1;
|
||||
|
||||
if let ArpOperation::Request = operation {
|
||||
let response = ArpRepr::EthernetIpv4 {
|
||||
@@ -194,6 +277,10 @@ impl EthernetLink {
|
||||
self.send_arp(now);
|
||||
break;
|
||||
}
|
||||
Ok((IpAddress::Ipv6(_), _)) => {
|
||||
let _ = waiting_packets.dequeue();
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.arp_state = ArpState::Discovered;
|
||||
break;
|
||||
@@ -204,7 +291,7 @@ impl EthernetLink {
|
||||
self.send_to(
|
||||
mac,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(packet),
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
EthernetProtocol::Ipv4,
|
||||
);
|
||||
}
|
||||
@@ -227,6 +314,10 @@ impl EthernetLink {
|
||||
|
||||
return;
|
||||
}
|
||||
Ok((IpAddress::Ipv6(_), _)) => {
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.arp_state = ArpState::Discovered;
|
||||
return;
|
||||
@@ -234,6 +325,7 @@ impl EthernetLink {
|
||||
}
|
||||
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
self.stats.rx_dropped += 1;
|
||||
debug!(
|
||||
"Dropped packet on {} because neighbor was not found",
|
||||
self.name
|
||||
@@ -241,8 +333,76 @@ impl EthernetLink {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_waiting_packets_v6(&mut self, ip: Ipv6Address, mac: EthernetAddress) {
|
||||
let mut waiting_packets =
|
||||
std::mem::replace(&mut self.waiting_packets, PacketBuffer::new(vec![], vec![]));
|
||||
loop {
|
||||
match waiting_packets.peek() {
|
||||
Ok((IpAddress::Ipv6(dst), _)) if *dst == ip => {}
|
||||
Ok((IpAddress::Ipv6(dst), _)) => {
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target: *dst,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_ndp_solicit(Instant::ZERO);
|
||||
break;
|
||||
}
|
||||
Ok((IpAddress::Ipv4(_), _)) => {
|
||||
let _ = waiting_packets.dequeue();
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.ndp_state = NdpState::Discovered;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let (_, packet) = waiting_packets.dequeue().unwrap();
|
||||
self.send_to(
|
||||
mac,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
|
||||
self.waiting_packets = waiting_packets;
|
||||
}
|
||||
|
||||
fn drop_waiting_packets_v6(&mut self, ip: Ipv6Address) {
|
||||
loop {
|
||||
match self.waiting_packets.peek() {
|
||||
Ok((IpAddress::Ipv6(dst), _)) if *dst == ip => {}
|
||||
Ok((IpAddress::Ipv6(dst), _)) => {
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target: *dst,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_ndp_solicit(Instant::ZERO);
|
||||
return;
|
||||
}
|
||||
Ok((IpAddress::Ipv4(_), _)) => {
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
self.stats.rx_dropped += 1;
|
||||
continue;
|
||||
}
|
||||
Err(_) => {
|
||||
self.ndp_state = NdpState::Discovered;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.waiting_packets.dequeue();
|
||||
self.stats.rx_dropped += 1;
|
||||
debug!("Dropped packet on {} because IPv6 neighbor was not found", self.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_missing_neighbor(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let Ok(buf) = self.waiting_packets.enqueue(packet.len(), next_hop) else {
|
||||
self.stats.rx_dropped += 1;
|
||||
warn!(
|
||||
"Dropped packet on {} because waiting queue was full",
|
||||
self.name
|
||||
@@ -251,24 +411,37 @@ impl EthernetLink {
|
||||
};
|
||||
buf.copy_from_slice(packet);
|
||||
|
||||
let IpAddress::Ipv4(next_hop) = next_hop;
|
||||
if let ArpState::Discovered = self.arp_state {
|
||||
self.arp_state = ArpState::Discovering {
|
||||
target: next_hop,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
|
||||
self.send_arp(now)
|
||||
match next_hop {
|
||||
IpAddress::Ipv4(v4) => {
|
||||
if let ArpState::Discovered = self.arp_state {
|
||||
self.arp_state = ArpState::Discovering {
|
||||
target: v4,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_arp(now)
|
||||
}
|
||||
}
|
||||
IpAddress::Ipv6(v6) => {
|
||||
if let NdpState::Discovered = self.ndp_state {
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target: v6,
|
||||
tries: 0,
|
||||
silent_until: Instant::ZERO,
|
||||
};
|
||||
self.send_ndp_solicit(now)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_arp(&mut self, now: Instant) {
|
||||
self.arp_requests += 1;
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(ip_address) = self.ip_address else {
|
||||
let Some(IpCidr::Ipv4(ip_address)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -303,38 +476,321 @@ impl EthernetLink {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send_ndp_solicit(&mut self, now: Instant) {
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(IpCidr::Ipv6(_)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
// Extract target. Do NOT destructure tries/silent_until by value
|
||||
// — the recursive call into drop_waiting_packets_v6 can modify
|
||||
// self.ndp_state and a later write-back would clobber that
|
||||
// update. We read tries/silent_until from the live state right
|
||||
// before we write it back.
|
||||
let target = match self.ndp_state {
|
||||
NdpState::Discovered => return,
|
||||
NdpState::Discovering { target, .. } => target,
|
||||
};
|
||||
|
||||
{
|
||||
let (tries, silent_until) = match self.ndp_state {
|
||||
NdpState::Discovering { tries, silent_until, .. } => (tries, silent_until),
|
||||
NdpState::Discovered => return,
|
||||
};
|
||||
if silent_until > now {
|
||||
return;
|
||||
}
|
||||
if tries >= NDP_MAX_TRIES {
|
||||
self.drop_waiting_packets_v6(target);
|
||||
self.ndp_state = NdpState::Discovered;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let snmc = solicited_node_multicast(&target);
|
||||
let dst_mac = multicast_mac(&snmc);
|
||||
let ndisc = NdiscRepr::NeighborSolicit {
|
||||
target_addr: target,
|
||||
lladdr: Some(RawHardwareAddress::from(hardware_address)),
|
||||
};
|
||||
let icmp = Icmpv6Repr::Ndisc(ndisc);
|
||||
let mut buf = [0u8; 128];
|
||||
let mut icmp_pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
let src_addr = Ipv6Address::new(0, 0, 0, 0, 0, 0, 0, 0);
|
||||
let dst_addr = snmc;
|
||||
icmp.emit(&src_addr, &dst_addr, &mut icmp_pkt, &ChecksumCapabilities::ignored());
|
||||
let icmp_len = icmp.buffer_len();
|
||||
|
||||
// Read the current state (which may have been updated by a
|
||||
// recursive call into drop_waiting_packets_v6) and update
|
||||
// incrementally. This avoids overwriting a recursive update.
|
||||
let (mut tries, mut silent_until) = match self.ndp_state {
|
||||
NdpState::Discovering { tries, silent_until, .. } => (tries, silent_until),
|
||||
NdpState::Discovered => return,
|
||||
};
|
||||
tries += 1;
|
||||
silent_until = now + Self::NDP_SILENCE_TIME;
|
||||
self.ndp_state = NdpState::Discovering {
|
||||
target,
|
||||
tries,
|
||||
silent_until,
|
||||
};
|
||||
|
||||
self.send_to(
|
||||
dst_mac,
|
||||
icmp_len,
|
||||
|out| out[..icmp_len].copy_from_slice(&buf[..icmp_len]),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
|
||||
fn send_router_solicitation(&mut self, now: Instant) {
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(IpCidr::Ipv6(our_addr)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let addr_octets = our_addr.address().octets();
|
||||
if addr_octets[0] != 0xfe || addr_octets[1] & 0xc0 != 0x80 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let SlaacState::RsSent { silent_until } = self.slaac_state {
|
||||
if silent_until > now {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let dst_mac = EthernetAddress([0x33, 0x33, 0, 0, 0, 2]);
|
||||
let ndisc = NdiscRepr::RouterSolicit {
|
||||
lladdr: Some(RawHardwareAddress::from(hardware_address)),
|
||||
};
|
||||
let icmp = Icmpv6Repr::Ndisc(ndisc);
|
||||
let mut buf = [0u8; 128];
|
||||
let mut icmp_pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
icmp.emit(
|
||||
&our_addr.address(),
|
||||
&slaac::ALL_ROUTERS_MULTICAST,
|
||||
&mut icmp_pkt,
|
||||
&ChecksumCapabilities::ignored(),
|
||||
);
|
||||
let icmp_len = icmp.buffer_len();
|
||||
|
||||
self.slaac_state = SlaacState::RsSent {
|
||||
silent_until: now + Self::NDP_SILENCE_TIME,
|
||||
};
|
||||
|
||||
self.send_to(
|
||||
dst_mac,
|
||||
icmp_len,
|
||||
|out| out[..icmp_len].copy_from_slice(&buf[..icmp_len]),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
|
||||
fn process_ndp(&mut self, packet: &[u8], now: Instant) {
|
||||
let Some(hardware_address) = self.hardware_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(IpCidr::Ipv6(our_addr)) = self.ip_address else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Ok(ipv6_pkt) = Ipv6Packet::new_checked(packet) else {
|
||||
return;
|
||||
};
|
||||
if ipv6_pkt.next_header() != IpProtocol::Icmpv6 {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(icmp_pkt) = Icmpv6Packet::new_checked(ipv6_pkt.payload()) else {
|
||||
return;
|
||||
};
|
||||
let Ok(icmp_repr) = Icmpv6Repr::parse(
|
||||
&our_addr.address(),
|
||||
&ipv6_pkt.src_addr(),
|
||||
&icmp_pkt,
|
||||
&ChecksumCapabilities::ignored(),
|
||||
) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Icmpv6Repr::Ndisc(ndisc) = icmp_repr else {
|
||||
return;
|
||||
};
|
||||
|
||||
match ndisc {
|
||||
NdiscRepr::NeighborSolicit { target_addr, lladdr } => {
|
||||
if target_addr != our_addr.address() {
|
||||
return;
|
||||
}
|
||||
let src_addr = ipv6_pkt.src_addr();
|
||||
let Some(src_mac) = lladdr.map(|a| EthernetAddress::from_bytes(a.as_bytes()))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(src_addr),
|
||||
Neighbor {
|
||||
hardware_address: src_mac,
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
|
||||
let response = NdiscRepr::NeighborAdvert {
|
||||
flags: NdiscNeighborFlags::SOLICITED | NdiscNeighborFlags::OVERRIDE,
|
||||
target_addr: our_addr.address(),
|
||||
lladdr: Some(RawHardwareAddress::from(hardware_address)),
|
||||
};
|
||||
let icmp_resp = Icmpv6Repr::Ndisc(response);
|
||||
let mut buf = [0u8; 128];
|
||||
let mut icmp_pkt = Icmpv6Packet::new_unchecked(&mut buf);
|
||||
icmp_resp.emit(
|
||||
&our_addr.address(),
|
||||
&src_addr,
|
||||
&mut icmp_pkt,
|
||||
&ChecksumCapabilities::ignored(),
|
||||
);
|
||||
let resp_len = icmp_resp.buffer_len();
|
||||
|
||||
self.send_to(
|
||||
src_mac,
|
||||
resp_len,
|
||||
|out| out[..resp_len].copy_from_slice(&buf[..resp_len]),
|
||||
EthernetProtocol::Ipv6,
|
||||
);
|
||||
}
|
||||
NdiscRepr::NeighborAdvert { target_addr, lladdr, .. } => {
|
||||
let Some(mac) = lladdr.map(|a| EthernetAddress::from_bytes(a.as_bytes())) else {
|
||||
return;
|
||||
};
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(target_addr),
|
||||
Neighbor {
|
||||
hardware_address: mac,
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
self.check_waiting_packets_v6(target_addr, mac);
|
||||
}
|
||||
NdiscRepr::RouterSolicit { lladdr } => {
|
||||
if let Some(ll) = lladdr {
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(ipv6_pkt.src_addr()),
|
||||
Neighbor {
|
||||
hardware_address: EthernetAddress::from_bytes(ll.as_bytes()),
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
NdiscRepr::RouterAdvert {
|
||||
hop_limit,
|
||||
router_lifetime,
|
||||
reachable_time,
|
||||
retrans_time,
|
||||
lladdr,
|
||||
mtu,
|
||||
prefix_info,
|
||||
..
|
||||
} => {
|
||||
self.slaac_state = SlaacState::Configured;
|
||||
if let Some(ll) = lladdr {
|
||||
self.neighbor_cache.insert(
|
||||
IpAddress::Ipv6(ipv6_pkt.src_addr()),
|
||||
Neighbor {
|
||||
hardware_address: EthernetAddress::from_bytes(ll.as_bytes()),
|
||||
expires_at: now + Self::NEIGHBOR_LIVE_TIME,
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(pi) = prefix_info {
|
||||
if pi.flags.contains(NdiscPrefixInfoFlags::ADDRCONF) {
|
||||
let valid_lft = pi.valid_lifetime;
|
||||
if valid_lft > Duration::ZERO {
|
||||
let slaac_addr = slaac::form_slaac_addr(
|
||||
Ipv6Cidr::new(pi.prefix, pi.prefix_len),
|
||||
hardware_address,
|
||||
);
|
||||
self.ip_address = Some(IpCidr::Ipv6(Ipv6Cidr::new(
|
||||
slaac_addr,
|
||||
pi.prefix_len,
|
||||
)));
|
||||
info!(
|
||||
"SLAAC: configured {} on {} (via RA from {})",
|
||||
slaac_addr,
|
||||
self.name,
|
||||
ipv6_pkt.src_addr()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for EthernetLink {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let local_broadcast = match self.ip_address.and_then(|cidr| cidr.broadcast()) {
|
||||
Some(addr) => IpAddress::Ipv4(addr) == next_hop,
|
||||
None => false,
|
||||
let eth_proto = if !packet.is_empty() && packet[0] >> 4 == 6 {
|
||||
EthernetProtocol::Ipv6
|
||||
} else {
|
||||
EthernetProtocol::Ipv4
|
||||
};
|
||||
|
||||
let owned = self.qdisc_config.enqueue_and_dequeue(packet.to_vec(), now);
|
||||
let Some(packet) = owned else {
|
||||
self.stats.tx_dropped += 1;
|
||||
return;
|
||||
};
|
||||
self.stats.tx_packets += 1;
|
||||
self.stats.tx_bytes += packet.len() as u64;
|
||||
|
||||
let local_broadcast = match self.ip_address {
|
||||
Some(IpCidr::Ipv4(cidr)) => cidr
|
||||
.broadcast()
|
||||
.map(|addr| IpAddress::Ipv4(addr) == next_hop)
|
||||
.unwrap_or(false),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if local_broadcast || next_hop.is_broadcast() {
|
||||
self.send_to(
|
||||
EthernetAddress::BROADCAST,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(packet),
|
||||
EthernetProtocol::Ipv4,
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
eth_proto,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
match self.neighbor_cache.entry(next_hop) {
|
||||
Entry::Vacant(_) => self.handle_missing_neighbor(next_hop, packet, now),
|
||||
Entry::Vacant(_) => {
|
||||
self.arp_cache_misses += 1;
|
||||
self.handle_missing_neighbor(next_hop, &packet, now)
|
||||
}
|
||||
Entry::Occupied(e) => {
|
||||
if e.get().expires_at < now {
|
||||
e.remove();
|
||||
self.handle_missing_neighbor(next_hop, packet, now)
|
||||
self.arp_cache_misses += 1;
|
||||
self.handle_missing_neighbor(next_hop, &packet, now)
|
||||
} else {
|
||||
self.arp_cache_hits += 1;
|
||||
let mac = e.get().hardware_address;
|
||||
self.send_to(
|
||||
mac,
|
||||
packet.len(),
|
||||
|buf| buf.copy_from_slice(packet),
|
||||
EthernetProtocol::Ipv4,
|
||||
|buf| buf.copy_from_slice(&packet),
|
||||
eth_proto,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -352,33 +808,59 @@ impl LinkDevice for EthernetLink {
|
||||
if e.kind() != ErrorKind::WouldBlock {
|
||||
error!("Failed to read ethernet device on link {}", self.name);
|
||||
} else {
|
||||
// No packet to read but we check if we have arp to send
|
||||
// No packet to read but we check if we have neighbor discovery to send
|
||||
self.send_arp(now);
|
||||
self.send_ndp_solicit(now);
|
||||
self.send_router_solicitation(now);
|
||||
}
|
||||
self.input_buffer = input_buffer;
|
||||
return None;
|
||||
}
|
||||
let packet = EthernetFrame::new_unchecked(&input_buffer[..]);
|
||||
let Ok(repr) = EthernetRepr::parse(&packet) else {
|
||||
debug!("Dropped incomming frame on {} (Malformed)", self.name);
|
||||
continue;
|
||||
|
||||
let (is_for_us, ethertype) = {
|
||||
let packet = EthernetFrame::new_unchecked(&input_buffer[..]);
|
||||
let Ok(repr) = EthernetRepr::parse(&packet) else {
|
||||
debug!("Dropped incomming frame on {} (Malformed)", self.name);
|
||||
self.stats.rx_errors += 1;
|
||||
continue;
|
||||
};
|
||||
|
||||
let is_for_us = repr.dst_addr.is_broadcast()
|
||||
|| repr.dst_addr == EMPTY_MAC
|
||||
|| repr.dst_addr == hardware_address;
|
||||
(is_for_us, repr.ethertype)
|
||||
};
|
||||
|
||||
// We let EMPTY_MAC pass because somehow this is the mac used when net=redir is used
|
||||
if !repr.dst_addr.is_broadcast()
|
||||
&& repr.dst_addr != EMPTY_MAC
|
||||
&& repr.dst_addr != hardware_address
|
||||
{
|
||||
// Drop packets which are not for us
|
||||
if !is_for_us && !self.promiscuous {
|
||||
continue;
|
||||
}
|
||||
|
||||
match repr.ethertype {
|
||||
EthernetProtocol::Ipv4 => {
|
||||
match ethertype {
|
||||
EthernetProtocol::Ipv4 | EthernetProtocol::Ipv6 => {
|
||||
let payload_start = EthernetFrame::<&[u8]>::header_len();
|
||||
let payload_len = input_buffer.len().saturating_sub(payload_start);
|
||||
let next_header_byte = if payload_len >= 8 {
|
||||
input_buffer[payload_start + 6]
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let is_ndp = ethertype == EthernetProtocol::Ipv6
|
||||
&& payload_len >= 8
|
||||
&& next_header_byte == u8::from(IpProtocol::Icmpv6);
|
||||
if is_ndp {
|
||||
let ndp_packet = input_buffer[payload_start..].to_vec();
|
||||
self.process_ndp(&ndp_packet, now);
|
||||
continue;
|
||||
}
|
||||
self.input_buffer = input_buffer;
|
||||
return Some(EthernetFrame::new_unchecked(&self.input_buffer[..]).payload());
|
||||
self.stats.rx_packets += 1;
|
||||
self.stats.rx_bytes += self.input_buffer[payload_start..].len() as u64;
|
||||
return Some(&self.input_buffer[payload_start..]);
|
||||
}
|
||||
EthernetProtocol::Arp => {
|
||||
let packet = EthernetFrame::new_unchecked(&input_buffer[..]);
|
||||
self.process_arp(packet.payload(), now);
|
||||
}
|
||||
EthernetProtocol::Arp => self.process_arp(packet.payload(), now),
|
||||
_ => continue,
|
||||
}
|
||||
}
|
||||
@@ -398,15 +880,135 @@ impl LinkDevice for EthernetLink {
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
self.hardware_address = Some(addr)
|
||||
self.hardware_address = Some(addr);
|
||||
if self.ip_address.is_none() {
|
||||
let link_local = slaac::form_link_local(addr);
|
||||
self.ip_address = Some(IpCidr::Ipv6(link_local));
|
||||
info!("SLAAC: configured link-local {} on {}", link_local.address(), self.name);
|
||||
}
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
Some(IpCidr::Ipv4(self.ip_address?))
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
let IpCidr::Ipv4(addr) = addr;
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
let mut out = String::new();
|
||||
for (ip, neighbor) in &self.neighbor_cache {
|
||||
out.push_str(&format!("{} {}\n", ip, neighbor.hardware_address));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn flush_arp(&mut self) {
|
||||
self.neighbor_cache.clear();
|
||||
}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
format!(
|
||||
"requests={} replies={} hits={} misses={} entries={}\n",
|
||||
self.arp_requests,
|
||||
self.arp_replies,
|
||||
self.arp_cache_hits,
|
||||
self.arp_cache_misses,
|
||||
self.neighbor_cache.len(),
|
||||
)
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
self.stats
|
||||
}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
if !self.enabled {
|
||||
"down"
|
||||
} else if self.hardware_address.is_some() {
|
||||
"up"
|
||||
} else {
|
||||
"down"
|
||||
}
|
||||
}
|
||||
|
||||
fn qdisc_info(&self) -> String {
|
||||
match &self.qdisc_config {
|
||||
QdiscConfig::None => "none\n".to_string(),
|
||||
QdiscConfig::TokenBucket(tb) => {
|
||||
format!("token_bucket rate={} burst={} tokens={}\n", tb.rate(), tb.burst(), tb.tokens())
|
||||
}
|
||||
QdiscConfig::PriorityQueue(pq) => {
|
||||
format!("priority_queue len={} max={}\n", pq.len(), pq.max_len())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_qdisc(&mut self, kind: &str) -> Result<(), String> {
|
||||
match kind.trim() {
|
||||
"none" => {
|
||||
self.qdisc_config = QdiscConfig::None;
|
||||
Ok(())
|
||||
}
|
||||
"token_bucket" | "tbf" => {
|
||||
self.qdisc_config = QdiscConfig::TokenBucket(
|
||||
crate::link::qdisc::TokenBucket::new(1_000_000, 1500)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
"priority_queue" | "pfifo_fast" => {
|
||||
self.qdisc_config = QdiscConfig::PriorityQueue(
|
||||
crate::link::qdisc::PriorityQueue::new(1000)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
_ => Err(format!("unknown qdisc type: {}\n", kind)),
|
||||
}
|
||||
}
|
||||
|
||||
fn mtu(&self) -> usize {
|
||||
self.mtu
|
||||
}
|
||||
|
||||
fn set_mtu(&mut self, mtu: usize) {
|
||||
self.mtu = mtu.clamp(576, 9000);
|
||||
}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
fn add_static_neighbor(&mut self, ip: IpAddress, mac: [u8; 6]) -> Result<(), String> {
|
||||
let hw_addr = EthernetAddress(mac);
|
||||
if !hw_addr.is_unicast() {
|
||||
return Err(format!("{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x} is not a unicast MAC\n",
|
||||
mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]));
|
||||
}
|
||||
self.neighbor_cache.insert(
|
||||
ip,
|
||||
Neighbor {
|
||||
hardware_address: hw_addr,
|
||||
expires_at: Instant::from_millis(i64::MAX / 2),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_neighbor(&mut self, ip: IpAddress) -> bool {
|
||||
self.neighbor_cache.remove(&ip).is_some()
|
||||
}
|
||||
|
||||
fn is_promiscuous(&self) -> bool {
|
||||
self.promiscuous
|
||||
}
|
||||
|
||||
fn set_promiscuous(&mut self, enabled: bool) {
|
||||
self.promiscuous = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
//! GRE Tunnel (Generic Routing Encapsulation) — mirrors Linux 7.1's
|
||||
//! `net/ipv4/ip_gre.c`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/ipv4/ip_gre.c:601` — `ipgre_tunnel_xmit()` — GRE header prepend
|
||||
//! - `net/ipv4/ip_gre.c:430` — `ipgre_rcv()` — GRE decapsulation
|
||||
//! - `include/uapi/linux/if_tunnel.h` — GRE header format
|
||||
//! - `include/net/gre.h` — GRE protocol constants
|
||||
//!
|
||||
//! GRE encapsulates an inner packet in an outer IP header + GRE header
|
||||
//! (4+ bytes) for tunneling through an IP network. Supports optional
|
||||
//! GRE key for multiplexing multiple tunnels over the same endpoints.
|
||||
//!
|
||||
//! GRE Header (minimum 4 bytes):
|
||||
//! [Flags 2B][Protocol 2B]
|
||||
//! Flags: C=checksum, K=key, S=sequence
|
||||
//! Protocol: 0x0800 = IPv4, 0x86DD = IPv6
|
||||
//!
|
||||
//! With GRE key (8 bytes):
|
||||
//! [Flags 2B][Protocol 2B][Key 4B]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol,
|
||||
};
|
||||
|
||||
use super::{DeviceList, LinkDevice};
|
||||
|
||||
const GRE_PROTO_IPV4: u16 = 0x0800;
|
||||
const GRE_FLAG_KEY: u16 = 0x2000;
|
||||
|
||||
pub struct GreDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
gre_key: Option<u32>,
|
||||
gre_header: [u8; 8],
|
||||
gre_header_len: usize,
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl GreDevice {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
parent_name: &str,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
gre_key: Option<u32>,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
) -> Self {
|
||||
let mut header = [0u8; 8];
|
||||
let header_len = if let Some(key) = gre_key {
|
||||
header[0] = (GRE_FLAG_KEY >> 8) as u8;
|
||||
header[1] = (GRE_FLAG_KEY & 0xff) as u8;
|
||||
header[2] = (GRE_PROTO_IPV4 >> 8) as u8;
|
||||
header[3] = (GRE_PROTO_IPV4 & 0xff) as u8;
|
||||
header[4] = (key >> 24) as u8;
|
||||
header[5] = ((key >> 16) & 0xff) as u8;
|
||||
header[6] = ((key >> 8) & 0xff) as u8;
|
||||
header[7] = (key & 0xff) as u8;
|
||||
8
|
||||
} else {
|
||||
header[2] = (GRE_PROTO_IPV4 >> 8) as u8;
|
||||
header[3] = (GRE_PROTO_IPV4 & 0xff) as u8;
|
||||
4
|
||||
};
|
||||
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
devices,
|
||||
local_ip,
|
||||
remote_ip,
|
||||
gre_key,
|
||||
gre_header: header,
|
||||
gre_header_len: header_len,
|
||||
send_buffer: Vec::with_capacity(1500),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
recv_queue: VecDeque::new(),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn build_encapsulated(&mut self, inner_packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
let outer_header = Ipv4Repr {
|
||||
src_addr: self.local_ip,
|
||||
dst_addr: self.remote_ip,
|
||||
next_header: IpProtocol::from(47u8),
|
||||
payload_len: self.gre_header_len + inner_packet.len(),
|
||||
hop_limit: 64,
|
||||
};
|
||||
let ip_hdr_len = outer_header.buffer_len();
|
||||
let total_len = ip_hdr_len + self.gre_header_len + inner_packet.len();
|
||||
self.send_buffer.resize(total_len, 0);
|
||||
let mut ip_pkt = Ipv4Packet::new_unchecked(&mut self.send_buffer);
|
||||
outer_header.emit(&mut ip_pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
let gre_start = ip_hdr_len;
|
||||
self.send_buffer[gre_start..gre_start + self.gre_header_len]
|
||||
.copy_from_slice(&self.gre_header[..self.gre_header_len]);
|
||||
self.send_buffer[gre_start + self.gre_header_len..]
|
||||
.copy_from_slice(inner_packet);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn matches_endpoint(&self, outer_packet: &[u8]) -> bool {
|
||||
if outer_packet.len() < 24 {
|
||||
return false;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(outer_packet) else {
|
||||
return false;
|
||||
};
|
||||
if u8::from(ipv4.next_header()) != 47 || ipv4.dst_addr() != self.local_ip {
|
||||
return false;
|
||||
}
|
||||
let ip_header_len = 20;
|
||||
let gre = &outer_packet[ip_header_len..];
|
||||
if gre.len() < 4 {
|
||||
return false;
|
||||
}
|
||||
let flags = u16::from_be_bytes([gre[0], gre[1]]);
|
||||
let proto = u16::from_be_bytes([gre[2], gre[3]]);
|
||||
if proto != GRE_PROTO_IPV4 {
|
||||
return false;
|
||||
}
|
||||
if let Some(expected_key) = self.gre_key {
|
||||
if flags & GRE_FLAG_KEY == 0 || gre.len() < 8 {
|
||||
return false;
|
||||
}
|
||||
let actual_key = u32::from_be_bytes([gre[4], gre[5], gre[6], gre[7]]);
|
||||
if actual_key != expected_key {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for GreDevice {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let encapsulated = self.build_encapsulated(packet).to_vec();
|
||||
if let Some(parent) = self.devices.borrow_mut().get_mut(&self.parent_name) {
|
||||
parent.send(next_hop, &encapsulated, now);
|
||||
} else {
|
||||
log::debug!("gre: parent {} not found, dropping {} byte frame",
|
||||
self.parent_name, packet.len());
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
if !self.matches_endpoint(&packet) {
|
||||
return None;
|
||||
}
|
||||
let ip_header_len = 20;
|
||||
let gre_header_len = if self.gre_key.is_some() { 8 } else { 4 };
|
||||
let payload_start = ip_header_len + gre_header_len;
|
||||
let payload = &packet[payload_start..];
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(payload);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! IPIP Tunnel (IP-in-IP) — mirrors Linux 7.1's `net/ipv4/ipip.c`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/ipv4/ipip.c:184` — `ipip_tunnel_xmit()` — build outer header, transmit
|
||||
//! - `net/ipv4/ip_tunnel.c:326` — `ip_tunnel_rcv()` — receive and decapsulate
|
||||
//! - `include/uapi/linux/in.h:30` — `IPPROTO_IPIP = 4`
|
||||
//!
|
||||
//! The IPIP tunnel wraps an inner IP packet in an outer IP header, routing
|
||||
//! it through a parent link-layer device to a remote endpoint. On reception,
|
||||
//! the outer header is stripped and the inner packet is delivered to the
|
||||
//! network stack.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol,
|
||||
};
|
||||
|
||||
use super::{DeviceList, LinkDevice};
|
||||
|
||||
const IPIP_PROTO: u8 = 4;
|
||||
|
||||
pub struct IpipDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl IpipDevice {
|
||||
pub fn new(name: &str, parent_name: &str, local_ip: Ipv4Address, remote_ip: Ipv4Address, devices: Rc<RefCell<DeviceList>>) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
devices,
|
||||
local_ip,
|
||||
remote_ip,
|
||||
send_buffer: Vec::with_capacity(1500),
|
||||
recv_buffer: Vec::with_capacity(1500),
|
||||
recv_queue: VecDeque::new(),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn build_outer_header(&mut self, inner_packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
let outer_header = Ipv4Repr {
|
||||
src_addr: self.local_ip,
|
||||
dst_addr: self.remote_ip,
|
||||
next_header: IpProtocol::from(IPIP_PROTO),
|
||||
payload_len: inner_packet.len(),
|
||||
hop_limit: 64,
|
||||
};
|
||||
let header_len = outer_header.buffer_len();
|
||||
self.send_buffer.resize(header_len + inner_packet.len(), 0);
|
||||
let mut ip_pkt = Ipv4Packet::new_unchecked(&mut self.send_buffer);
|
||||
outer_header.emit(&mut ip_pkt, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
self.send_buffer[header_len..].copy_from_slice(inner_packet);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn matches_endpoint(&self, outer_packet: &[u8]) -> bool {
|
||||
if outer_packet.len() < 20 {
|
||||
return false;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(outer_packet) else {
|
||||
return false;
|
||||
};
|
||||
u8::from(ipv4.next_header()) == IPIP_PROTO && ipv4.dst_addr() == self.local_ip
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for IpipDevice {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let encapsulated = self.build_outer_header(packet).to_vec();
|
||||
if let Some(parent) = self.devices.borrow_mut().get_mut(&self.parent_name) {
|
||||
parent.send(next_hop, &encapsulated, now);
|
||||
} else {
|
||||
log::debug!("ipip: parent {} not found, dropping {} byte frame",
|
||||
self.parent_name, packet.len());
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
if !self.matches_endpoint(&packet) {
|
||||
return None;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(&packet) else {
|
||||
return None;
|
||||
};
|
||||
let header_len = 20;
|
||||
let payload = &packet[header_len..];
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(payload);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ pub type PacketBuffer = smoltcp::storage::PacketBuffer<'static, ()>;
|
||||
pub struct LoopbackDevice {
|
||||
name: Rc<str>,
|
||||
buffer: PacketBuffer,
|
||||
enabled: bool,
|
||||
promiscuous: bool,
|
||||
}
|
||||
|
||||
impl Default for LoopbackDevice {
|
||||
@@ -23,6 +25,8 @@ impl Default for LoopbackDevice {
|
||||
LoopbackDevice {
|
||||
name: "loopback".into(),
|
||||
buffer,
|
||||
enabled: true,
|
||||
promiscuous: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +61,30 @@ impl LinkDevice for LoopbackDevice {
|
||||
Some("127.0.0.1/8".parse().unwrap())
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, _addr: smoltcp::wire::IpCidr) {
|
||||
todo!()
|
||||
fn set_ip_address(&mut self, _addr: smoltcp::wire::IpCidr) {}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
self.enabled
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, enabled: bool) {
|
||||
self.enabled = enabled;
|
||||
}
|
||||
|
||||
fn is_promiscuous(&self) -> bool {
|
||||
self.promiscuous
|
||||
}
|
||||
|
||||
fn set_promiscuous(&mut self, enabled: bool) {
|
||||
self.promiscuous = enabled;
|
||||
}
|
||||
|
||||
fn mtu(&self) -> usize {
|
||||
1500
|
||||
}
|
||||
|
||||
fn set_mtu(&mut self, _mtu: usize) {
|
||||
// Loopback's packet buffer is fixed at 1500 bytes at construction;
|
||||
// reallocation is not supported. The new value is ignored.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
pub mod bond;
|
||||
pub mod bridge;
|
||||
pub mod ethernet;
|
||||
pub mod gre;
|
||||
pub mod ipip;
|
||||
pub mod loopback;
|
||||
pub mod qdisc;
|
||||
pub mod stp;
|
||||
pub mod tun;
|
||||
pub mod vlan;
|
||||
pub mod vxlan;
|
||||
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -29,6 +38,70 @@ pub trait LinkDevice {
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr>;
|
||||
fn set_ip_address(&mut self, addr: IpCidr);
|
||||
|
||||
fn arp_table(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn flush_arp(&mut self) {}
|
||||
|
||||
fn arp_stats(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn qdisc_info(&self) -> String {
|
||||
"none\n".to_string()
|
||||
}
|
||||
|
||||
fn set_qdisc(&mut self, _kind: &str) -> Result<(), String> {
|
||||
Err("qdisc configuration not supported on this device\n".to_string())
|
||||
}
|
||||
|
||||
fn add_static_neighbor(&mut self, _ip: IpAddress, _mac: [u8; 6]) -> Result<(), String> {
|
||||
Err("static ARP entry not supported on this device\n".to_string())
|
||||
}
|
||||
|
||||
fn remove_neighbor(&mut self, _ip: IpAddress) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_promiscuous(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn set_promiscuous(&mut self, _enabled: bool) {}
|
||||
|
||||
fn mtu(&self) -> usize {
|
||||
1500
|
||||
}
|
||||
|
||||
fn set_mtu(&mut self, _mtu: usize) {}
|
||||
|
||||
fn is_enabled(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn set_enabled(&mut self, _enabled: bool) {}
|
||||
|
||||
fn link_state(&self) -> &'static str {
|
||||
"unknown"
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
Stats::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub struct Stats {
|
||||
pub rx_bytes: u64,
|
||||
pub rx_packets: u64,
|
||||
pub tx_bytes: u64,
|
||||
pub tx_packets: u64,
|
||||
pub rx_errors: u64,
|
||||
pub tx_errors: u64,
|
||||
pub rx_dropped: u64,
|
||||
pub tx_dropped: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
//! Traffic Control (qdisc) — mirrors Linux 7.1's `net/sched/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/sched/sch_tbf.c` — Token Bucket Filter (`tbf_enqueue`, `tbf_dequeue`)
|
||||
//! - `net/sched/sch_prio.c` — Priority queue (`prio_enqueue`, `prio_dequeue`)
|
||||
//! - `net/sched/sch_api.c` — Qdisc registration and API
|
||||
//!
|
||||
//! Two qdiscs are implemented:
|
||||
//! - **TokenBucket**: rate-limits outgoing packets using a token bucket.
|
||||
//! Packets exceeding the rate are dropped. Mirrors `TBF`.
|
||||
//! - **PriorityQueue**: three FIFO bands prioritized by IP TOS field.
|
||||
//! Mirrors `pfifo_fast` (the default Linux qdisc).
|
||||
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TokenBucket {
|
||||
rate: u64,
|
||||
burst: u64,
|
||||
tokens: u64,
|
||||
last_update: Instant,
|
||||
}
|
||||
|
||||
impl TokenBucket {
|
||||
pub fn new(rate_bps: u64, burst_bytes: u64) -> Self {
|
||||
Self { rate: rate_bps, burst: burst_bytes.max(1500), tokens: burst_bytes.max(1500), last_update: Instant::from_millis(0) }
|
||||
}
|
||||
pub fn rate(&self) -> u64 { self.rate }
|
||||
pub fn burst(&self) -> u64 { self.burst }
|
||||
pub fn tokens(&self) -> u64 { self.tokens }
|
||||
|
||||
pub fn set_rate(&mut self, rate_bps: u64) {
|
||||
self.rate = rate_bps;
|
||||
}
|
||||
|
||||
pub fn set_burst(&mut self, burst_bytes: u64) {
|
||||
self.burst = burst_bytes.max(1500);
|
||||
if self.tokens > self.burst {
|
||||
self.tokens = self.burst;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn consume(&mut self, bytes: u64, now: Instant) -> bool {
|
||||
let elapsed = now - self.last_update;
|
||||
if elapsed > Duration::ZERO {
|
||||
// Saturating mul: rate (bytes/s) * elapsed (ms) / 8000.
|
||||
// saturating_mul prevents overflow on long elapsed durations.
|
||||
let token_add = self.rate
|
||||
.saturating_mul(elapsed.total_millis() as u64)
|
||||
/ 8000;
|
||||
self.tokens = (self.tokens + token_add).min(self.burst);
|
||||
self.last_update = now;
|
||||
}
|
||||
if self.tokens >= bytes {
|
||||
self.tokens -= bytes;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PriorityQueue {
|
||||
bands: [VecDeque<Vec<u8>>; 3],
|
||||
max_len: usize,
|
||||
}
|
||||
|
||||
impl PriorityQueue {
|
||||
pub fn new(max_len: usize) -> Self { Self { bands: [VecDeque::new(), VecDeque::new(), VecDeque::new()], max_len } }
|
||||
pub fn max_len(&self) -> usize { self.max_len }
|
||||
|
||||
fn classify(packet: &[u8]) -> usize {
|
||||
if packet.len() < 2 || packet[0] >> 4 != 4 {
|
||||
return 1;
|
||||
}
|
||||
let tos = if packet.len() > 1 { packet[1] } else { 0 };
|
||||
let precedence = tos >> 5;
|
||||
if precedence >= 6 {
|
||||
0
|
||||
} else if precedence >= 4 {
|
||||
1
|
||||
} else {
|
||||
2
|
||||
}
|
||||
}
|
||||
|
||||
pub fn enqueue(&mut self, packet: Vec<u8>) -> bool {
|
||||
let band = Self::classify(&packet);
|
||||
if self.bands[band].len() >= self.max_len {
|
||||
return false;
|
||||
}
|
||||
self.bands[band].push_back(packet);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn dequeue(&mut self) -> Option<Vec<u8>> {
|
||||
for band in 0..3 {
|
||||
if let Some(packet) = self.bands[band].pop_front() {
|
||||
return Some(packet);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.bands[0].len() + self.bands[1].len() + self.bands[2].len()
|
||||
}
|
||||
|
||||
pub fn flush(&mut self) -> Vec<Vec<u8>> {
|
||||
let mut result = Vec::new();
|
||||
while let Some(pkt) = self.dequeue() {
|
||||
result.push(pkt);
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum QdiscConfig {
|
||||
None,
|
||||
TokenBucket(TokenBucket),
|
||||
PriorityQueue(PriorityQueue),
|
||||
}
|
||||
|
||||
impl Default for QdiscConfig {
|
||||
fn default() -> Self {
|
||||
Self::None
|
||||
}
|
||||
}
|
||||
|
||||
impl QdiscConfig {
|
||||
pub fn enqueue_and_dequeue(
|
||||
&mut self,
|
||||
packet: Vec<u8>,
|
||||
now: Instant,
|
||||
) -> Option<Vec<u8>> {
|
||||
match self {
|
||||
QdiscConfig::None => Some(packet),
|
||||
QdiscConfig::TokenBucket(tb) => {
|
||||
if tb.consume(packet.len() as u64, now) {
|
||||
Some(packet)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
QdiscConfig::PriorityQueue(pq) => {
|
||||
pq.enqueue(packet);
|
||||
pq.dequeue()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
//! Spanning Tree Protocol (IEEE 802.1D) — mirrors Linux 7.1's `net/bridge/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/bridge/br_stp.c` — STP state machine (`br_set_state`, `br_become_root_bridge`)
|
||||
//! - `net/bridge/br_stp_bpdu.c` — BPDU handling (`br_stp_rcv`, `br_send_config_bpdu`)
|
||||
//! - `net/bridge/br_stp_timer.c` — timers (`br_hello_timer_expired`, `br_tcn_timer_expired`)
|
||||
//! - `net/bridge/br_private_stp.h` — port roles/states
|
||||
//!
|
||||
//! Prevents Ethernet loops by selectively blocking ports. Uses BPDU messages
|
||||
//! (multicast to 01:80:c2:00:00:00) to elect a root bridge and compute the
|
||||
//! spanning tree. Ports are either Forwarding or Blocking (simplified from the
|
||||
//! full 802.1D state machine: Blocking→Listening→Learning→Forwarding).
|
||||
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::EthernetAddress;
|
||||
|
||||
pub const BPDU_MAC: EthernetAddress = EthernetAddress([0x01, 0x80, 0xc2, 0x00, 0x00, 0x00]);
|
||||
pub const DEFAULT_PRIORITY: u16 = 32768;
|
||||
pub const DEFAULT_HELLO: Duration = Duration::from_secs(2);
|
||||
pub const DEFAULT_MAX_AGE: Duration = Duration::from_secs(20);
|
||||
pub const DEFAULT_FORWARD_DELAY: Duration = Duration::from_secs(15);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PortState {
|
||||
Blocking,
|
||||
Forwarding,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StpState {
|
||||
pub bridge_priority: u16,
|
||||
pub bridge_mac: EthernetAddress,
|
||||
pub root_id: u64,
|
||||
pub root_path_cost: u32,
|
||||
pub root_port: Option<usize>,
|
||||
pub hello_timer: Instant,
|
||||
pub port_states: Vec<PortState>,
|
||||
}
|
||||
|
||||
impl StpState {
|
||||
pub fn new(priority: u16, mac: EthernetAddress, port_count: usize) -> Self {
|
||||
let root_id = ((priority as u64) << 48) | mac_to_u64(mac);
|
||||
Self {
|
||||
bridge_priority: priority,
|
||||
bridge_mac: mac,
|
||||
root_id,
|
||||
root_path_cost: 0,
|
||||
root_port: None,
|
||||
hello_timer: Instant::from_millis(0),
|
||||
port_states: vec![PortState::Forwarding; port_count],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bridge_id(&self) -> u64 {
|
||||
((self.bridge_priority as u64) << 48) | mac_to_u64(self.bridge_mac)
|
||||
}
|
||||
|
||||
pub fn send_hello(&mut self, now: Instant) -> bool {
|
||||
if now < self.hello_timer + DEFAULT_HELLO {
|
||||
return false;
|
||||
}
|
||||
self.hello_timer = now;
|
||||
true
|
||||
}
|
||||
|
||||
pub fn process_bpdu(
|
||||
&mut self,
|
||||
port_idx: usize,
|
||||
data: &[u8],
|
||||
now: Instant,
|
||||
) -> Option<Vec<u8>> {
|
||||
let bpdu = BpduMessage::parse(data)?;
|
||||
|
||||
if bpdu.root_id < self.root_id {
|
||||
self.root_id = bpdu.root_id;
|
||||
self.root_path_cost = bpdu.root_path_cost + self.port_cost();
|
||||
self.root_port = Some(port_idx);
|
||||
return Some(self.build_bpdu());
|
||||
}
|
||||
|
||||
if bpdu.root_id == self.root_id {
|
||||
if port_idx == self.root_port.unwrap_or(usize::MAX) {
|
||||
self.root_path_cost = bpdu.root_path_cost + self.port_cost();
|
||||
} else if bpdu.root_path_cost < self.root_path_cost {
|
||||
self.port_states[port_idx] = PortState::Blocking;
|
||||
}
|
||||
}
|
||||
|
||||
if self.bridge_id() < bpdu.bridge_id && bpdu.root_id == self.root_id {
|
||||
self.root_id = self.bridge_id();
|
||||
self.root_path_cost = 0;
|
||||
self.root_port = None;
|
||||
for state in &mut self.port_states {
|
||||
*state = PortState::Forwarding;
|
||||
}
|
||||
return Some(self.build_bpdu());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
pub fn is_blocked(&self, port_idx: usize) -> bool {
|
||||
port_idx < self.port_states.len() && self.port_states[port_idx] == PortState::Blocking
|
||||
}
|
||||
|
||||
fn port_cost(&self) -> u32 {
|
||||
4
|
||||
}
|
||||
|
||||
pub fn build_bpdu(&self) -> Vec<u8> {
|
||||
let mut buf = vec![0u8; 35];
|
||||
buf[0..2].copy_from_slice(&[0x00, 0x00]);
|
||||
buf[2] = 0x00;
|
||||
buf[3] = 0x00;
|
||||
buf[4] = 0x00;
|
||||
buf[5..13].copy_from_slice(&self.root_id.to_be_bytes());
|
||||
buf[13..17].copy_from_slice(&self.root_path_cost.to_be_bytes());
|
||||
buf[17..25].copy_from_slice(&self.bridge_id().to_be_bytes());
|
||||
buf[25..27].copy_from_slice(&0u16.to_be_bytes());
|
||||
buf[27..29].copy_from_slice(&0u16.to_be_bytes());
|
||||
// IEEE 802.1D timer fields are in units of 1/256 second.
|
||||
// 1 second = 256 ticks. So seconds * 256 = ticks.
|
||||
let to_ticks = |d: Duration| -> u16 {
|
||||
// total_millis returns i64; convert to u16 ticks (1s = 256 ticks).
|
||||
// Clamp to u16::MAX to avoid overflow.
|
||||
((d.total_millis() as u64).wrapping_mul(256).wrapping_div(1000) as u16)
|
||||
};
|
||||
buf[29..31].copy_from_slice(&to_ticks(DEFAULT_MAX_AGE).to_be_bytes());
|
||||
buf[31..33].copy_from_slice(&to_ticks(DEFAULT_HELLO).to_be_bytes());
|
||||
buf[33..35].copy_from_slice(&to_ticks(DEFAULT_FORWARD_DELAY).to_be_bytes());
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
struct BpduMessage {
|
||||
root_id: u64,
|
||||
root_path_cost: u32,
|
||||
bridge_id: u64,
|
||||
}
|
||||
|
||||
impl BpduMessage {
|
||||
fn parse(data: &[u8]) -> Option<Self> {
|
||||
if data.len() < 35 || data[0..2] != [0x00, 0x00] || data[2] != 0x00 {
|
||||
return None;
|
||||
}
|
||||
let root_id = u64::from_be_bytes(data[5..13].try_into().ok()?);
|
||||
let root_path_cost = u32::from_be_bytes(data[13..17].try_into().ok()?);
|
||||
let bridge_id = u64::from_be_bytes(data[17..25].try_into().ok()?);
|
||||
Some(Self { root_id, root_path_cost, bridge_id })
|
||||
}
|
||||
}
|
||||
|
||||
fn mac_to_u64(mac: EthernetAddress) -> u64 {
|
||||
let b = mac.as_bytes();
|
||||
((b[0] as u64) << 40) | ((b[1] as u64) << 32) | ((b[2] as u64) << 24)
|
||||
| ((b[3] as u64) << 16) | ((b[4] as u64) << 8) | (b[5] as u64)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_minimal_bpdu() -> Vec<u8> {
|
||||
// Minimal config BPDU: protocol=0x0000, version=0, type=0, flags=0,
|
||||
// root_id(8) + cost(4) + bridge_id(8) + port(2) + age(2) + max_age(2) +
|
||||
// hello(2) + fwd(2) = 35 bytes.
|
||||
let mut p = vec![0u8; 35];
|
||||
p[0] = 0x00; p[1] = 0x00; // protocol id
|
||||
p[2] = 0x00; // version
|
||||
p[3] = 0x00; // type = config
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bpdu_minimal_parses() {
|
||||
let p = make_minimal_bpdu();
|
||||
let m = BpduMessage::parse(&p);
|
||||
assert!(m.is_some(), "Valid config BPDU should parse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bpdu_short_returns_none() {
|
||||
let p = vec![0u8; 10];
|
||||
assert!(BpduMessage::parse(&p).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bpdu_wrong_protocol_returns_none() {
|
||||
let mut p = make_minimal_bpdu();
|
||||
p[0] = 0x80;
|
||||
assert!(BpduMessage::parse(&p).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_bpdu_does_not_panic() {
|
||||
// Regression test: build_bpdu used to panic because
|
||||
// Duration::total_millis() returns i64 (8 bytes) but the
|
||||
// destination buffer was only 2 bytes.
|
||||
let stp = StpState::new(32768, EthernetAddress([0,0,0,0,0,0]), 1);
|
||||
let buf = stp.build_bpdu();
|
||||
// Must produce 35 bytes
|
||||
assert_eq!(buf.len(), 35);
|
||||
// And must be a valid BPDU
|
||||
assert_eq!(&buf[0..2], &[0x00, 0x00]);
|
||||
assert_eq!(buf[2], 0x00); // version
|
||||
assert_eq!(buf[3], 0x00); // type
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//! TUN virtual network device — mirrors Linux 7.1's `drivers/net/tun.c`.
|
||||
//!
|
||||
//! The TUN device operates at layer 3 (IP), providing a virtual network
|
||||
//! interface that exchanges raw IP packets with a userspace program via
|
||||
//! the `scheme:tun` interface. This enables VPN software, custom routing
|
||||
//! daemons, and container networking.
|
||||
//!
|
||||
//! Linux reference: `tun_net_xmit()` (kernel→userspace) and
|
||||
//! `tun_get_user()` (userspace→kernel) in `drivers/net/tun.c`.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr};
|
||||
|
||||
use super::LinkDevice;
|
||||
use super::Stats;
|
||||
|
||||
pub type PacketQueue = Rc<RefCell<VecDeque<Vec<u8>>>>;
|
||||
|
||||
pub struct TunDevice {
|
||||
name: Rc<str>,
|
||||
rx_queue: PacketQueue,
|
||||
tx_queue: PacketQueue,
|
||||
recv_buffer: Vec<u8>,
|
||||
ip_address: Option<IpCidr>,
|
||||
stats: Stats,
|
||||
}
|
||||
|
||||
impl TunDevice {
|
||||
pub fn new(name: &str, rx: PacketQueue, tx: PacketQueue) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
rx_queue: rx,
|
||||
tx_queue: tx,
|
||||
recv_buffer: Vec::new(),
|
||||
ip_address: None,
|
||||
stats: Stats::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for TunDevice {
|
||||
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], _now: Instant) {
|
||||
self.stats.tx_bytes += packet.len() as u64;
|
||||
self.stats.tx_packets += 1;
|
||||
self.tx_queue.borrow_mut().push_back(packet.to_vec());
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
self.recv_buffer = self.rx_queue.borrow_mut().pop_front()?;
|
||||
self.stats.rx_bytes += self.recv_buffer.len() as u64;
|
||||
self.stats.rx_packets += 1;
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.rx_queue.borrow().is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, _addr: EthernetAddress) {}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
|
||||
fn statistics(&self) -> Stats {
|
||||
self.stats
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
//! 802.1Q VLAN device — mirrors Linux 7.1's `net/8021q/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `net/8021q/vlan_dev.c` — `vlan_dev_hard_start_xmit()` (send path)
|
||||
//! - `include/linux/if_vlan.h:411` — `__vlan_insert_tag()` (tag insertion)
|
||||
//! - `include/uapi/linux/if_ether.h:44` — `ETH_P_8021Q = 0x8100`
|
||||
//!
|
||||
//! The VLAN device wraps a parent link-layer device and inserts/strips
|
||||
//! the 4-byte 802.1Q tag (TPID 0x8100 + TCI) before/after the Ethernet
|
||||
//! header on each frame.
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, EthernetFrame, EthernetProtocol, IpAddress, IpCidr,
|
||||
};
|
||||
|
||||
use super::{DeviceList, LinkDevice};
|
||||
|
||||
const TPID_8021Q: u16 = 0x8100;
|
||||
|
||||
pub struct VlanDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
vlan_id: u16,
|
||||
priority: u8,
|
||||
tag: [u8; 4],
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
mac_address: Option<EthernetAddress>,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl VlanDevice {
|
||||
pub fn new(name: &str, parent_name: &str, vlan_id: u16, devices: Rc<RefCell<DeviceList>>) -> Self {
|
||||
let tci: u16 = vlan_id & 0x0fff;
|
||||
let tag: [u8; 4] = [
|
||||
(TPID_8021Q >> 8) as u8,
|
||||
(TPID_8021Q & 0xff) as u8,
|
||||
(tci >> 8) as u8,
|
||||
(tci & 0xff) as u8,
|
||||
];
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
devices,
|
||||
vlan_id,
|
||||
priority: 0,
|
||||
tag,
|
||||
send_buffer: Vec::with_capacity(1522),
|
||||
recv_buffer: Vec::with_capacity(1522),
|
||||
recv_queue: VecDeque::new(),
|
||||
mac_address: None,
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vlan_id(&self) -> u16 {
|
||||
self.vlan_id
|
||||
}
|
||||
|
||||
pub fn parent_name(&self) -> &str {
|
||||
&self.parent_name
|
||||
}
|
||||
|
||||
pub fn set_priority(&mut self, pcp: u8) {
|
||||
self.priority = pcp & 0x07;
|
||||
let tci: u16 = ((self.priority as u16) << 13) | (self.vlan_id & 0x0fff);
|
||||
self.tag[2] = (tci >> 8) as u8;
|
||||
self.tag[3] = (tci & 0xff) as u8;
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn insert_tag(&mut self, packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
if packet.len() < 14 {
|
||||
self.send_buffer.extend_from_slice(packet);
|
||||
return &self.send_buffer;
|
||||
}
|
||||
self.send_buffer.extend_from_slice(&packet[..12]);
|
||||
self.send_buffer.extend_from_slice(&self.tag);
|
||||
self.send_buffer.extend_from_slice(&packet[12..]);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn strip_tag(&mut self, packet: &[u8]) -> Option<&[u8]> {
|
||||
if packet.len() < 18 {
|
||||
return None;
|
||||
}
|
||||
let tpid = u16::from_be_bytes([packet[12], packet[13]]);
|
||||
if tpid != TPID_8021Q {
|
||||
return None;
|
||||
}
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(&packet[..12]);
|
||||
self.recv_buffer.extend_from_slice(&packet[16..]);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
pub fn matches_tag(&self, packet: &[u8]) -> bool {
|
||||
if packet.len() < 18 {
|
||||
return false;
|
||||
}
|
||||
let tpid = u16::from_be_bytes([packet[12], packet[13]]);
|
||||
if tpid != TPID_8021Q {
|
||||
return false;
|
||||
}
|
||||
let tci = u16::from_be_bytes([packet[14], packet[15]]);
|
||||
let vid = tci & 0x0fff;
|
||||
vid == self.vlan_id
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for VlanDevice {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let tagged = self.insert_tag(packet).to_vec();
|
||||
// Forward the tagged frame to the parent device by looking it
|
||||
// up in the shared device list. If the parent doesn't exist or
|
||||
// isn't ready, drop the packet with a debug log.
|
||||
if let Some(parent) = self.devices.borrow_mut().get_mut(&self.parent_name) {
|
||||
parent.send(next_hop, &tagged, now);
|
||||
} else {
|
||||
log::debug!("vlan: parent {} not found, dropping {} byte frame",
|
||||
self.parent_name, packet.len());
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
if packet.len() < 18 {
|
||||
return None;
|
||||
}
|
||||
let tpid = u16::from_be_bytes([packet[12], packet[13]]);
|
||||
if tpid != TPID_8021Q {
|
||||
return None;
|
||||
}
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(&packet[..12]);
|
||||
self.recv_buffer.extend_from_slice(&packet[16..]);
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
self.mac_address
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
self.mac_address = Some(addr);
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
//! VXLAN (Virtual eXtensible LAN) — mirrors Linux 7.1's `drivers/net/vxlan/`.
|
||||
//!
|
||||
//! Reference files:
|
||||
//! - `drivers/net/vxlan/vxlan_core.c:1855` — `vxlan_xmit()` — encapsulation
|
||||
//! - `drivers/net/vxlan/vxlan_core.c:1366` — `vxlan_rcv()` — decapsulation
|
||||
//! - `include/net/vxlan.h` — VXLAN header format, VNI, default port 4789
|
||||
//!
|
||||
//! VXLAN encapsulates L2 Ethernet frames in UDP for overlay networking.
|
||||
//! Each overlay network is identified by a 24-bit VNI (Virtual Network
|
||||
//! Identifier). The default UDP destination port is 4789 (IANA-assigned).
|
||||
//!
|
||||
//! Packet structure:
|
||||
//! [Outer Eth][Outer IP][UDP:4789][VXLAN 8B][Inner Eth][Inner IP][Payload]
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::{
|
||||
EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, IpAddress, IpCidr,
|
||||
Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol, UdpPacket, UdpRepr,
|
||||
};
|
||||
|
||||
use super::{DeviceList, LinkDevice};
|
||||
|
||||
const VXLAN_PORT: u16 = 4789;
|
||||
const VXLAN_FLAGS: u8 = 0x08;
|
||||
|
||||
pub struct VxlanDevice {
|
||||
name: Rc<str>,
|
||||
parent_name: Rc<str>,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
vni: u32,
|
||||
vxlan_header: [u8; 8],
|
||||
send_buffer: Vec<u8>,
|
||||
recv_buffer: Vec<u8>,
|
||||
recv_queue: VecDeque<Vec<u8>>,
|
||||
virtual_mac: EthernetAddress,
|
||||
ip_address: Option<IpCidr>,
|
||||
}
|
||||
|
||||
impl VxlanDevice {
|
||||
pub fn new(
|
||||
name: &str,
|
||||
parent_name: &str,
|
||||
local_ip: Ipv4Address,
|
||||
remote_ip: Ipv4Address,
|
||||
vni: u32,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
) -> Self {
|
||||
let vni_bytes = vni.to_be_bytes();
|
||||
Self {
|
||||
name: name.into(),
|
||||
parent_name: parent_name.into(),
|
||||
devices,
|
||||
local_ip,
|
||||
remote_ip,
|
||||
vni: vni & 0x00ffffff,
|
||||
vxlan_header: [
|
||||
VXLAN_FLAGS, 0, 0, 0,
|
||||
vni_bytes[1], vni_bytes[2], vni_bytes[3], 0,
|
||||
],
|
||||
send_buffer: Vec::with_capacity(1550),
|
||||
recv_buffer: Vec::with_capacity(1550),
|
||||
recv_queue: VecDeque::new(),
|
||||
virtual_mac: EthernetAddress([0x00, 0x00, 0x5e, 0x00, 0x01, 0x01]),
|
||||
ip_address: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_received(&mut self, packet: Vec<u8>) {
|
||||
self.recv_queue.push_back(packet);
|
||||
}
|
||||
|
||||
fn build_encapsulated(&mut self, inner_packet: &[u8]) -> &[u8] {
|
||||
self.send_buffer.clear();
|
||||
let inner_eth = {
|
||||
let mut buf = [0u8; 14];
|
||||
buf[..6].copy_from_slice(&EthernetAddress::BROADCAST.0);
|
||||
buf[6..12].copy_from_slice(&self.virtual_mac.0);
|
||||
let ethtype = if !inner_packet.is_empty() && inner_packet[0] >> 4 == 6 {
|
||||
EthernetProtocol::Ipv6
|
||||
} else {
|
||||
EthernetProtocol::Ipv4
|
||||
};
|
||||
let proto_bytes: [u8; 2] = match ethtype {
|
||||
EthernetProtocol::Ipv4 => [0x08, 0x00],
|
||||
EthernetProtocol::Ipv6 => [0x86, 0xDD],
|
||||
_ => [0x08, 0x00],
|
||||
};
|
||||
buf[12..14].copy_from_slice(&proto_bytes);
|
||||
buf
|
||||
};
|
||||
let inner_frame_len = inner_eth.len() + inner_packet.len();
|
||||
|
||||
let udp_repr = UdpRepr {
|
||||
src_port: VXLAN_PORT,
|
||||
dst_port: VXLAN_PORT,
|
||||
};
|
||||
let udp_payload_len = 8 + inner_frame_len;
|
||||
let outer_ip_repr = Ipv4Repr {
|
||||
src_addr: self.local_ip,
|
||||
dst_addr: self.remote_ip,
|
||||
next_header: IpProtocol::Udp,
|
||||
payload_len: 8 + udp_payload_len,
|
||||
hop_limit: 64,
|
||||
};
|
||||
|
||||
let total_len = outer_ip_repr.buffer_len() + 8 + 8 + inner_frame_len;
|
||||
self.send_buffer.resize(total_len, 0);
|
||||
let mut ip = Ipv4Packet::new_unchecked(&mut self.send_buffer);
|
||||
outer_ip_repr.emit(&mut ip, &smoltcp::phy::ChecksumCapabilities::ignored());
|
||||
|
||||
let ip_hdr_len = outer_ip_repr.buffer_len();
|
||||
let mut udp = UdpPacket::new_unchecked(&mut self.send_buffer[ip_hdr_len..]);
|
||||
udp_repr.emit(
|
||||
&mut udp,
|
||||
&IpAddress::Ipv4(self.local_ip),
|
||||
&IpAddress::Ipv4(self.remote_ip),
|
||||
udp_payload_len,
|
||||
|buf| {
|
||||
buf[..8].copy_from_slice(&self.vxlan_header);
|
||||
buf[8..8 + inner_eth.len()].copy_from_slice(&inner_eth);
|
||||
buf[8 + inner_eth.len()..].copy_from_slice(inner_packet);
|
||||
},
|
||||
&smoltcp::phy::ChecksumCapabilities::ignored(),
|
||||
);
|
||||
&self.send_buffer
|
||||
}
|
||||
|
||||
fn matches_endpoint(&self, outer_packet: &[u8]) -> bool {
|
||||
if outer_packet.len() < 50 {
|
||||
return false;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(outer_packet) else {
|
||||
return false;
|
||||
};
|
||||
if u8::from(ipv4.next_header()) != 17 || ipv4.dst_addr() != self.local_ip {
|
||||
return false;
|
||||
}
|
||||
let ip_hdr_len = 20;
|
||||
let udp = &outer_packet[ip_hdr_len..];
|
||||
if udp.len() < 18 {
|
||||
return false;
|
||||
}
|
||||
let dst_port = u16::from_be_bytes([udp[2], udp[3]]);
|
||||
if dst_port != VXLAN_PORT {
|
||||
return false;
|
||||
}
|
||||
if udp[8] != VXLAN_FLAGS {
|
||||
return false;
|
||||
}
|
||||
let pkt_vni = u32::from_be_bytes([0, udp[12], udp[13], udp[14]]) & 0x00ffffff;
|
||||
pkt_vni == self.vni
|
||||
}
|
||||
|
||||
fn decapsulate(&mut self, outer_packet: &[u8]) -> Option<&[u8]> {
|
||||
if !self.matches_endpoint(outer_packet) {
|
||||
return None;
|
||||
}
|
||||
let inner_frame_start = 20 + 8 + 8;
|
||||
let inner_frame = &outer_packet[inner_frame_start..];
|
||||
if inner_frame.len() < 14 {
|
||||
return None;
|
||||
}
|
||||
let eth = EthernetFrame::new_unchecked(inner_frame);
|
||||
let Ok(repr) = EthernetRepr::parse(ð) else {
|
||||
return None;
|
||||
};
|
||||
if repr.ethertype != EthernetProtocol::Ipv4 && repr.ethertype != EthernetProtocol::Ipv6 {
|
||||
return None;
|
||||
}
|
||||
self.recv_buffer.clear();
|
||||
self.recv_buffer.extend_from_slice(eth.payload());
|
||||
Some(&self.recv_buffer)
|
||||
}
|
||||
}
|
||||
|
||||
impl LinkDevice for VxlanDevice {
|
||||
fn send(&mut self, next_hop: IpAddress, packet: &[u8], now: Instant) {
|
||||
let enc = self.build_encapsulated(packet).to_vec();
|
||||
if let Some(parent) = self.devices.borrow_mut().get_mut(&self.parent_name) {
|
||||
parent.send(next_hop, &enc, now);
|
||||
} else {
|
||||
log::debug!("vxlan: parent {} not found, dropping {} byte frame",
|
||||
self.parent_name, packet.len());
|
||||
}
|
||||
}
|
||||
|
||||
fn recv(&mut self, _now: Instant) -> Option<&[u8]> {
|
||||
let packet = self.recv_queue.pop_front()?;
|
||||
self.decapsulate(&packet)
|
||||
}
|
||||
|
||||
fn name(&self) -> &Rc<str> {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn can_recv(&self) -> bool {
|
||||
!self.recv_queue.is_empty()
|
||||
}
|
||||
|
||||
fn mac_address(&self) -> Option<EthernetAddress> {
|
||||
Some(self.virtual_mac)
|
||||
}
|
||||
|
||||
fn set_mac_address(&mut self, addr: EthernetAddress) {
|
||||
self.virtual_mac = addr;
|
||||
}
|
||||
|
||||
fn ip_address(&self) -> Option<IpCidr> {
|
||||
self.ip_address
|
||||
}
|
||||
|
||||
fn set_ip_address(&mut self, addr: IpCidr) {
|
||||
self.ip_address = Some(addr);
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ pub fn init_logger(process_name: &str) {
|
||||
OutputBuilder::stdout()
|
||||
.with_ansi_escape_codes()
|
||||
.flush_on_newline(true)
|
||||
.with_filter(log::LevelFilter::Warn)
|
||||
.with_filter(log::LevelFilter::Trace)
|
||||
.build(),
|
||||
)
|
||||
.with_process_name(process_name.into())
|
||||
|
||||
+65
-38
@@ -13,54 +13,48 @@ use smoltcp::wire::EthernetAddress;
|
||||
|
||||
mod buffer_pool;
|
||||
mod error;
|
||||
mod filter;
|
||||
mod icmp_error;
|
||||
mod link;
|
||||
mod logger;
|
||||
mod observer;
|
||||
mod port_set;
|
||||
mod router;
|
||||
mod scheme;
|
||||
mod slaac;
|
||||
|
||||
fn get_network_adapter() -> Result<String> {
|
||||
fn get_all_network_adapters() -> Result<Vec<String>> {
|
||||
use std::fs;
|
||||
|
||||
let mut adapters = vec![];
|
||||
|
||||
for entry_res in fs::read_dir("/scheme")? {
|
||||
let Ok(entry) = entry_res else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(scheme) = entry.file_name().into_string() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !scheme.starts_with("network") {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(entry) = entry_res else { continue };
|
||||
let Ok(scheme) = entry.file_name().into_string() else { continue };
|
||||
if !scheme.starts_with("network") { continue; }
|
||||
adapters.push(scheme);
|
||||
}
|
||||
|
||||
if adapters.is_empty() {
|
||||
bail!("no network adapter found");
|
||||
} else {
|
||||
let adapter = adapters.remove(0);
|
||||
if !adapters.is_empty() {
|
||||
// FIXME allow using multiple network adapters at the same time
|
||||
warn!("Multiple network adapters found. Only {adapter} will be used");
|
||||
}
|
||||
Ok(adapter)
|
||||
}
|
||||
info!("Found {} network adapter(s): {:?}", adapters.len(), adapters);
|
||||
Ok(adapters)
|
||||
}
|
||||
|
||||
fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
let adapter = get_network_adapter()?;
|
||||
trace!("opening {adapter}:");
|
||||
let network_fd = Fd::open(&format!("/scheme/{adapter}"), O_RDWR | O_NONBLOCK, 0)
|
||||
.map_err(|e| anyhow!("failed to open {adapter}: {e}"))?;
|
||||
let adapters = get_all_network_adapters()?;
|
||||
trace!("found {} network adapter(s)", adapters.len());
|
||||
|
||||
let hardware_addr = std::fs::read(format!("/scheme/{adapter}/mac"))
|
||||
.map(|mac_address| EthernetAddress::from_bytes(&mac_address))
|
||||
.context("failed to get mac address from network adapter")?;
|
||||
let mut network_fds = Vec::new();
|
||||
for name in &adapters {
|
||||
let fd = Fd::open(&format!("/scheme/{name}"), O_RDWR | O_NONBLOCK, 0)
|
||||
.map_err(|e| anyhow!("failed to open {name}: {e}"))?;
|
||||
let hw = std::fs::read(format!("/scheme/{name}/mac"))
|
||||
.map(|mac| EthernetAddress::from_bytes(&mac))
|
||||
.with_context(|| format!("failed to get mac for {name}"))?;
|
||||
network_fds.push((fd, hw, name.clone()));
|
||||
}
|
||||
|
||||
trace!("opening ip scheme socket");
|
||||
let ip_fd = Socket::nonblock()
|
||||
@@ -82,6 +76,14 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
let netcfg_fd =
|
||||
Socket::nonblock().map_err(|e| anyhow!("failed to open netcfg scheme socket {:?}", e))?;
|
||||
|
||||
trace!("opening netfilter scheme socket");
|
||||
let netfilter_fd =
|
||||
Socket::nonblock().map_err(|e| anyhow!("failed to open netfilter scheme socket: {:?}", e))?;
|
||||
|
||||
trace!("opening tun scheme socket");
|
||||
let tun_fd =
|
||||
Socket::nonblock().map_err(|e| anyhow!("failed to open tun scheme socket: {:?}", e))?;
|
||||
|
||||
let time_path = format!("/scheme/time/{}", syscall::CLOCK_MONOTONIC);
|
||||
let time_fd = Fd::open(&time_path, O_RDWR, 0).context("failed to open /scheme/time")?;
|
||||
|
||||
@@ -94,6 +96,8 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
TcpScheme,
|
||||
IcmpScheme,
|
||||
NetcfgScheme,
|
||||
NetfilterScheme,
|
||||
TunScheme,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +106,13 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
|
||||
daemon.ready();
|
||||
|
||||
event_queue
|
||||
.subscribe(network_fd.raw(), EventSource::Network, EventFlags::READ)
|
||||
.map_err(|e| anyhow!("failed to listen to network events: {:?}", e))?;
|
||||
// Subscribe to the first adapter for I/O events; the router handles
|
||||
// packet dispatch across all adapters in the DeviceList.
|
||||
if let Some((first_fd, _, _)) = network_fds.first() {
|
||||
event_queue
|
||||
.subscribe(first_fd.raw(), EventSource::Network, EventFlags::READ)
|
||||
.map_err(|e| anyhow!("failed to listen to network events: {:?}", e))?;
|
||||
}
|
||||
|
||||
event_queue
|
||||
.subscribe(time_fd.raw(), EventSource::Time, EventFlags::READ)
|
||||
@@ -112,7 +120,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
|
||||
event_queue
|
||||
.subscribe(ip_fd.inner().raw(), EventSource::IpScheme, EventFlags::READ)
|
||||
.context("failed to listen to ip scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to ip scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -120,7 +128,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::UdpScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to udp scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to udp scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -128,7 +136,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::TcpScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to tcp scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to tcp scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -136,7 +144,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::IcmpScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to icmp scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to icmp scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
@@ -144,17 +152,34 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::NetcfgScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.context("failed to listen to netcfg scheme events")?;
|
||||
.map_err(|e| anyhow!("failed to listen to netcfg scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
netfilter_fd.inner().raw(),
|
||||
EventSource::NetfilterScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.map_err(|e| anyhow!("failed to listen to netfilter scheme events: {:?}", e))?;
|
||||
|
||||
event_queue
|
||||
.subscribe(
|
||||
tun_fd.inner().raw(),
|
||||
EventSource::TunScheme,
|
||||
EventFlags::READ,
|
||||
)
|
||||
.map_err(|e| anyhow!("failed to listen to tun scheme events: {:?}", e))?;
|
||||
|
||||
let mut smolnetd = Smolnetd::new(
|
||||
network_fd,
|
||||
hardware_addr,
|
||||
network_fds,
|
||||
ip_fd,
|
||||
udp_fd,
|
||||
tcp_fd,
|
||||
icmp_fd,
|
||||
time_fd,
|
||||
netcfg_fd,
|
||||
netfilter_fd,
|
||||
tun_fd,
|
||||
)
|
||||
.context("smolnetd: failed to initialize smolnetd")?;
|
||||
|
||||
@@ -162,7 +187,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
|
||||
let all = {
|
||||
use EventSource::*;
|
||||
[Network, Time, IpScheme, UdpScheme, IcmpScheme, NetcfgScheme].map(Ok)
|
||||
[Network, Time, IpScheme, UdpScheme, TcpScheme, IcmpScheme, NetcfgScheme, NetfilterScheme, TunScheme].map(Ok)
|
||||
};
|
||||
|
||||
for event_res in all
|
||||
@@ -177,6 +202,8 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
|
||||
EventSource::TcpScheme => smolnetd.on_tcp_scheme_event(),
|
||||
EventSource::IcmpScheme => smolnetd.on_icmp_scheme_event(),
|
||||
EventSource::NetcfgScheme => smolnetd.on_netcfg_scheme_event(),
|
||||
EventSource::NetfilterScheme => smolnetd.on_netfilter_scheme_event(),
|
||||
EventSource::TunScheme => smolnetd.on_tun_scheme_event(),
|
||||
}
|
||||
.map_err(|e| error!("Received packet error: {:?}", e));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
//! Packet observer / capture facility — mirrors Linux 7.1's AF_PACKET + tcpdump.
|
||||
//!
|
||||
//! Provides a ring buffer that captures packets flowing through the network
|
||||
//! stack. Exposed via `/scheme/netcfg/capture` for reading by diagnostic tools.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cat /scheme/netcfg/capture/read → drain captured packets (hex dump)
|
||||
//! cat /scheme/netcfg/capture/count → stats
|
||||
//! echo tcp > /scheme/netcfg/capture/filter → capture only TCP
|
||||
//! echo > /scheme/netcfg/capture/enable → start
|
||||
//! echo > /scheme/netcfg/capture/disable → stop
|
||||
//! echo tcp port 80 > /scheme/netcfg/capture/filter → TCP port 80 only
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
|
||||
const MAX_CAPTURE_PACKETS: usize = 256;
|
||||
const MAX_CAPTURE_BYTES: usize = 65536;
|
||||
|
||||
pub struct Observer {
|
||||
enabled: AtomicBool,
|
||||
buffer: RefCell<Vec<Vec<u8>>>,
|
||||
filter: RefCell<CaptureFilter>,
|
||||
max_packets: usize,
|
||||
total_captured: AtomicU64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct CaptureFilter {
|
||||
proto: Option<u8>,
|
||||
port: Option<u16>,
|
||||
}
|
||||
|
||||
impl CaptureFilter {
|
||||
fn new() -> Self {
|
||||
Self { proto: None, port: None }
|
||||
}
|
||||
|
||||
fn matches(&self, packet: &[u8]) -> bool {
|
||||
if self.proto.is_none() && self.port.is_none() {
|
||||
return true;
|
||||
}
|
||||
if packet.len() < 20 {
|
||||
return false; // too short to determine protocol/port
|
||||
}
|
||||
let version = packet[0] >> 4;
|
||||
let (proto, src_port_offset, dst_port_offset) = match version {
|
||||
4 => {
|
||||
if packet.len() < 24 { return true; }
|
||||
let ihl = (packet[0] & 0x0f) as usize * 4;
|
||||
if packet.len() < ihl + 4 { return true; }
|
||||
(packet[9], ihl, ihl + 2)
|
||||
}
|
||||
6 => {
|
||||
if packet.len() < 48 { return true; }
|
||||
(packet[6], 40, 42)
|
||||
}
|
||||
_ => return true,
|
||||
};
|
||||
if let Some(p) = self.proto {
|
||||
if proto != p { return false; }
|
||||
}
|
||||
if let Some(port) = self.port {
|
||||
let sp = u16::from_be_bytes([packet[src_port_offset], packet[src_port_offset + 1]]);
|
||||
let dp = u16::from_be_bytes([packet[dst_port_offset], packet[dst_port_offset + 1]]);
|
||||
if sp != port && dp != port { return false; }
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn from_str(s: &str) -> Self {
|
||||
let mut filter = Self::new();
|
||||
let lower = s.trim().to_lowercase();
|
||||
let parts: Vec<&str> = lower.split_whitespace().collect();
|
||||
let mut i = 0;
|
||||
while i < parts.len() {
|
||||
match parts[i] {
|
||||
"tcp" => filter.proto = Some(6),
|
||||
"udp" => filter.proto = Some(17),
|
||||
"icmp" => filter.proto = Some(1),
|
||||
"port" if i + 1 < parts.len() => {
|
||||
if let Ok(p) = parts[i + 1].parse::<u16>() {
|
||||
filter.port = Some(p);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
pub type ObserverRef = Rc<Observer>;
|
||||
|
||||
impl Observer {
|
||||
pub fn new() -> ObserverRef {
|
||||
Rc::new(Observer {
|
||||
enabled: AtomicBool::new(false),
|
||||
buffer: RefCell::new(Vec::with_capacity(MAX_CAPTURE_PACKETS)),
|
||||
filter: RefCell::new(CaptureFilter::new()),
|
||||
max_packets: MAX_CAPTURE_PACKETS,
|
||||
total_captured: AtomicU64::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn enable(&self) {
|
||||
self.enabled.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn disable(&self) {
|
||||
self.enabled.store(false, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool {
|
||||
self.enabled.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn set_filter(&self, filter_str: &str) {
|
||||
*self.filter.borrow_mut() = CaptureFilter::from_str(filter_str);
|
||||
}
|
||||
|
||||
pub fn filter_str(&self) -> String {
|
||||
let f = self.filter.borrow();
|
||||
let mut s = String::new();
|
||||
if let Some(p) = f.proto {
|
||||
s.push_str(match p { 6 => "tcp", 17 => "udp", 1 => "icmp", _ => "?" });
|
||||
}
|
||||
if let Some(port) = f.port {
|
||||
if !s.is_empty() { s.push(' '); }
|
||||
s.push_str(&format!("port {}", port));
|
||||
}
|
||||
if s.is_empty() { s.push_str("any"); }
|
||||
s
|
||||
}
|
||||
|
||||
pub fn capture(&self, packet: &[u8]) {
|
||||
if !self.is_enabled() {
|
||||
return;
|
||||
}
|
||||
if !self.filter.borrow().matches(packet) {
|
||||
return;
|
||||
}
|
||||
let mut buf = self.buffer.borrow_mut();
|
||||
if buf.len() >= self.max_packets {
|
||||
buf.remove(0);
|
||||
}
|
||||
// Truncate to per-packet size limit so a single large packet
|
||||
// can't exhaust the total capture buffer.
|
||||
let per_packet_limit = MAX_CAPTURE_BYTES / self.max_packets.max(1);
|
||||
let truncated_len = packet.len().min(per_packet_limit);
|
||||
let mut copy = Vec::with_capacity(truncated_len);
|
||||
copy.extend_from_slice(&packet[..truncated_len]);
|
||||
buf.push(copy);
|
||||
self.total_captured.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn drain_hex(&self) -> String {
|
||||
let mut out = String::new();
|
||||
let mut buf = self.buffer.borrow_mut();
|
||||
for packet in buf.drain(..) {
|
||||
out.push_str(&format!("# {} bytes\n", packet.len()));
|
||||
for chunk in packet.chunks(16) {
|
||||
for byte in chunk {
|
||||
out.push_str(&format!("{:02x} ", byte));
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out.push('\n');
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.buffer.borrow().len()
|
||||
}
|
||||
|
||||
pub fn total(&self) -> u64 {
|
||||
self.total_captured.load(Ordering::Relaxed)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,16 @@ impl PortSet {
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the reference count for an already-claimed port (SO_REUSEADDR).
|
||||
/// Always succeeds: the caller has indicated SO_REUSEADDR is set, so
|
||||
/// multiple sockets are allowed to bind to the same port. The actual
|
||||
/// collision check (returning EADDRINUSE) is done by `claim_port` first.
|
||||
/// Returns true on success.
|
||||
pub fn claim_port_reuse(&mut self, port: u16) -> bool {
|
||||
self.ports.entry(port).and_modify(|c| *c += 1).or_insert(1);
|
||||
true
|
||||
}
|
||||
|
||||
pub fn acquire_port(&mut self, port: u16) {
|
||||
*self.ports.entry(port).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
+425
-26
@@ -1,13 +1,16 @@
|
||||
use std::cell::RefCell;
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::rc::Rc;
|
||||
|
||||
use smoltcp::phy::{Device, DeviceCapabilities, Medium};
|
||||
use smoltcp::storage::PacketMetadata;
|
||||
use smoltcp::time::Instant;
|
||||
use smoltcp::wire::IpAddress;
|
||||
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv4Packet, Ipv6Packet};
|
||||
|
||||
use self::route_table::RouteTable;
|
||||
use self::route_table::{RouteTable, RouteType};
|
||||
use crate::filter::{FilterTable, Hook, PacketContext, Verdict};
|
||||
use crate::icmp_error;
|
||||
use crate::link::DeviceList;
|
||||
use crate::observer::ObserverRef;
|
||||
use crate::scheme::Smolnetd;
|
||||
|
||||
pub mod route_table;
|
||||
@@ -19,10 +22,12 @@ pub struct Router {
|
||||
tx_buffer: PacketBuffer,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
route_table: Rc<RefCell<RouteTable>>,
|
||||
pub ip_forward: Rc<Cell<bool>>,
|
||||
observer: ObserverRef,
|
||||
}
|
||||
|
||||
impl Router {
|
||||
pub fn new(devices: Rc<RefCell<DeviceList>>, route_table: Rc<RefCell<RouteTable>>) -> Self {
|
||||
pub fn new(devices: Rc<RefCell<DeviceList>>, route_table: Rc<RefCell<RouteTable>>, observer: ObserverRef) -> Self {
|
||||
let rx_buffer = PacketBuffer::new(
|
||||
vec![PacketMetadata::EMPTY; Smolnetd::SOCKET_BUFFER_SIZE],
|
||||
vec![0u8; Router::MTU * Smolnetd::SOCKET_BUFFER_SIZE],
|
||||
@@ -36,6 +41,8 @@ impl Router {
|
||||
tx_buffer,
|
||||
devices,
|
||||
route_table,
|
||||
ip_forward: Rc::new(Cell::new(true)),
|
||||
observer,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +56,204 @@ impl Router {
|
||||
can_recv
|
||||
}
|
||||
|
||||
pub fn filter_input(&mut self, filter_table: &Rc<RefCell<FilterTable>>, now: Instant) {
|
||||
let mut filtered: Vec<Vec<u8>> = Vec::new();
|
||||
while let Ok(((), packet)) = self.rx_buffer.dequeue() {
|
||||
if packet.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let mut table = filter_table.borrow_mut();
|
||||
let context = infer_context(Hook::InputLocal, None, packet);
|
||||
match table.evaluate(&context, now) {
|
||||
Verdict::Accept => {
|
||||
drop(table);
|
||||
filtered.push(packet.to_vec());
|
||||
}
|
||||
Verdict::Reject => {
|
||||
drop(table);
|
||||
let err_fn = if packet[0] >> 4 == 6 {
|
||||
icmp_error::build_icmpv6_port_unreachable
|
||||
} else {
|
||||
icmp_error::build_icmpv4_port_unreachable
|
||||
};
|
||||
if let Some(err_pkt) = err_fn(packet) {
|
||||
if let Ok(buf) = self.tx_buffer.enqueue(err_pkt.len(), ()) {
|
||||
buf.copy_from_slice(&err_pkt);
|
||||
}
|
||||
}
|
||||
debug!("filter: rejected INPUT packet {} → {}", context.src_addr, context.dst_addr);
|
||||
}
|
||||
Verdict::Drop | Verdict::Log => {
|
||||
drop(table);
|
||||
debug!("filter: dropped INPUT packet {} → {}", context.src_addr, context.dst_addr);
|
||||
}
|
||||
}
|
||||
}
|
||||
for packet in filtered {
|
||||
let Ok(buf) = self.rx_buffer.enqueue(packet.len(), ()) else {
|
||||
break;
|
||||
};
|
||||
buf.copy_from_slice(&packet);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn forward_packets(&mut self, filter_table: &Rc<RefCell<FilterTable>>, now: Instant) {
|
||||
if !self.ip_forward.get() {
|
||||
return;
|
||||
}
|
||||
let mut forwarded: Vec<Vec<u8>> = Vec::new();
|
||||
let mut local: Vec<Vec<u8>> = Vec::new();
|
||||
|
||||
while let Ok(((), packet)) = self.rx_buffer.dequeue() {
|
||||
let packet_data: &[u8] = packet;
|
||||
if packet_data.is_empty() || packet_data[0] >> 4 != 4 {
|
||||
local.push(packet_data.to_vec());
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(packet_data) else {
|
||||
local.push(packet_data.to_vec());
|
||||
continue;
|
||||
};
|
||||
|
||||
let dst = IpAddress::Ipv4(ipv4.dst_addr());
|
||||
let is_broadcast = ipv4.dst_addr().is_broadcast() || dst.is_multicast();
|
||||
if is_broadcast {
|
||||
local.push(packet.to_vec());
|
||||
continue;
|
||||
}
|
||||
|
||||
let route_info = {
|
||||
let table = self.route_table.borrow();
|
||||
table.lookup_rule(&dst).map(|r| (r.dev.clone(), r.via, r.route_type))
|
||||
};
|
||||
|
||||
let Some((dev_name, _via, route_type)) = route_info else {
|
||||
local.push(packet.to_vec());
|
||||
continue;
|
||||
};
|
||||
|
||||
match route_type {
|
||||
RouteType::Blackhole => continue,
|
||||
RouteType::Unreachable | RouteType::Prohibit => {
|
||||
// Build the ICMPv4 error and queue it for transmit
|
||||
// back to the original sender. Using rx_buffer here
|
||||
// would re-route the error back into the input path
|
||||
// (infinite loop) and never reach the sender.
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_port_unreachable(packet) {
|
||||
let _ = self.tx_buffer.enqueue(error_pkt.len(), ())
|
||||
.map(|b| b.copy_from_slice(&error_pkt));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RouteType::Unicast => {}
|
||||
}
|
||||
let mut buf = packet.to_vec();
|
||||
let context = infer_context(Hook::Forward, Some(dev_name.clone()), &buf);
|
||||
|
||||
{
|
||||
let mut table = filter_table.borrow_mut();
|
||||
if table.evaluate(&context, now) == Verdict::Drop {
|
||||
debug!("filter: dropped FORWARD packet");
|
||||
continue;
|
||||
}
|
||||
if let Some((trans_addr, trans_port)) = table.nat_table.lookup_snat(
|
||||
Hook::Forward,
|
||||
IpAddress::Ipv4(ipv4.src_addr()),
|
||||
dst,
|
||||
) {
|
||||
let IpAddress::Ipv4(new_src) = trans_addr else { continue; };
|
||||
let _ = crate::filter::rewrite_src_ipv4(&mut buf, new_src);
|
||||
if let Some(port) = trans_port {
|
||||
table.nat_table.record_snat(
|
||||
IpAddress::Ipv4(ipv4.src_addr()),
|
||||
IpAddress::Ipv4(new_src),
|
||||
0, port,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ttl = ipv4.hop_limit();
|
||||
if ttl <= 1 {
|
||||
debug!("forward: TTL expired");
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_time_exceeded(&buf) {
|
||||
if let Ok(buf) = self.tx_buffer.enqueue(error_pkt.len(), ()) {
|
||||
buf.copy_from_slice(&error_pkt);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
buf[8] = ttl - 1;
|
||||
if let Ok(mut pkt) = Ipv4Packet::new_checked(&mut buf) {
|
||||
pkt.fill_checksum();
|
||||
}
|
||||
|
||||
forwarded.push(buf);
|
||||
}
|
||||
|
||||
for packet in &forwarded {
|
||||
self.observer.capture(packet);
|
||||
}
|
||||
for packet in &local {
|
||||
self.observer.capture(packet);
|
||||
}
|
||||
|
||||
for packet in local {
|
||||
let Ok(buf) = self.rx_buffer.enqueue(packet.len(), ()) else {
|
||||
break;
|
||||
};
|
||||
buf.copy_from_slice(&packet);
|
||||
}
|
||||
for packet in forwarded {
|
||||
let Ok(buf) = self.tx_buffer.enqueue(packet.len(), ()) else {
|
||||
break;
|
||||
};
|
||||
buf.copy_from_slice(&packet);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn filter_output(&self, packet: &[u8], filter_table: &Rc<RefCell<FilterTable>>, now: Instant) -> Verdict {
|
||||
if packet.is_empty() {
|
||||
return Verdict::Accept;
|
||||
}
|
||||
let context = infer_context(Hook::OutputLocal, None, packet);
|
||||
filter_table.borrow_mut().evaluate(&context, now)
|
||||
}
|
||||
|
||||
fn apply_snat(
|
||||
&self,
|
||||
packet: &mut [u8],
|
||||
filter_table: &Rc<RefCell<FilterTable>>,
|
||||
) {
|
||||
if packet.is_empty() {
|
||||
return;
|
||||
}
|
||||
let version = packet[0] >> 4;
|
||||
if version != 4 {
|
||||
return;
|
||||
}
|
||||
let Ok(ipv4) = Ipv4Packet::new_checked(&*packet) else {
|
||||
return;
|
||||
};
|
||||
let src = IpAddress::Ipv4(ipv4.src_addr());
|
||||
let dst = IpAddress::Ipv4(ipv4.dst_addr());
|
||||
let table = filter_table.borrow();
|
||||
let snat = table.nat_table.lookup_snat(
|
||||
crate::filter::Hook::OutputLocal,
|
||||
src,
|
||||
dst,
|
||||
);
|
||||
drop(table);
|
||||
|
||||
if let Some((trans_addr, _trans_port)) = snat {
|
||||
let IpAddress::Ipv4(new_src) = trans_addr else {
|
||||
return;
|
||||
};
|
||||
crate::filter::rewrite_src_ipv4(packet, new_src);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn poll(&mut self, now: Instant) {
|
||||
for dev in self.devices.borrow_mut().iter_mut() {
|
||||
if self.rx_buffer.is_full() {
|
||||
@@ -72,41 +277,130 @@ impl Router {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dispatch(&mut self, now: Instant) {
|
||||
pub fn dispatch(&mut self, now: Instant, filter_table: &Rc<RefCell<FilterTable>>) {
|
||||
let mut packets: Vec<Vec<u8>> = Vec::new();
|
||||
while let Ok(((), packet)) = self.tx_buffer.dequeue() {
|
||||
if let Ok(mut packet) = smoltcp::wire::Ipv4Packet::new_checked(packet) {
|
||||
let dst_addr = IpAddress::Ipv4(packet.dst_addr());
|
||||
if packet.dst_addr().is_broadcast() {
|
||||
let buf = packet.into_inner();
|
||||
for dev in self.devices.borrow_mut().iter_mut() {
|
||||
dev.send(dst_addr, buf, now)
|
||||
}
|
||||
} else {
|
||||
let route_table = self.route_table.borrow();
|
||||
let Some(rule) = route_table.lookup_rule(&dst_addr) else {
|
||||
warn!("No route found for destination: {}", dst_addr);
|
||||
if !packet.is_empty() {
|
||||
self.observer.capture(packet);
|
||||
packets.push(packet.to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
for packet in packets {
|
||||
if packet.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
match packet[0] >> 4 {
|
||||
4 => {
|
||||
let mut packet_buf = packet;
|
||||
let Ok(mut ipv4_pkt) = smoltcp::wire::Ipv4Packet::new_checked(&mut packet_buf)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let dst_addr = IpAddress::Ipv4(ipv4_pkt.dst_addr());
|
||||
let src_addr = ipv4_pkt.src_addr();
|
||||
|
||||
let next_hop = match rule.via {
|
||||
Some(via) => via,
|
||||
None => dst_addr,
|
||||
let (next_hop, src_rule, dev_name, route_type) = {
|
||||
let route_table = self.route_table.borrow();
|
||||
let Some(rule) = route_table.lookup_rule(&dst_addr) else {
|
||||
warn!("No route found for destination: {}", dst_addr);
|
||||
continue;
|
||||
};
|
||||
let next_hop = rule.via.unwrap_or(dst_addr);
|
||||
(next_hop, rule.src, rule.dev.clone(), rule.route_type)
|
||||
};
|
||||
|
||||
match route_type {
|
||||
RouteType::Blackhole => continue,
|
||||
RouteType::Unreachable => {
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_port_unreachable(&packet_buf) {
|
||||
let _ = self.tx_buffer.enqueue(error_pkt.len(), ())
|
||||
.map(|b| b.copy_from_slice(&error_pkt));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RouteType::Prohibit => {
|
||||
if let Some(error_pkt) = icmp_error::build_icmpv4_port_unreachable(&packet_buf) {
|
||||
let _ = self.tx_buffer.enqueue(error_pkt.len(), ())
|
||||
.map(|b| b.copy_from_slice(&error_pkt));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
RouteType::Unicast => {}
|
||||
}
|
||||
|
||||
if ipv4_pkt.dst_addr().is_broadcast() {
|
||||
let buf = ipv4_pkt.into_inner();
|
||||
if self.filter_output(buf, filter_table, now) == Verdict::Drop {
|
||||
debug!("filter: dropped OUTPUT broadcast IPv4 packet");
|
||||
continue;
|
||||
}
|
||||
self.apply_snat(buf, filter_table);
|
||||
for dev in self.devices.borrow_mut().iter_mut() {
|
||||
dev.send(dst_addr, buf, now);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let IpAddress::Ipv4(src) = src_rule {
|
||||
if src != src_addr {
|
||||
ipv4_pkt.set_src_addr(src);
|
||||
ipv4_pkt.fill_checksum();
|
||||
}
|
||||
}
|
||||
|
||||
let buf = ipv4_pkt.into_inner();
|
||||
let mut devices = self.devices.borrow_mut();
|
||||
let Some(dev) = devices.get_mut(&rule.dev) else {
|
||||
warn!("Device {} not found", rule.dev);
|
||||
let Some(dev) = devices.get_mut(&dev_name) else {
|
||||
warn!("Device {} not found", dev_name);
|
||||
// TODO: Remove route if device doesn't exist anymore ?
|
||||
continue;
|
||||
};
|
||||
if self.filter_output(buf, filter_table, now) == Verdict::Drop {
|
||||
debug!("filter: dropped OUTPUT IPv4 packet");
|
||||
continue;
|
||||
}
|
||||
self.apply_snat(buf, filter_table);
|
||||
dev.send(next_hop, buf, now);
|
||||
}
|
||||
6 => {
|
||||
let Ok(ipv6_pkt) = smoltcp::wire::Ipv6Packet::new_checked(&packet) else {
|
||||
continue;
|
||||
};
|
||||
let dst_addr = IpAddress::Ipv6(ipv6_pkt.dst_addr());
|
||||
|
||||
let IpAddress::Ipv4(src) = rule.src;
|
||||
if src != packet.src_addr() {
|
||||
packet.set_src_addr(src);
|
||||
packet.fill_checksum()
|
||||
let (next_hop, dev_name, route_type) = {
|
||||
let route_table = self.route_table.borrow();
|
||||
let Some(rule) = route_table.lookup_rule(&dst_addr) else {
|
||||
warn!("No route found for destination: {}", dst_addr);
|
||||
continue;
|
||||
};
|
||||
let next_hop = rule.via.unwrap_or(dst_addr);
|
||||
(next_hop, rule.dev.clone(), rule.route_type)
|
||||
};
|
||||
|
||||
match route_type {
|
||||
RouteType::Blackhole => continue,
|
||||
RouteType::Unreachable | RouteType::Prohibit => continue,
|
||||
RouteType::Unicast => {}
|
||||
}
|
||||
|
||||
dev.send(next_hop, packet.into_inner(), now);
|
||||
let mut devices = self.devices.borrow_mut();
|
||||
let Some(dev) = devices.get_mut(&dev_name) else {
|
||||
warn!("Device {} not found", dev_name);
|
||||
continue;
|
||||
};
|
||||
|
||||
if self.filter_output(&packet, filter_table, now) == Verdict::Drop {
|
||||
debug!("filter: dropped OUTPUT IPv6 packet");
|
||||
continue;
|
||||
}
|
||||
|
||||
dev.send(next_hop, &packet, now);
|
||||
}
|
||||
version => {
|
||||
debug!("Dropped packet with unknown IP version: {}", version);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -188,3 +482,108 @@ impl<'a> smoltcp::phy::RxToken for RxToken<'a> {
|
||||
f(buf)
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_context(
|
||||
hook: Hook,
|
||||
_dev_name: Option<Rc<str>>,
|
||||
packet: &[u8],
|
||||
) -> PacketContext<'_> {
|
||||
let version = if packet.is_empty() { 0 } else { packet[0] >> 4 };
|
||||
match version {
|
||||
4 => {
|
||||
if let Ok(ipv4) = Ipv4Packet::new_checked(packet) {
|
||||
let (src_port, dst_port) = parse_ports(&ipv4.next_header(), ipv4.payload());
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv4(ipv4.src_addr()),
|
||||
dst_addr: IpAddress::Ipv4(ipv4.dst_addr()),
|
||||
protocol: ipv4.next_header().into(),
|
||||
src_port,
|
||||
dst_port,
|
||||
packet,
|
||||
}
|
||||
} else {
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
dst_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
protocol: 0,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
6 => {
|
||||
if let Ok(ipv6) = Ipv6Packet::new_checked(packet) {
|
||||
let nh = ipv6.next_header();
|
||||
let payload_start = 40; // fixed IPv6 header
|
||||
// Extension headers (Hop-by-Hop, Routing, Fragment, etc.)
|
||||
// would shift the transport header to a higher offset.
|
||||
// smoltcp's next_header() chases extension headers to return
|
||||
// the final protocol, but we don't compute the actual
|
||||
// transport offset. For packets with extension headers the
|
||||
// port extraction below will read the wrong bytes and return
|
||||
// None/None, which is safe: the filter matches on IP+protocol
|
||||
// but silently skips port matching.
|
||||
let payload = if packet.len() > payload_start {
|
||||
&packet[payload_start..]
|
||||
} else {
|
||||
&[]
|
||||
};
|
||||
let (src_port, dst_port) = parse_ports(&nh, payload);
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv6(ipv6.src_addr()),
|
||||
dst_addr: IpAddress::Ipv6(ipv6.dst_addr()),
|
||||
protocol: nh.into(),
|
||||
src_port,
|
||||
dst_port,
|
||||
packet,
|
||||
}
|
||||
} else {
|
||||
PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv6(smoltcp::wire::Ipv6Address::UNSPECIFIED),
|
||||
dst_addr: IpAddress::Ipv6(smoltcp::wire::Ipv6Address::UNSPECIFIED),
|
||||
protocol: 0,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
packet,
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => PacketContext {
|
||||
hook,
|
||||
in_dev: None,
|
||||
out_dev: None,
|
||||
src_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
dst_addr: IpAddress::Ipv4(smoltcp::wire::Ipv4Address::UNSPECIFIED),
|
||||
protocol: 0,
|
||||
src_port: None,
|
||||
dst_port: None,
|
||||
packet,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ports(protocol: &smoltcp::wire::IpProtocol, payload: &[u8]) -> (Option<u16>, Option<u16>) {
|
||||
let proto_byte: u8 = (*protocol).into();
|
||||
if proto_byte != 6 && proto_byte != 17 && proto_byte != 58 {
|
||||
return (None, None);
|
||||
}
|
||||
if payload.len() < 4 {
|
||||
return (None, None);
|
||||
}
|
||||
let src = u16::from_be_bytes([payload[0], payload[1]]);
|
||||
let dst = u16::from_be_bytes([payload[2], payload[3]]);
|
||||
(Some(src), Some(dst))
|
||||
}
|
||||
|
||||
@@ -3,12 +3,43 @@ use std::rc::Rc;
|
||||
|
||||
use smoltcp::wire::{IpAddress, IpCidr};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RouteType {
|
||||
Unicast,
|
||||
Blackhole,
|
||||
Unreachable,
|
||||
Prohibit,
|
||||
}
|
||||
|
||||
impl RouteType {
|
||||
pub const fn name(self) -> &'static str {
|
||||
match self {
|
||||
RouteType::Unicast => "unicast",
|
||||
RouteType::Blackhole => "blackhole",
|
||||
RouteType::Unreachable => "unreachable",
|
||||
RouteType::Prohibit => "prohibit",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s {
|
||||
"unicast" => Some(RouteType::Unicast),
|
||||
"blackhole" => Some(RouteType::Blackhole),
|
||||
"unreachable" => Some(RouteType::Unreachable),
|
||||
"prohibit" => Some(RouteType::Prohibit),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Rule {
|
||||
pub filter: IpCidr,
|
||||
pub via: Option<IpAddress>,
|
||||
pub dev: Rc<str>,
|
||||
pub src: IpAddress,
|
||||
pub route_type: RouteType,
|
||||
pub metric: u32,
|
||||
}
|
||||
|
||||
impl Rule {
|
||||
@@ -18,12 +49,32 @@ impl Rule {
|
||||
via,
|
||||
dev,
|
||||
src,
|
||||
route_type: RouteType::Unicast,
|
||||
metric: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_type(
|
||||
filter: IpCidr,
|
||||
via: Option<IpAddress>,
|
||||
dev: Rc<str>,
|
||||
src: IpAddress,
|
||||
route_type: RouteType,
|
||||
) -> Self {
|
||||
Self { filter, via, dev, src, route_type, metric: 0 }
|
||||
}
|
||||
|
||||
pub fn with_metric(mut self, metric: u32) -> Self {
|
||||
self.metric = metric;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Rule {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if self.route_type != RouteType::Unicast {
|
||||
write!(f, "{} ", self.route_type.name())?;
|
||||
}
|
||||
if self.filter.prefix_len() == 0 {
|
||||
write!(f, "default")?;
|
||||
} else {
|
||||
@@ -37,6 +88,10 @@ impl Display for Rule {
|
||||
write!(f, " dev {}", self.dev)?;
|
||||
write!(f, " src {}", self.src)?;
|
||||
|
||||
if self.metric != 0 {
|
||||
write!(f, " metric {}", self.metric)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -48,10 +103,15 @@ pub struct RouteTable {
|
||||
|
||||
impl RouteTable {
|
||||
pub fn lookup_rule(&self, dst: &IpAddress) -> Option<&Rule> {
|
||||
// Find the longest-prefix match. Among rules with the same
|
||||
// prefix length, prefer the one with the lowest metric.
|
||||
// Rules are sorted by (prefix_len, metric) — highest prefix
|
||||
// length first, lowest metric first within each prefix.
|
||||
self.rules
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|rule| rule.filter.contains_addr(dst))
|
||||
.filter(|rule| rule.filter.contains_addr(dst))
|
||||
.min_by_key(|rule| rule.metric)
|
||||
}
|
||||
|
||||
pub fn lookup_src_addr(&self, dst: &IpAddress) -> Option<IpAddress> {
|
||||
@@ -86,6 +146,10 @@ impl RouteTable {
|
||||
rule.src = new_src;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.rules.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for RouteTable {
|
||||
|
||||
+53
-14
@@ -17,6 +17,16 @@ use crate::router::Router;
|
||||
|
||||
pub type IcmpScheme = SchemeWrapper<IcmpSocket<'static>>;
|
||||
|
||||
// ICMP socket variants.
|
||||
//
|
||||
// Echo: standard ICMP echo request/reply (e.g. for ping). Ident is
|
||||
// assigned from the ICMP ident pool (1..0xffff).
|
||||
//
|
||||
// Udp: notification endpoint for ICMP error messages received for
|
||||
// connected UDP sockets. Mirrors Linux's `IP_RECVERR` mechanism. When
|
||||
// the network receives an ICMPv4 error for a UDP datagram, the matching
|
||||
// socket is woken with `EWOULDBLOCK` and the error is queued. The user
|
||||
// reads it via the ICMP scheme's read path. This path is read-only.
|
||||
enum IcmpSocketType {
|
||||
Echo,
|
||||
Udp,
|
||||
@@ -214,22 +224,51 @@ impl<'a> SchemeSocket for IcmpSocket<'a> {
|
||||
return Ok(0);
|
||||
}
|
||||
while self.can_recv(&file.data) {
|
||||
let (payload, _) = self.recv().expect("Can't recv icmp packet");
|
||||
let (payload, _) = match self.recv() {
|
||||
Ok(p) => p,
|
||||
Err(_) => break, // malformed recv, stop reading
|
||||
};
|
||||
let icmp_packet = Icmpv4Packet::new_unchecked(&payload);
|
||||
//TODO: replace default with actual caps
|
||||
let icmp_repr = Icmpv4Repr::parse(&icmp_packet, &Default::default()).unwrap();
|
||||
// Drop packets that fail to parse — don't crash the daemon.
|
||||
let Ok(icmp_repr) = Icmpv4Repr::parse(&icmp_packet, &Default::default()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Icmpv4Repr::EchoReply { seq_no, data, .. } = icmp_repr {
|
||||
if buf.len() < mem::size_of::<u16>() + data.len() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
match file.data.socket_type {
|
||||
IcmpSocketType::Echo => {
|
||||
// For echo sockets, only echo replies are expected.
|
||||
// Other ICMP types are silently dropped.
|
||||
if let Icmpv4Repr::EchoReply { seq_no, data, .. } = icmp_repr {
|
||||
if buf.len() < mem::size_of::<u16>() + data.len() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
buf[0..2].copy_from_slice(&seq_no.to_be_bytes());
|
||||
for i in 0..data.len() {
|
||||
buf[mem::size_of::<u16>() + i] = data[i];
|
||||
}
|
||||
return Ok(mem::size_of::<u16>() + data.len());
|
||||
}
|
||||
}
|
||||
buf[0..2].copy_from_slice(&seq_no.to_be_bytes());
|
||||
|
||||
for i in 0..data.len() {
|
||||
buf[mem::size_of::<u16>() + i] = data[i];
|
||||
IcmpSocketType::Udp => {
|
||||
// For ICMP error notification endpoints (IP_RECVERR
|
||||
// style), serialize the ICMP error type and original
|
||||
// packet metadata. Format:
|
||||
// [0]: icmp type (e.g. 3=dst unreachable, 11=time exceeded)
|
||||
// [1]: icmp code
|
||||
// [2..6]: reserved
|
||||
// [6..]: original IP header (up to buf.len()-6)
|
||||
if buf.len() < 7 {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
buf[0] = payload[0];
|
||||
buf[1] = payload[1];
|
||||
let copy_len = (buf.len() - 6).min(payload.len().saturating_sub(8));
|
||||
if copy_len > 0 {
|
||||
buf[6..6 + copy_len].copy_from_slice(&payload[8..8 + copy_len]);
|
||||
}
|
||||
return Ok(6 + copy_len);
|
||||
}
|
||||
|
||||
return Ok(mem::size_of::<u16>() + data.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,9 +308,9 @@ impl<'a> SchemeSocket for IcmpSocket<'a> {
|
||||
|
||||
fn handle_recvmsg(
|
||||
&mut self,
|
||||
file: &mut SchemeFile<Self>,
|
||||
how: &mut [u8],
|
||||
flags: usize,
|
||||
_file: &mut SchemeFile<Self>,
|
||||
_how: &mut [u8],
|
||||
_flags: usize,
|
||||
) -> SyscallResult<usize> {
|
||||
return Err(SyscallError::new(syscall::EOPNOTSUPP));
|
||||
}
|
||||
|
||||
@@ -53,10 +53,12 @@ impl<'a> SchemeSocket for RawSocket<'a> {
|
||||
}
|
||||
|
||||
fn hop_limit(&self) -> u8 {
|
||||
0
|
||||
smoltcp::socket::raw::Socket::hop_limit(self)
|
||||
}
|
||||
|
||||
fn set_hop_limit(&mut self, _hop_limit: u8) {}
|
||||
fn set_hop_limit(&mut self, hop_limit: u8) {
|
||||
smoltcp::socket::raw::Socket::set_hop_limit(self, hop_limit);
|
||||
}
|
||||
|
||||
fn new_socket(
|
||||
socket_set: &mut SocketSet,
|
||||
@@ -80,8 +82,8 @@ impl<'a> SchemeSocket for RawSocket<'a> {
|
||||
vec![0; Router::MTU * Smolnetd::SOCKET_BUFFER_SIZE],
|
||||
);
|
||||
let ip_socket = RawSocket::new(
|
||||
Some(IpVersion::Ipv4),
|
||||
Some(IpProtocol::from(proto)),
|
||||
IpVersion::Ipv4,
|
||||
IpProtocol::from(proto),
|
||||
rx_buffer,
|
||||
tx_buffer,
|
||||
);
|
||||
@@ -139,16 +141,16 @@ impl<'a> SchemeSocket for RawSocket<'a> {
|
||||
|
||||
fn fpath(&self, _file: &SchemeFile<Self>, buf: &mut [u8]) -> SyscallResult<usize> {
|
||||
FpathWriter::with(buf, "ip", |w| {
|
||||
write!(w, "{}", self.ip_protocol().unwrap()).unwrap();
|
||||
write!(w, "{}", self.ip_protocol()).unwrap();
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_recvmsg(
|
||||
&mut self,
|
||||
file: &mut SchemeFile<Self>,
|
||||
how: &mut [u8],
|
||||
flags: usize,
|
||||
_file: &mut SchemeFile<Self>,
|
||||
_how: &mut [u8],
|
||||
_flags: usize,
|
||||
) -> SyscallResult<usize> {
|
||||
return Err(SyscallError::new(syscall::EOPNOTSUPP));
|
||||
}
|
||||
|
||||
+98
-16
@@ -1,6 +1,7 @@
|
||||
use crate::link::ethernet::EthernetLink;
|
||||
use crate::link::LinkDevice;
|
||||
use crate::link::{loopback::LoopbackDevice, DeviceList};
|
||||
use crate::observer::{Observer, ObserverRef};
|
||||
use crate::router::route_table::{RouteTable, Rule};
|
||||
use crate::router::Router;
|
||||
use crate::scheme::smoltcp::iface::SocketSet as SmoltcpSocketSet;
|
||||
@@ -33,19 +34,25 @@ use syscall::Error as SyscallError;
|
||||
use self::icmp::IcmpScheme;
|
||||
use self::ip::IpScheme;
|
||||
use self::netcfg::NetCfgScheme;
|
||||
use self::netfilter::NetFilterScheme;
|
||||
use self::tcp::TcpScheme;
|
||||
use self::tun::TunScheme;
|
||||
use self::udp::UdpScheme;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::filter::{FilterTable, PacketContext, Verdict};
|
||||
|
||||
mod icmp;
|
||||
mod ip;
|
||||
mod netcfg;
|
||||
mod netfilter;
|
||||
mod socket;
|
||||
mod tcp;
|
||||
mod tun;
|
||||
mod udp;
|
||||
|
||||
type SocketSet = SmoltcpSocketSet<'static>;
|
||||
type Interface = Rc<RefCell<SmoltcpInterface>>;
|
||||
type FilterTableRef = Rc<RefCell<FilterTable>>;
|
||||
|
||||
const MAX_DURATION: Duration = Duration::from_micros(u64::MAX);
|
||||
const MIN_DURATION: Duration = Duration::from_micros(0);
|
||||
@@ -70,6 +77,10 @@ pub struct Smolnetd {
|
||||
tcp_scheme: TcpScheme,
|
||||
icmp_scheme: IcmpScheme,
|
||||
netcfg_scheme: NetCfgScheme,
|
||||
netfilter_scheme: NetFilterScheme,
|
||||
tun_scheme: TunScheme,
|
||||
filter_table: FilterTableRef,
|
||||
observer: ObserverRef,
|
||||
}
|
||||
|
||||
impl Smolnetd {
|
||||
@@ -79,41 +90,66 @@ impl Smolnetd {
|
||||
pub const MAX_CHECK_TIMEOUT: Duration = Duration::from_millis(500);
|
||||
|
||||
pub fn new(
|
||||
network_file: Fd,
|
||||
hardware_addr: EthernetAddress,
|
||||
network_files: Vec<(Fd, EthernetAddress, String)>,
|
||||
ip_file: Socket,
|
||||
udp_file: Socket,
|
||||
tcp_file: Socket,
|
||||
icmp_file: Socket,
|
||||
time_file: Fd,
|
||||
netcfg_file: Socket,
|
||||
netfilter_file: Socket,
|
||||
tun_file: Socket,
|
||||
) -> Result<Smolnetd> {
|
||||
let protocol_addrs = vec![
|
||||
//This is a placeholder IP for DHCP
|
||||
IpCidr::new(IpAddress::v4(0, 0, 0, 0), 8),
|
||||
IpCidr::new(IpAddress::v4(127, 0, 0, 1), 8),
|
||||
IpCidr::new(IpAddress::v6(0, 0, 0, 0, 0, 0, 0, 1), 128),
|
||||
];
|
||||
|
||||
let default_gw = Ipv4Address::from_str(getcfg("ip_router").unwrap().trim())
|
||||
.expect("Can't parse the 'ip_router' cfg.");
|
||||
// Default gateway from /etc/net/ip_router. If the file is missing
|
||||
// or malformed, fall back to 0.0.0.0 and emit a warning rather
|
||||
// than panicking. The route lookup below will return None for
|
||||
// an invalid gateway, which is the correct degraded behavior.
|
||||
let default_gw = match getcfg("ip_router") {
|
||||
Ok(s) => Ipv4Address::from_str(s.trim()).unwrap_or_else(|_| {
|
||||
log::warn!("smolnetd: invalid ip_router '{}' in cfg, using 0.0.0.0", s);
|
||||
Ipv4Address::new(0, 0, 0, 0)
|
||||
}),
|
||||
Err(e) => {
|
||||
log::warn!("smolnetd: ip_router not set in cfg ({:?}), using 0.0.0.0", e);
|
||||
Ipv4Address::new(0, 0, 0, 0)
|
||||
}
|
||||
};
|
||||
|
||||
let devices = Rc::new(RefCell::new(DeviceList::default()));
|
||||
let route_table = Rc::new(RefCell::new(RouteTable::default()));
|
||||
let observer = Observer::new();
|
||||
let mut network_device = Tracer::new(
|
||||
Router::new(Rc::clone(&devices), Rc::clone(&route_table)),
|
||||
Router::new(Rc::clone(&devices), Rc::clone(&route_table), Rc::clone(&observer)),
|
||||
|_timestamp, printer| trace!("{}", printer),
|
||||
);
|
||||
|
||||
let ip_forward = network_device.get_mut().ip_forward.clone();
|
||||
|
||||
let config = Config::new(HardwareAddress::Ip);
|
||||
let mut iface = SmoltcpInterface::new(config, &mut network_device, Instant::now());
|
||||
iface.update_ip_addrs(|ip_addrs| ip_addrs.extend(protocol_addrs));
|
||||
iface
|
||||
.routes_mut()
|
||||
.add_default_ipv4_route(default_gw)
|
||||
.expect("Failed to add default gateway");
|
||||
// Skip the default route if the gateway is 0.0.0.0 (no gateway
|
||||
// configured). smoltcp rejects a default route with 0.0.0.0 as
|
||||
// gateway. Falling back to no default route is correct behavior.
|
||||
if !default_gw.is_unspecified() {
|
||||
if let Err(e) = iface
|
||||
.routes_mut()
|
||||
.add_default_ipv4_route(default_gw)
|
||||
{
|
||||
log::warn!("smolnetd: failed to add default route: {:?}", e);
|
||||
}
|
||||
}
|
||||
|
||||
let iface = Rc::new(RefCell::new(iface));
|
||||
let socket_set = Rc::new(RefCell::new(SocketSet::new(vec![])));
|
||||
let filter_table = Rc::new(RefCell::new(FilterTable::new()));
|
||||
|
||||
let loopback = LoopbackDevice::default();
|
||||
route_table.borrow_mut().insert_rule(Rule::new(
|
||||
@@ -122,14 +158,26 @@ impl Smolnetd {
|
||||
Rc::clone(loopback.name()),
|
||||
"127.0.0.1".parse().unwrap(),
|
||||
));
|
||||
|
||||
let mut eth0 = EthernetLink::new("eth0", unsafe {
|
||||
File::from_raw_fd(network_file.into_raw() as RawFd)
|
||||
});
|
||||
eth0.set_mac_address(hardware_addr);
|
||||
route_table.borrow_mut().insert_rule(Rule::new(
|
||||
"::1/128".parse().unwrap(),
|
||||
None,
|
||||
Rc::clone(loopback.name()),
|
||||
"::1".parse().unwrap(),
|
||||
));
|
||||
|
||||
devices.borrow_mut().push(loopback);
|
||||
devices.borrow_mut().push(eth0);
|
||||
for (i, (nf, hw_addr, name)) in network_files.into_iter().enumerate() {
|
||||
let dev_name = if name.is_empty() {
|
||||
format!("eth{}", i)
|
||||
} else {
|
||||
name
|
||||
};
|
||||
let mut link = EthernetLink::new(&dev_name, unsafe {
|
||||
File::from_raw_fd(nf.into_raw() as RawFd)
|
||||
});
|
||||
link.set_mac_address(hw_addr);
|
||||
devices.borrow_mut().push(link);
|
||||
}
|
||||
|
||||
Ok(Smolnetd {
|
||||
iface: Rc::clone(&iface),
|
||||
@@ -170,7 +218,18 @@ impl Smolnetd {
|
||||
netcfg_file,
|
||||
Rc::clone(&route_table),
|
||||
Rc::clone(&devices),
|
||||
Rc::clone(&socket_set),
|
||||
ip_forward,
|
||||
Rc::clone(&observer),
|
||||
Rc::clone(&filter_table),
|
||||
)?,
|
||||
netfilter_scheme: NetFilterScheme::new(
|
||||
netfilter_file,
|
||||
Rc::clone(&filter_table),
|
||||
)?,
|
||||
tun_scheme: TunScheme::new(tun_file, Rc::clone(&devices))?,
|
||||
filter_table,
|
||||
observer,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -215,6 +274,17 @@ impl Smolnetd {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_netfilter_scheme_event(&mut self) -> Result<()> {
|
||||
self.netfilter_scheme.on_scheme_event()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn on_tun_scheme_event(&mut self) -> Result<()> {
|
||||
self.tun_scheme.on_scheme_event()?;
|
||||
let _ = self.poll()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn schedule_time_event(&mut self, timeout: Duration) -> Result<()> {
|
||||
let mut time = TimeSpec::default();
|
||||
if self.time_file.read(&mut time)? < size_of::<TimeSpec>() {
|
||||
@@ -248,10 +318,22 @@ impl Smolnetd {
|
||||
|
||||
self.router_device.get_mut().poll(timestamp);
|
||||
|
||||
self.router_device.get_mut().forward_packets(&self.filter_table, timestamp);
|
||||
|
||||
// INPUT filter hook: drop packets before smoltcp processing.
|
||||
// Mirrors Linux's NF_INET_LOCAL_IN hook in iptable_filter.c.
|
||||
self.router_device.get_mut().filter_input(&self.filter_table, timestamp);
|
||||
|
||||
// TODO: Check what if the bool returned by poll can be useful
|
||||
iface.poll(timestamp, &mut self.router_device, &mut socket_set);
|
||||
|
||||
self.router_device.get_mut().dispatch(timestamp);
|
||||
self.router_device.get_mut().dispatch(timestamp, &self.filter_table);
|
||||
|
||||
self.filter_table
|
||||
.borrow_mut()
|
||||
.conntrack
|
||||
.as_mut()
|
||||
.map(|ct| ct.clean_expired(timestamp));
|
||||
|
||||
if !self.router_device.get_ref().can_recv() {
|
||||
match iface.poll_delay(timestamp, &socket_set) {
|
||||
|
||||
@@ -7,8 +7,8 @@ use redox_scheme::{
|
||||
CallerCtx, OpenResult, RequestKind, SignalBehavior, Socket,
|
||||
};
|
||||
use scheme_utils::HandleMap;
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address};
|
||||
use std::cell::RefCell;
|
||||
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv6Address};
|
||||
use std::cell::{Cell, RefCell};
|
||||
use std::collections::BTreeMap;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
@@ -22,11 +22,12 @@ use syscall::{Error as SyscallError, EventFlags as SyscallEventFlags, Result as
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::link::DeviceList;
|
||||
use crate::observer::ObserverRef;
|
||||
use crate::router::route_table::{RouteTable, Rule};
|
||||
|
||||
use self::nodes::*;
|
||||
use self::notifier::*;
|
||||
use super::{post_fevent, Interface};
|
||||
use super::{post_fevent, Interface, SocketSet};
|
||||
|
||||
const WRITE_BUFFER_MAX_SIZE: usize = 0xffff;
|
||||
|
||||
@@ -35,6 +36,35 @@ fn gateway_cidr() -> IpCidr {
|
||||
IpCidr::new(IpAddress::v4(0, 0, 0, 0), 0)
|
||||
}
|
||||
|
||||
fn cidr_network(cidr: IpCidr) -> IpCidr {
|
||||
match cidr {
|
||||
IpCidr::Ipv4(c) => IpCidr::Ipv4(c.network()),
|
||||
IpCidr::Ipv6(c) => {
|
||||
let prefix = c.prefix_len();
|
||||
let bytes = c.address().octets();
|
||||
let mut masked = [0u8; 16];
|
||||
let full_bytes = (prefix / 8) as usize;
|
||||
let remainder = prefix % 8;
|
||||
for i in 0..full_bytes {
|
||||
masked[i] = bytes[i];
|
||||
}
|
||||
if remainder > 0 && full_bytes < 16 {
|
||||
masked[full_bytes] = bytes[full_bytes] & (0xff << (8 - remainder));
|
||||
}
|
||||
let segments: [u16; 8] = core::array::from_fn(|i| {
|
||||
u16::from_be_bytes([masked[i * 2], masked[i * 2 + 1]])
|
||||
});
|
||||
IpCidr::Ipv6(smoltcp::wire::Ipv6Cidr::new(
|
||||
Ipv6Address::new(
|
||||
segments[0], segments[1], segments[2], segments[3],
|
||||
segments[4], segments[5], segments[6], segments[7],
|
||||
),
|
||||
prefix,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_route(value: &str, route_table: &RouteTable) -> SyscallResult<Rule> {
|
||||
let mut parts = value.split_whitespace();
|
||||
let cidr_str = parts.next().ok_or(SyscallError::new(syscall::EINVAL))?;
|
||||
@@ -45,24 +75,55 @@ fn parse_route(value: &str, route_table: &RouteTable) -> SyscallResult<Rule> {
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?,
|
||||
};
|
||||
|
||||
let via: IpAddress = match parts.next().ok_or(SyscallError::new(syscall::EINVAL))? {
|
||||
"via" => parts
|
||||
.next()
|
||||
.ok_or(SyscallError::new(syscall::EINVAL))?
|
||||
.parse()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?,
|
||||
let dev;
|
||||
let via;
|
||||
let src;
|
||||
|
||||
// Accept "via <ip>" or "dev <name>" or both.
|
||||
match parts.next() {
|
||||
Some("via") => {
|
||||
let gw: IpAddress = parts
|
||||
.next()
|
||||
.ok_or(SyscallError::new(syscall::EINVAL))?
|
||||
.parse()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
if !gw.is_unicast() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
let rule = route_table
|
||||
.lookup_rule(&gw)
|
||||
.ok_or(SyscallError::new(syscall::EINVAL))?;
|
||||
via = Some(gw);
|
||||
dev = rule.dev.clone();
|
||||
src = rule.src;
|
||||
}
|
||||
Some("dev") => {
|
||||
dev = parts
|
||||
.next()
|
||||
.ok_or(SyscallError::new(syscall::EINVAL))?
|
||||
.into();
|
||||
via = None;
|
||||
// Use 0.0.0.0 as source for direct routes — the routing
|
||||
// layer will pick the correct source IP when sending.
|
||||
src = IpAddress::v4(0, 0, 0, 0);
|
||||
}
|
||||
_ => return Err(SyscallError::new(syscall::EINVAL)),
|
||||
};
|
||||
|
||||
if !via.is_unicast() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
let mut metric: u32 = 0;
|
||||
if let Some(keyword) = parts.next() {
|
||||
if keyword == "metric" {
|
||||
metric = parts
|
||||
.next()
|
||||
.ok_or(SyscallError::new(syscall::EINVAL))?
|
||||
.parse::<u32>()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
} else {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
}
|
||||
|
||||
let rule = route_table
|
||||
.lookup_rule(&via)
|
||||
.ok_or(SyscallError::new(syscall::EINVAL))?;
|
||||
|
||||
Ok(Rule::new(cidr, Some(via), rule.dev.clone(), rule.src))
|
||||
Ok(Rule::new(cidr, via, dev, src).with_metric(metric))
|
||||
}
|
||||
|
||||
fn mk_root_node(
|
||||
@@ -71,8 +132,202 @@ fn mk_root_node(
|
||||
dns_config: DNSConfigRef,
|
||||
route_table: Rc<RefCell<RouteTable>>,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
socket_set: Rc<RefCell<SocketSet>>,
|
||||
ip_forward: Rc<Cell<bool>>,
|
||||
observer: ObserverRef,
|
||||
filter_table: Rc<RefCell<crate::filter::FilterTable>>,
|
||||
) -> CfgNodeRef {
|
||||
cfg_node! {
|
||||
"help" => {
|
||||
ro [] || {
|
||||
"summary — one-read network state\n\
|
||||
sockets — active socket list (TCP/UDP/ICMP)\n\
|
||||
conntrack — connection tracking (stats / list)\n\
|
||||
nat — NAT bindings (bindings / stats)\n\
|
||||
capture — packet capture (enable/read/filter/count)\n\
|
||||
sysctl — kernel tunables (net/ipv4/ip_forward)\n\
|
||||
route — routing table (list/add/rm/flush/count/gateway)\n\
|
||||
resolv — DNS configuration (nameserver/nameserver6)\n\
|
||||
ifaces — per-interface config/stats (eth0/lo/...)\n\
|
||||
version — netstack version info\n"
|
||||
.to_string()
|
||||
}
|
||||
},
|
||||
"version" => {
|
||||
ro [] || {
|
||||
format!("{}\n", env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
},
|
||||
"summary" => {
|
||||
ro [devices, route_table, socket_set, ip_forward, filter_table] || {
|
||||
let mut out = String::new();
|
||||
let devs = devices.borrow();
|
||||
let eth0 = devs.get("eth0");
|
||||
let lo = devs.get("loopback");
|
||||
let eth_state = eth0.map(|d| d.link_state()).unwrap_or("missing");
|
||||
let lo_state = lo.map(|d| d.link_state()).unwrap_or("missing");
|
||||
out.push_str(&format!("interfaces: eth0={} lo={}\n", eth_state, lo_state));
|
||||
out.push_str(&format!("routes: {}\n", route_table.borrow().len()));
|
||||
let set = socket_set.borrow();
|
||||
let mut tcp = 0u32; let mut udp = 0u32; let mut icmp = 0u32; let mut raw = 0u32;
|
||||
for (_handle, socket) in set.iter() {
|
||||
match socket {
|
||||
smoltcp::socket::Socket::Tcp(_) => tcp += 1,
|
||||
smoltcp::socket::Socket::Udp(_) => udp += 1,
|
||||
smoltcp::socket::Socket::Icmp(_) => icmp += 1,
|
||||
smoltcp::socket::Socket::Raw(_) => raw += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
out.push_str(&format!("sockets: {} (tcp={} udp={} icmp={} raw={})\n",
|
||||
tcp + udp + icmp + raw, tcp, udp, icmp, raw));
|
||||
out.push_str(&format!("ip_forward: {}\n",
|
||||
if ip_forward.get() { "1" } else { "0" }));
|
||||
if let Some(dev) = eth0 {
|
||||
let s = dev.statistics();
|
||||
out.push_str(&format!("eth0 stats: rx={}/{} tx={}/{} err={}/{} drop={}/{}\n",
|
||||
s.rx_bytes, s.rx_packets, s.tx_bytes, s.tx_packets,
|
||||
s.rx_errors, s.tx_errors, s.rx_dropped, s.tx_dropped));
|
||||
}
|
||||
out.push_str("filter chains:\n");
|
||||
out.push_str(&filter_table.borrow().chain_summary());
|
||||
if let Some(ref ct) = filter_table.borrow().conntrack {
|
||||
out.push_str("conntrack stats:\n");
|
||||
out.push_str(&ct.stats());
|
||||
}
|
||||
out
|
||||
}
|
||||
},
|
||||
"conntrack" => {
|
||||
"stats" => {
|
||||
ro [filter_table] || {
|
||||
let table = filter_table.borrow();
|
||||
if let Some(ref ct) = table.conntrack {
|
||||
ct.stats()
|
||||
} else {
|
||||
"conntrack: not enabled\n".to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
"list" => {
|
||||
ro [filter_table] || {
|
||||
let table = filter_table.borrow();
|
||||
if let Some(ref ct) = table.conntrack {
|
||||
ct.format()
|
||||
} else {
|
||||
"conntrack: not enabled\n".to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
"nat" => {
|
||||
"bindings" => {
|
||||
ro [filter_table] || {
|
||||
filter_table.borrow().nat_table.format_bindings()
|
||||
}
|
||||
},
|
||||
"stats" => {
|
||||
ro [filter_table] || {
|
||||
filter_table.borrow().nat_table.format()
|
||||
}
|
||||
},
|
||||
},
|
||||
"capture" => {
|
||||
"enable" => {
|
||||
wo [observer] (Option<()>, None)
|
||||
|_cur_value, _line| { Ok(()) }
|
||||
|_cur_value| {
|
||||
observer.enable();
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
"disable" => {
|
||||
wo [observer] (Option<()>, None)
|
||||
|_cur_value, _line| { Ok(()) }
|
||||
|_cur_value| {
|
||||
observer.disable();
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
"read" => {
|
||||
ro [observer] || {
|
||||
if observer.is_enabled() {
|
||||
observer.drain_hex()
|
||||
} else {
|
||||
"capture disabled (echo to /scheme/netcfg/capture/enable to start)\n".to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
"filter" => {
|
||||
rw [observer] (Option<String>, None)
|
||||
|| {
|
||||
format!("{}\n", observer.filter_str())
|
||||
}
|
||||
|cur_value, line| {
|
||||
let s = line.trim().to_string();
|
||||
observer.set_filter(&s);
|
||||
*cur_value = Some(s);
|
||||
Ok(())
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
},
|
||||
"count" => {
|
||||
ro [observer] || {
|
||||
format!("captured={} buffered={} enabled={}\n",
|
||||
observer.total(), observer.len(), observer.is_enabled())
|
||||
}
|
||||
},
|
||||
},
|
||||
"sockets" => {
|
||||
"list" => {
|
||||
ro [socket_set] || {
|
||||
let set = socket_set.borrow();
|
||||
let mut tcp = 0u32;
|
||||
let mut udp = 0u32;
|
||||
let mut icmp = 0u32;
|
||||
let mut raw = 0u32;
|
||||
let mut out = String::from("");
|
||||
for (_handle, socket) in set.iter() {
|
||||
match socket {
|
||||
smoltcp::socket::Socket::Tcp(_) => tcp += 1,
|
||||
smoltcp::socket::Socket::Udp(_) => udp += 1,
|
||||
smoltcp::socket::Socket::Icmp(_) => icmp += 1,
|
||||
smoltcp::socket::Socket::Raw(_) => raw += 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let total = tcp + udp + icmp + raw;
|
||||
out.push_str(&format!("sockets: {} (tcp={} udp={} icmp={} raw={})\n",
|
||||
total, tcp, udp, icmp, raw));
|
||||
for (_handle, socket) in set.iter() {
|
||||
if let smoltcp::socket::Socket::Tcp(tcp_sock) = socket {
|
||||
let local = tcp_sock.local_endpoint();
|
||||
let remote = tcp_sock.remote_endpoint();
|
||||
let state = tcp_sock.state();
|
||||
let sq = tcp_sock.send_queue();
|
||||
let rq = tcp_sock.recv_queue();
|
||||
let sc = tcp_sock.send_capacity();
|
||||
let rc = tcp_sock.recv_capacity();
|
||||
out.push_str(&format!("tcp: {:?} {} -> {} sendq={}/{} recvq={}/{}\n",
|
||||
state,
|
||||
local.map(|e| format!("{}", e)).unwrap_or_else(|| "-".to_string()),
|
||||
remote.map(|e| format!("{}", e)).unwrap_or_else(|| "-".to_string()),
|
||||
sq, sc, rq, rc));
|
||||
}
|
||||
if let smoltcp::socket::Socket::Udp(udp_sock) = socket {
|
||||
let endpoint = udp_sock.endpoint();
|
||||
out.push_str(&format!("udp: local={}\n",
|
||||
if endpoint.port != 0 {
|
||||
format!("{}", endpoint)
|
||||
} else {
|
||||
"-".to_string()
|
||||
}));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
},
|
||||
"resolv" => {
|
||||
"nameserver" => {
|
||||
rw [dns_config, notifier] (Option<Ipv4Address>, None)
|
||||
@@ -99,6 +354,35 @@ fn mk_root_node(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
"nameserver6" => {
|
||||
rw [dns_config, notifier] (Option<Ipv6Address>, None)
|
||||
|| {
|
||||
match dns_config.borrow().name_server6 {
|
||||
Some(ip) => format!("{}\n", ip),
|
||||
None => "Not configured\n".into(),
|
||||
}
|
||||
}
|
||||
|cur_value, line| {
|
||||
if cur_value.is_none() {
|
||||
let ip = Ipv6Address::from_str(line.trim())
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
if ip.is_multicast() || ip.is_unspecified() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
*cur_value = Some(ip);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::EINVAL))
|
||||
}
|
||||
}
|
||||
|cur_value| {
|
||||
if let Some(ip) = *cur_value {
|
||||
dns_config.borrow_mut().name_server6 = Some(ip);
|
||||
notifier.borrow_mut().schedule_notify("resolv/nameserver6");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
},
|
||||
"route" => {
|
||||
@@ -107,6 +391,46 @@ fn mk_root_node(
|
||||
format!("{}", route_table.borrow())
|
||||
}
|
||||
},
|
||||
"count" => {
|
||||
ro [route_table] || {
|
||||
let count = route_table.borrow().len();
|
||||
format!("routes: {}\n", count)
|
||||
}
|
||||
},
|
||||
"gateway" => {
|
||||
ro [route_table] || {
|
||||
let table = route_table.borrow();
|
||||
let zero: IpAddress = IpAddress::v4(0, 0, 0, 0);
|
||||
let gw = table.lookup_gateway(&zero);
|
||||
match gw {
|
||||
Some(addr) => format!("default via {}\n", addr),
|
||||
None => "no default route\n".to_string(),
|
||||
}
|
||||
}
|
||||
},
|
||||
"lookup" => {
|
||||
rw [route_table] (Option<String>, None)
|
||||
|| {
|
||||
"write IP address to query route\n".to_string()
|
||||
}
|
||||
|cur_value, line| {
|
||||
let ip: IpAddress = line.trim().parse()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
let table = route_table.borrow();
|
||||
let rule = table.lookup_rule(&ip);
|
||||
match rule {
|
||||
Some(r) => {
|
||||
*cur_value = Some(format!("{}", r));
|
||||
Ok(())
|
||||
}
|
||||
None => {
|
||||
*cur_value = Some(format!("no route to {}\n", ip));
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
},
|
||||
"add" => {
|
||||
wo [iface, notifier, route_table] (Option<Rule>, None)
|
||||
|cur_value, line| {
|
||||
@@ -132,13 +456,14 @@ fn mk_root_node(
|
||||
wo [iface, notifier, route_table] (Option<IpCidr>, None)
|
||||
|cur_value, line| {
|
||||
if cur_value.is_none() {
|
||||
match line.parse() {
|
||||
Ok(cidr) => {
|
||||
*cur_value = Some(cidr);
|
||||
Ok(())
|
||||
}
|
||||
Err(_) => Err(SyscallError::new(syscall::EINVAL))
|
||||
}
|
||||
let cidr = if line.trim() == "default" {
|
||||
gateway_cidr()
|
||||
} else {
|
||||
line.parse::<IpCidr>()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?
|
||||
};
|
||||
*cur_value = Some(cidr);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::EINVAL))
|
||||
}
|
||||
@@ -154,6 +479,35 @@ fn mk_root_node(
|
||||
}
|
||||
},
|
||||
},
|
||||
"sysctl" => {
|
||||
"net" => {
|
||||
"ipv4" => {
|
||||
"ip_forward" => {
|
||||
rw [ip_forward] (Option<bool>, None)
|
||||
|| {
|
||||
let val: u8 = if ip_forward.get() { 1 } else { 0 };
|
||||
format!("{}\n", val)
|
||||
}
|
||||
|cur_value, line| {
|
||||
let val = line.trim().parse::<u8>()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
match val {
|
||||
0 => { *cur_value = Some(false); }
|
||||
1 => { *cur_value = Some(true); }
|
||||
_ => return Err(SyscallError::new(syscall::EINVAL)),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|cur_value| {
|
||||
if let Some(enabled) = cur_value {
|
||||
ip_forward.set(*enabled);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"ifaces" => {
|
||||
"eth0" => {
|
||||
"mac" => {
|
||||
@@ -230,8 +584,7 @@ fn mk_root_node(
|
||||
|
||||
let mut route_table = route_table.borrow_mut();
|
||||
if let Some(old_addr) = dev.ip_address() {
|
||||
let IpCidr::Ipv4(old_v4_cidr) = old_addr;
|
||||
let old_network = IpCidr::Ipv4(old_v4_cidr.network());
|
||||
let old_network = cidr_network(old_addr);
|
||||
|
||||
route_table.remove_rule(old_network);
|
||||
route_table.change_src(old_addr.address(), cidr.address());
|
||||
@@ -247,17 +600,266 @@ fn mk_root_node(
|
||||
// job to find give this source.
|
||||
iface.borrow_mut().update_ip_addrs(|addrs| addrs.insert(0, cidr).unwrap());
|
||||
|
||||
let IpCidr::Ipv4(v4_cidr) = cidr;
|
||||
let network_cidr = IpCidr::Ipv4(v4_cidr.network());
|
||||
let network_cidr = cidr_network(cidr);
|
||||
route_table.insert_rule(Rule::new(network_cidr, None, dev.name().clone(), cidr.address()))
|
||||
}
|
||||
notifier.borrow_mut().schedule_notify("ifaces/eth0/addr/list");
|
||||
notifier.borrow_mut().schedule_notify("route/list");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
"flush" => {
|
||||
wo [route_table, notifier] (Option<()>, None)
|
||||
|_cur_value, _line| {
|
||||
Ok(())
|
||||
}
|
||||
|_cur_value| {
|
||||
*route_table.borrow_mut() = RouteTable::default();
|
||||
notifier.borrow_mut().schedule_notify("route/list");
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
},
|
||||
"arp" => {
|
||||
"list" => {
|
||||
ro [devices] || {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => dev.arp_table(),
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats" => {
|
||||
ro [devices] || {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => dev.arp_stats(),
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
},
|
||||
"max" => {
|
||||
ro [] || {
|
||||
"1024\n".to_string()
|
||||
}
|
||||
},
|
||||
"flush" => {
|
||||
wo [devices] (Option<()>, None)
|
||||
|_cur_value, _line| {
|
||||
Ok(())
|
||||
}
|
||||
|cur_value| {
|
||||
*cur_value = None;
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
dev.flush_arp();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
},
|
||||
"add" => {
|
||||
wo [devices] (Option<()>, None)
|
||||
|_cur_value, line| {
|
||||
let s = line.trim();
|
||||
let parts: Vec<&str> = s.split_whitespace().collect();
|
||||
if parts.len() != 2 {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
let ip: IpAddress = parts[0].parse::<IpAddress>()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
let mut mac = [0u8; 6];
|
||||
let mut idx = 0;
|
||||
for part in parts[1].split(|c: char| c == ':' || c == '-') {
|
||||
if idx >= 6 {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
mac[idx] = u8::from_str_radix(part, 16)
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
idx += 1;
|
||||
}
|
||||
if idx != 6 {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
dev.add_static_neighbor(ip, mac).map_err(|_| {
|
||||
SyscallError::new(syscall::EINVAL)
|
||||
})?;
|
||||
} else {
|
||||
return Err(SyscallError::new(syscall::ENODEV));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
},
|
||||
"del" => {
|
||||
wo [devices] (Option<()>, None)
|
||||
|_cur_value, line| {
|
||||
let ip: IpAddress = line.trim().parse()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
if !dev.remove_neighbor(ip) {
|
||||
return Err(SyscallError::new(syscall::ENOENT));
|
||||
}
|
||||
} else {
|
||||
return Err(SyscallError::new(syscall::ENODEV));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
},
|
||||
},
|
||||
"stats" => {
|
||||
ro [devices] || {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => {
|
||||
let s = dev.statistics();
|
||||
format!("rx_bytes={} rx_packets={} tx_bytes={} tx_packets={} rx_errors={} tx_errors={} rx_dropped={} tx_dropped={} mtu={} link={}\n",
|
||||
s.rx_bytes, s.rx_packets, s.tx_bytes, s.tx_packets,
|
||||
s.rx_errors, s.tx_errors, s.rx_dropped, s.tx_dropped,
|
||||
dev.mtu(), dev.link_state())
|
||||
}
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
},
|
||||
"link" => {
|
||||
ro [devices] || {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => format!("{}\n", dev.link_state()),
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
},
|
||||
"mtu" => {
|
||||
rw [devices] (Option<usize>, None)
|
||||
|| {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => format!("{}\n", dev.mtu()),
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
|cur_value, line| {
|
||||
let mtu = line.trim().parse::<usize>()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
dev.set_mtu(mtu);
|
||||
*cur_value = Some(mtu);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::ENODEV))
|
||||
}
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
},
|
||||
"qdisc" => {
|
||||
rw [devices] (Option<String>, None)
|
||||
|| {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => dev.qdisc_info(),
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
|cur_value, line| {
|
||||
let kind = line.trim().to_string();
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
dev.set_qdisc(&kind).map_err(|e| {
|
||||
SyscallError::new(syscall::EINVAL)
|
||||
})?;
|
||||
*cur_value = Some(kind);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::ENODEV))
|
||||
}
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
},
|
||||
"enabled" => {
|
||||
rw [devices] (Option<bool>, None)
|
||||
|| {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => if dev.is_enabled() { "up\n".to_string() } else { "down\n".to_string() },
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
|cur_value, line| {
|
||||
let s = line.trim();
|
||||
match s {
|
||||
"up" | "1" | "true" | "yes" | "on" => {
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
dev.set_enabled(true);
|
||||
*cur_value = Some(true);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::ENODEV))
|
||||
}
|
||||
}
|
||||
"down" | "0" | "false" | "no" | "off" => {
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
dev.set_enabled(false);
|
||||
*cur_value = Some(false);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::ENODEV))
|
||||
}
|
||||
}
|
||||
_ => Err(SyscallError::new(syscall::EINVAL)),
|
||||
}
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
},
|
||||
"promiscuous" => {
|
||||
rw [devices] (Option<bool>, None)
|
||||
|| {
|
||||
match devices.borrow().get("eth0") {
|
||||
Some(dev) => if dev.is_promiscuous() { "on\n".to_string() } else { "off\n".to_string() },
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
|cur_value, line| {
|
||||
let s = line.trim();
|
||||
let enabled = match s {
|
||||
"on" | "1" | "true" | "yes" => true,
|
||||
"off" | "0" | "false" | "no" => false,
|
||||
_ => return Err(SyscallError::new(syscall::EINVAL)),
|
||||
};
|
||||
if let Some(dev) = devices.borrow_mut().get_mut("eth0") {
|
||||
dev.set_promiscuous(enabled);
|
||||
*cur_value = Some(enabled);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::ENODEV))
|
||||
}
|
||||
}
|
||||
|_cur_value| { Ok(()) }
|
||||
}
|
||||
},
|
||||
"lo" => {
|
||||
"addr" => {
|
||||
"list" => {
|
||||
ro [devices] || {
|
||||
match devices.borrow().get("loopback") {
|
||||
Some(dev) => match dev.ip_address() {
|
||||
Some(addr) => format!("{addr}\n"),
|
||||
None => "Not configured\n".into(),
|
||||
},
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"stats" => {
|
||||
ro [devices] || {
|
||||
match devices.borrow().get("loopback") {
|
||||
Some(dev) => {
|
||||
let s = dev.statistics();
|
||||
format!("rx_bytes={} rx_packets={} tx_bytes={} tx_packets={} rx_errors={} tx_errors={} rx_dropped={} tx_dropped={} mtu={} link={}\n",
|
||||
s.rx_bytes, s.rx_packets, s.tx_bytes, s.tx_packets,
|
||||
s.rx_errors, s.tx_errors, s.rx_dropped, s.tx_dropped,
|
||||
dev.mtu(), dev.link_state())
|
||||
}
|
||||
None => "Device not found\n".into(),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,6 +867,7 @@ fn mk_root_node(
|
||||
|
||||
struct DNSConfig {
|
||||
name_server: Ipv4Address,
|
||||
name_server6: Option<Ipv6Address>,
|
||||
}
|
||||
|
||||
type DNSConfigRef = Rc<RefCell<DNSConfig>>;
|
||||
@@ -337,10 +940,15 @@ impl NetCfgScheme {
|
||||
scheme_file: Socket,
|
||||
route_table: Rc<RefCell<RouteTable>>,
|
||||
devices: Rc<RefCell<DeviceList>>,
|
||||
socket_set: Rc<RefCell<SocketSet>>,
|
||||
ip_forward: Rc<Cell<bool>>,
|
||||
observer: ObserverRef,
|
||||
filter_table: Rc<RefCell<crate::filter::FilterTable>>,
|
||||
) -> Result<NetCfgScheme> {
|
||||
let notifier = Notifier::new_ref();
|
||||
let dns_config = Rc::new(RefCell::new(DNSConfig {
|
||||
name_server: Ipv4Address::new(8, 8, 8, 8),
|
||||
name_server6: None,
|
||||
}));
|
||||
let mut inner = NetCfgSchemeInner {
|
||||
scheme_file,
|
||||
@@ -351,6 +959,10 @@ impl NetCfgScheme {
|
||||
dns_config,
|
||||
route_table,
|
||||
devices,
|
||||
socket_set,
|
||||
ip_forward,
|
||||
observer,
|
||||
filter_table,
|
||||
),
|
||||
notifier,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,428 @@
|
||||
//! Netfilter scheme: exposes the `filter` table over the `netfilter:` scheme.
|
||||
//!
|
||||
//! This is the user-facing control plane that mirrors how Linux exposes
|
||||
//! netfilter to userspace:
|
||||
//! - Linux: `iptables -A INPUT ...` writes a rule via the `nf_setsockopt`
|
||||
//! interface (`net/ipv4/netfilter/ip_tables.c:ipt_setsockopt`).
|
||||
//! - Linux: `nft add rule ...` writes via the netlink interface
|
||||
//! (`net/netfilter/nf_tables_api.c`).
|
||||
//!
|
||||
//! In Red Bear, both mechanisms are replaced by the `netfilter:` scheme,
|
||||
//! which exposes a filesystem-style API:
|
||||
//!
|
||||
//! ```text
|
||||
//! /netfilter/
|
||||
//! ├── rule/
|
||||
//! │ ├── list — read: text dump of every rule (mirrors `iptables -L`)
|
||||
//! │ ├── add — write: append one rule (mirrors `iptables -A`)
|
||||
//! │ └── del — write: remove one rule by id (mirrors `iptables -D`)
|
||||
//! ├── policy/
|
||||
//! │ ├── input — read/write: default policy for INPUT (ACCEPT/DROP)
|
||||
//! │ ├── output — read/write: default policy for OUTPUT (ACCEPT/DROP)
|
||||
//! │ ├── forward — read/write: default policy for FORWARD (ACCEPT/DROP)
|
||||
//! │ ├── prerouting — read/write: default policy for PREROUTING
|
||||
//! │ └── postrouting — read/write: default policy for POSTROUTING
|
||||
//! └── reset — write: clear all rules and restore default policies
|
||||
//! ```
|
||||
//!
|
||||
//! Each `add` returns the newly assigned numeric rule id on a successful
|
||||
//! `fsync` (the standard scheme write/commit convention used elsewhere
|
||||
//! in netstack, e.g. `netcfg/`).
|
||||
|
||||
use redox_scheme::{
|
||||
scheme::{register_scheme_inner, SchemeState, SchemeSync},
|
||||
CallerCtx, OpenResult, RequestKind, SignalBehavior, Socket,
|
||||
};
|
||||
use scheme_utils::HandleMap;
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
use std::str;
|
||||
use syscall;
|
||||
use syscall::data::Stat;
|
||||
use syscall::flag::{MODE_DIR, MODE_FILE};
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
use syscall::{Error as SyscallError, Result as SyscallResult};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::filter::{parse_nat_rule, parse_rule, FilterTable, Hook, NatType, Verdict};
|
||||
|
||||
const WRITE_BUFFER_MAX_SIZE: usize = 0xffff;
|
||||
|
||||
type FilterTableRef = Rc<RefCell<FilterTable>>;
|
||||
|
||||
pub struct NetFilterScheme {
|
||||
inner: NetFilterSchemeInner,
|
||||
state: SchemeState,
|
||||
}
|
||||
|
||||
impl NetFilterScheme {
|
||||
pub fn new(scheme_file: Socket, table: FilterTableRef) -> Result<NetFilterScheme> {
|
||||
let mut inner = NetFilterSchemeInner {
|
||||
scheme_file,
|
||||
handles: HandleMap::new(),
|
||||
table,
|
||||
};
|
||||
let cap_id = inner
|
||||
.scheme_root()
|
||||
.map_err(|e| Error::from_syscall_error(e, "failed to get scheme root id"))?;
|
||||
register_scheme_inner(&inner.scheme_file, "netfilter", cap_id).map_err(|e| {
|
||||
Error::from_syscall_error(e, "failed to register netfilter scheme to namespace")
|
||||
})?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
state: SchemeState::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_scheme_event(&mut self) -> Result<Option<()>> {
|
||||
let result = loop {
|
||||
let request = match self.inner.scheme_file.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(req)) => req,
|
||||
Ok(None) => break Some(()),
|
||||
Err(error)
|
||||
if error.errno == syscall::EWOULDBLOCK || error.errno == syscall::EAGAIN =>
|
||||
{
|
||||
break None;
|
||||
}
|
||||
Err(other) => {
|
||||
return Err(Error::from_syscall_error(
|
||||
other,
|
||||
"failed to receive new request",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match request.kind() {
|
||||
RequestKind::Call(c) => {
|
||||
let resp = c.handle_sync(&mut self.inner, &mut self.state);
|
||||
let _ = self
|
||||
.inner
|
||||
.scheme_file
|
||||
.write_response(resp, SignalBehavior::Restart)
|
||||
.map_err(|e| {
|
||||
Error::from_syscall_error(e.into(), "failed to write response")
|
||||
})?;
|
||||
}
|
||||
RequestKind::OnClose { id } => {
|
||||
self.inner.on_close(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
enum Handle {
|
||||
SchemeRoot,
|
||||
File(NetFilterFile),
|
||||
}
|
||||
|
||||
struct NetFilterFile {
|
||||
path: String,
|
||||
is_dir: bool,
|
||||
is_writable: bool,
|
||||
is_readable: bool,
|
||||
read_buf: Vec<u8>,
|
||||
write_buf: Vec<u8>,
|
||||
pos: usize,
|
||||
done: bool,
|
||||
}
|
||||
|
||||
struct NetFilterSchemeInner {
|
||||
scheme_file: Socket,
|
||||
handles: HandleMap<Handle>,
|
||||
table: FilterTableRef,
|
||||
}
|
||||
|
||||
impl NetFilterSchemeInner {
|
||||
fn on_close(&mut self, fd: usize) {
|
||||
if let Some(handle) = self.handles.remove(fd) {
|
||||
match handle {
|
||||
Handle::SchemeRoot => {}
|
||||
Handle::File(mut file) => {
|
||||
if !file.done {
|
||||
let _ = file.commit(&self.table);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn open_path(&mut self, path: &str) -> SyscallResult<usize> {
|
||||
let is_dir = path.is_empty() || path.ends_with('/');
|
||||
let parts: Vec<&str> = if is_dir {
|
||||
path.trim_end_matches('/').split('/').collect()
|
||||
} else {
|
||||
path.split('/').collect()
|
||||
};
|
||||
|
||||
let (read_buf, is_readable, is_writable) = match parts.as_slice() {
|
||||
[] | [""] => (
|
||||
self.table.borrow().format().into_bytes(),
|
||||
true,
|
||||
false,
|
||||
),
|
||||
["rule", "list"] => (self.table.borrow().format().into_bytes(), true, false),
|
||||
["rule", "add"] | ["rule", "del"] => (Vec::new(), false, true),
|
||||
["nat", "list"] => (self.table.borrow().nat_table.format().into_bytes(), true, false),
|
||||
["nat", "add"] | ["nat", "del"] => (Vec::new(), false, true),
|
||||
["conntrack", "list"] => (
|
||||
self.table.borrow().conntrack.as_ref()
|
||||
.map(|ct| ct.format().into_bytes())
|
||||
.unwrap_or_else(|| b"conntrack: not enabled\n".to_vec()),
|
||||
true, false,
|
||||
),
|
||||
["conntrack", "stats"] => (
|
||||
self.table.borrow().conntrack.as_ref()
|
||||
.map(|ct| ct.stats().into_bytes())
|
||||
.unwrap_or_else(|| b"conntrack: not enabled\n".to_vec()),
|
||||
true, false,
|
||||
),
|
||||
["log"] => {
|
||||
let buf = &self.table.borrow().log_buffer;
|
||||
let mut out = String::new();
|
||||
for entry in buf.iter().rev().take(20) {
|
||||
out.push_str(entry);
|
||||
out.push('\n');
|
||||
}
|
||||
(out.into_bytes(), true, false)
|
||||
},
|
||||
["policy", name] => {
|
||||
let hook = parse_hook_name(name)
|
||||
.ok_or_else(|| SyscallError::new(syscall::ENOENT))?;
|
||||
let policy = self
|
||||
.table
|
||||
.borrow()
|
||||
.default_policy
|
||||
.get(&hook)
|
||||
.copied()
|
||||
.unwrap_or(Verdict::Accept);
|
||||
(format!("{}\n", policy.name()).into_bytes(), true, true)
|
||||
}
|
||||
["reset"] => (Vec::new(), false, true),
|
||||
["stats"] => {
|
||||
let table = self.table.borrow();
|
||||
let mut out = String::new();
|
||||
for &hook in &Hook::ALL {
|
||||
let name = hook.name();
|
||||
let (pkts, bytes) = table.chain_counters.get(&hook).copied().unwrap_or((0, 0));
|
||||
let policy = table.default_policy.get(&hook).copied().unwrap_or(Verdict::Accept);
|
||||
out.push_str(&format!("{}: packets={} bytes={} policy={}\n", name, pkts, bytes, policy.name()));
|
||||
}
|
||||
let snat_count = table.nat_table.rules.iter().filter(|r| r.nat_type == NatType::Snat).count();
|
||||
let dnat_count = table.nat_table.rules.len() - snat_count;
|
||||
out.push_str(&format!("rules: {} active (snat={} dnat={})\n",
|
||||
table.rules.len(), snat_count, dnat_count));
|
||||
if let Some(ref ct) = table.conntrack {
|
||||
out.push_str(&format!("conntrack: {}\n", ct.len()));
|
||||
out.push_str(&ct.stats());
|
||||
}
|
||||
(out.into_bytes(), true, false)
|
||||
},
|
||||
_ => return Err(SyscallError::new(syscall::ENOENT)),
|
||||
};
|
||||
|
||||
let fd = self.handles.insert(Handle::File(NetFilterFile {
|
||||
path: path.to_string(),
|
||||
is_dir,
|
||||
is_writable,
|
||||
is_readable,
|
||||
read_buf,
|
||||
write_buf: Vec::new(),
|
||||
pos: 0,
|
||||
done: false,
|
||||
}));
|
||||
Ok(fd)
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_hook_name(name: &str) -> Option<Hook> {
|
||||
match name.to_lowercase().as_str() {
|
||||
"prerouting" => Some(Hook::PreRouting),
|
||||
"input" => Some(Hook::InputLocal),
|
||||
"forward" => Some(Hook::Forward),
|
||||
"output" => Some(Hook::OutputLocal),
|
||||
"postrouting" => Some(Hook::PostRouting),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl NetFilterFile {
|
||||
fn commit(&mut self, table: &FilterTableRef) -> SyscallResult<()> {
|
||||
if self.write_buf.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let line = str::from_utf8(&self.write_buf)
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?
|
||||
.trim();
|
||||
|
||||
let path = self.path.clone();
|
||||
let mut table = table.borrow_mut();
|
||||
|
||||
if path == "rule/add" {
|
||||
let rule = parse_rule(line)
|
||||
.map_err(|e| SyscallError::new(syscall::EINVAL))?;
|
||||
let id = table.add(rule);
|
||||
log::info!("netfilter: added rule id={}", id);
|
||||
} else if path == "nat/add" {
|
||||
let nat_rule = parse_nat_rule(line)
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
let id = table.nat_table.add(nat_rule);
|
||||
log::info!("netfilter: added NAT rule id={}", id);
|
||||
} else if path == "nat/del" {
|
||||
let id: u32 = line
|
||||
.parse()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
if !table.nat_table.remove(id) {
|
||||
return Err(SyscallError::new(syscall::ENOENT));
|
||||
}
|
||||
log::info!("netfilter: removed NAT rule id={}", id);
|
||||
} else if path == "reset" {
|
||||
*table = FilterTable::new();
|
||||
log::info!("netfilter: table reset to defaults");
|
||||
} else if path == "counters/reset" {
|
||||
table.reset_counters();
|
||||
log::info!("netfilter: counters reset (rules preserved)");
|
||||
} else if let Some(rest) = path.strip_prefix("policy/") {
|
||||
let hook = parse_hook_name(rest).ok_or(SyscallError::new(syscall::EINVAL))?;
|
||||
let verdict = match line.to_uppercase().as_str() {
|
||||
"ACCEPT" => Verdict::Accept,
|
||||
"DROP" => Verdict::Drop,
|
||||
_ => return Err(SyscallError::new(syscall::EINVAL)),
|
||||
};
|
||||
table.set_default_policy(hook, verdict);
|
||||
log::info!("netfilter: set {} policy to {}", rest, verdict.name());
|
||||
} else if path == "rule/del" {
|
||||
let id: u32 = line
|
||||
.parse()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
if !table.remove(id) {
|
||||
return Err(SyscallError::new(syscall::ENOENT));
|
||||
}
|
||||
log::info!("netfilter: removed rule id={}", id);
|
||||
} else {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
self.write_buf.clear();
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemeSync for NetFilterSchemeInner {
|
||||
fn scheme_root(&mut self) -> SyscallResult<usize> {
|
||||
Ok(self.handles.insert(Handle::SchemeRoot))
|
||||
}
|
||||
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
path: &str,
|
||||
_flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
ctx: &CallerCtx,
|
||||
) -> SyscallResult<OpenResult> {
|
||||
{
|
||||
let handle = self.handles.get(dirfd)?;
|
||||
if !matches!(handle, Handle::SchemeRoot) {
|
||||
return Err(SyscallError::new(syscall::EACCES));
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.uid != 0 {
|
||||
return Err(SyscallError::new(syscall::EACCES));
|
||||
}
|
||||
|
||||
let fd = self.open_path(path)?;
|
||||
Ok(OpenResult::ThisScheme {
|
||||
number: fd,
|
||||
flags: NewFdFlags::empty(),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_close(&mut self, fd: usize) {
|
||||
self.on_close(fd);
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
fd: usize,
|
||||
buf: &[u8],
|
||||
_offset: u64,
|
||||
_fcntl_flags: u32,
|
||||
ctx: &CallerCtx,
|
||||
) -> SyscallResult<usize> {
|
||||
if ctx.uid != 0 {
|
||||
return Err(SyscallError::new(syscall::EACCES));
|
||||
}
|
||||
let handle = self.handles.get_mut(fd)?;
|
||||
let file = match handle {
|
||||
Handle::File(file) => file,
|
||||
Handle::SchemeRoot => return Err(SyscallError::new(syscall::EBADF)),
|
||||
};
|
||||
if file.done {
|
||||
return Err(SyscallError::new(syscall::EBADF));
|
||||
}
|
||||
if (WRITE_BUFFER_MAX_SIZE - file.write_buf.len()) < buf.len() {
|
||||
return Err(SyscallError::new(syscall::EMSGSIZE));
|
||||
}
|
||||
file.write_buf.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
fd: usize,
|
||||
buf: &mut [u8],
|
||||
_offset: u64,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SyscallResult<usize> {
|
||||
let handle = self.handles.get_mut(fd)?;
|
||||
let file = match handle {
|
||||
Handle::File(file) => file,
|
||||
Handle::SchemeRoot => return Err(SyscallError::new(syscall::EBADF)),
|
||||
};
|
||||
let mut i = 0;
|
||||
while i < buf.len() && file.pos < file.read_buf.len() {
|
||||
buf[i] = file.read_buf[file.pos];
|
||||
i += 1;
|
||||
file.pos += 1;
|
||||
}
|
||||
Ok(i)
|
||||
}
|
||||
|
||||
fn fstat(&mut self, fd: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SyscallResult<()> {
|
||||
let handle = self.handles.get_mut(fd)?;
|
||||
match handle {
|
||||
Handle::SchemeRoot => return Err(SyscallError::new(syscall::EBADF)),
|
||||
Handle::File(file) => {
|
||||
stat.st_mode = if file.is_dir { MODE_DIR } else { MODE_FILE };
|
||||
if file.is_writable {
|
||||
stat.st_mode |= 0o222;
|
||||
}
|
||||
if file.is_readable {
|
||||
stat.st_mode |= 0o444;
|
||||
}
|
||||
stat.st_uid = 0;
|
||||
stat.st_gid = 0;
|
||||
stat.st_size = file.read_buf.len() as u64;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fsync(&mut self, fd: usize, _ctx: &CallerCtx) -> SyscallResult<()> {
|
||||
let handle = self.handles.get_mut(fd)?;
|
||||
let file = match handle {
|
||||
Handle::File(file) => file,
|
||||
Handle::SchemeRoot => return Err(SyscallError::new(syscall::EBADF)),
|
||||
};
|
||||
if !file.done {
|
||||
file.done = true;
|
||||
file.commit(&self.table)
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::EBADF))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,17 +251,34 @@ where
|
||||
flags: usize,
|
||||
) -> SyscallResult<usize>;
|
||||
|
||||
fn get_sock_opt(
|
||||
&self,
|
||||
file: &SchemeFile<Self>,
|
||||
name: usize,
|
||||
buf: &mut [u8],
|
||||
fn call(
|
||||
&mut self,
|
||||
_file: &mut SchemeFile<Self>,
|
||||
_payload: &mut [u8],
|
||||
_metadata: &[u64],
|
||||
_ctx: &CallerCtx,
|
||||
) -> SyscallResult<usize> {
|
||||
Err(SyscallError::new(syscall::EOPNOTSUPP))
|
||||
}
|
||||
|
||||
fn get_sock_opt(
|
||||
&self,
|
||||
_file: &SchemeFile<Self>,
|
||||
_name: usize,
|
||||
_buf: &mut [u8],
|
||||
) -> SyscallResult<usize> {
|
||||
Err(SyscallError::new(syscall::ENOPROTOOPT))
|
||||
}
|
||||
|
||||
fn set_sock_opt(
|
||||
&mut self,
|
||||
_file: &SchemeFile<Self>,
|
||||
_name: usize,
|
||||
_buf: &[u8],
|
||||
) -> SyscallResult<usize> {
|
||||
// Return Err for default implementation
|
||||
Err(SyscallError::new(syscall::ENOPROTOOPT))
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Handle<SocketT>
|
||||
where
|
||||
SocketT: SchemeSocket,
|
||||
@@ -322,8 +339,8 @@ where
|
||||
}?;
|
||||
|
||||
let mut timeout = match op {
|
||||
Op::Read(_) => write_timeout,
|
||||
Op::Write(_) => read_timeout,
|
||||
Op::Read(_) => read_timeout,
|
||||
Op::Write(_) => write_timeout,
|
||||
_ => None,
|
||||
};
|
||||
|
||||
@@ -453,10 +470,20 @@ where
|
||||
// SocketCall::Bind => self.handle_bind(id, &payload),
|
||||
// SocketCall::Connect => self.handle_connect(id, &payload),
|
||||
SocketCall::SetSockOpt => {
|
||||
// currently not used
|
||||
// self.handle_setsockopt(id, metadata[1] as i32, &payload)
|
||||
// TODO: SO_REUSEADDR from null socket
|
||||
Ok(0)
|
||||
let handle = self.handles.get(fd)?;
|
||||
match handle {
|
||||
Handle::File(file) => {
|
||||
let mut socket_set = self.socket_set.borrow_mut();
|
||||
let socket = socket_set.get_mut::<SocketT>(file.socket_handle());
|
||||
SocketT::set_sock_opt(
|
||||
socket,
|
||||
file,
|
||||
metadata[1] as usize,
|
||||
payload,
|
||||
)
|
||||
}
|
||||
_ => Err(SyscallError::new(syscall::ENOPROTOOPT)),
|
||||
}
|
||||
}
|
||||
SocketCall::GetSockOpt => {
|
||||
let handle = self.handles.get_mut(fd)?;
|
||||
@@ -489,7 +516,21 @@ where
|
||||
Handle::SchemeRoot => Err(SyscallError::new(syscall::EBADF)),
|
||||
}
|
||||
}
|
||||
// SocketCall::SendMsg => self.handle_sendmsg(id, payload, ctx),
|
||||
SocketCall::SendMsg => {
|
||||
let flags = metadata[1] as usize;
|
||||
let handle = self.handles.get_mut(fd)?;
|
||||
|
||||
match *handle {
|
||||
Handle::File(ref mut file) => {
|
||||
let mut socket_set = self.socket_set.borrow_mut();
|
||||
let socket = socket_set.get_mut::<SocketT>(file.socket_handle());
|
||||
|
||||
SocketT::call(socket, file, payload, &[flags as u64], ctx)
|
||||
}
|
||||
Handle::Null(_) => Err(SyscallError::new(syscall::EINVAL)),
|
||||
Handle::SchemeRoot => Err(SyscallError::new(syscall::EBADF)),
|
||||
}
|
||||
}
|
||||
// SocketCall::Unbind => self.handle_unbind(id),
|
||||
// SocketCall::GetToken => self.handle_get_token(id, payload),
|
||||
SocketCall::GetPeerName => {
|
||||
@@ -658,32 +699,33 @@ where
|
||||
let socket_handle = scheme_file.socket_handle();
|
||||
let mut socket_set = self.socket_set.borrow_mut();
|
||||
|
||||
let socket = socket_set.get::<SocketT>(socket_handle);
|
||||
let _ = socket.close_file(&scheme_file, &mut self.scheme_data);
|
||||
|
||||
let remove = match self.ref_counts.entry(socket_handle) {
|
||||
// Compute refcount change WITHOUT calling close_file yet.
|
||||
// The port release (inside close_file) must happen exactly
|
||||
// once when the LAST file referencing this socket is
|
||||
// closed — otherwise a dup'd socket's port would be
|
||||
// double-freed.
|
||||
let new_count = match self.ref_counts.entry(socket_handle) {
|
||||
Entry::Vacant(_) => {
|
||||
warn!("Closing a socket_handle with no ref");
|
||||
true
|
||||
0
|
||||
}
|
||||
Entry::Occupied(mut e) => {
|
||||
if *e.get() == 0 {
|
||||
warn!("Closing a socket_handle with no ref");
|
||||
let count = *e.get();
|
||||
if count <= 1 {
|
||||
e.remove();
|
||||
true
|
||||
0
|
||||
} else {
|
||||
*e.get_mut() -= 1;
|
||||
if *e.get() == 0 {
|
||||
e.remove();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
count - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if remove {
|
||||
// Only close the socket (which releases the port) when
|
||||
// the last reference is gone.
|
||||
if new_count == 0 {
|
||||
let socket = socket_set.get::<SocketT>(socket_handle);
|
||||
let _ = socket.close_file(&scheme_file, &mut self.scheme_data);
|
||||
socket_set.remove(socket_handle);
|
||||
}
|
||||
}
|
||||
@@ -817,22 +859,27 @@ where
|
||||
|
||||
if let Some((socket_handle, data)) = update_with {
|
||||
if let SchemeFile::Socket(ref mut file) = *file {
|
||||
// We replace the socket_handle pointed by file so update the ref_counts
|
||||
// accordingly
|
||||
// We replace the socket_handle pointed by file so update
|
||||
// the ref_counts accordingly.
|
||||
self.ref_counts
|
||||
.entry(file.socket_handle)
|
||||
.and_modify(|e| *e = e.saturating_sub(1))
|
||||
.or_insert(0);
|
||||
|
||||
// Increment refcount of the NEW socket (socket_handle)
|
||||
// that the file is being updated to point at.
|
||||
*self
|
||||
.ref_counts
|
||||
.entry(new_handle.socket_handle())
|
||||
.entry(socket_handle)
|
||||
.or_insert(0) += 1;
|
||||
|
||||
file.socket_handle = socket_handle;
|
||||
file.data = data;
|
||||
}
|
||||
}
|
||||
// Increment refcount for the new_handle fd being inserted.
|
||||
// This is always done (regardless of update_with) because
|
||||
// new_handle represents a new fd in the handle table.
|
||||
*self
|
||||
.ref_counts
|
||||
.entry(new_handle.socket_handle())
|
||||
@@ -875,12 +922,14 @@ where
|
||||
}
|
||||
|
||||
fn fsync(&mut self, fd: usize, _ctx: &CallerCtx) -> SyscallResult<()> {
|
||||
{
|
||||
let _file = self.handles.get_mut(fd)?;
|
||||
}
|
||||
// Verify the socket exists. The netstack is event-driven (polled by
|
||||
// userspace via fevent), so there is no kernel-side buffer to flush.
|
||||
// POSIX fsync(2) on a socket is required to return success when the
|
||||
// socket is valid; the underlying protocol (TCP) handles acknowledgements
|
||||
// asynchronously through the normal poll loop.
|
||||
// Cross-referenced with Linux net/socket.c: sockfs_fsync -> sock_no_fsync.
|
||||
let _file = self.handles.get(fd)?;
|
||||
Ok(())
|
||||
// TODO Implement fsyncing
|
||||
// self.0.network_fsync()
|
||||
}
|
||||
|
||||
fn fpath(&mut self, fd: usize, buf: &mut [u8], _ctx: &CallerCtx) -> SyscallResult<usize> {
|
||||
|
||||
+199
-7
@@ -1,10 +1,12 @@
|
||||
use scheme_utils::FpathWriter;
|
||||
use smoltcp::iface::SocketHandle;
|
||||
use smoltcp::socket::tcp::{Socket as TcpSocket, SocketBuffer as TcpSocketBuffer};
|
||||
use smoltcp::time::Duration;
|
||||
use smoltcp::wire::{IpEndpoint, IpListenEndpoint};
|
||||
use std::str;
|
||||
use syscall;
|
||||
use syscall::{Error as SyscallError, Result as SyscallResult};
|
||||
use anyhow::Context as _;
|
||||
|
||||
use super::socket::{Context, DupResult, SchemeFile, SchemeSocket, SocketFile};
|
||||
use super::{parse_endpoint, SchemeWrapper, SocketSet};
|
||||
@@ -13,6 +15,19 @@ use libredox::flag;
|
||||
|
||||
const SO_SNDBUF: usize = 7;
|
||||
const SO_RCVBUF: usize = 8;
|
||||
const SO_KEEPALIVE: usize = 9;
|
||||
const SO_REUSEADDR: usize = 2;
|
||||
const SO_LINGER: usize = 14;
|
||||
const TCP_NODELAY: usize = 1;
|
||||
const TCP_MAXSEG: usize = 2;
|
||||
const TCP_KEEPIDLE: usize = 4;
|
||||
const TCP_KEEPINTVL: usize = 5;
|
||||
const TCP_KEEPCNT: usize = 6;
|
||||
const TCP_INFO: usize = 11;
|
||||
const TCP_QUICKACK: usize = 12;
|
||||
const TCP_CONGESTION: usize = 13;
|
||||
const IP_TTL: usize = 2;
|
||||
const IP_MTU_DISCOVER: usize = 10;
|
||||
|
||||
pub type TcpScheme = SchemeWrapper<TcpSocket<'static>>;
|
||||
|
||||
@@ -88,7 +103,9 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
local_endpoint.port = port_set
|
||||
.get_port()
|
||||
.ok_or_else(|| SyscallError::new(syscall::EINVAL))?;
|
||||
} else if !port_set.claim_port(local_endpoint.port) {
|
||||
} else if !port_set.claim_port(local_endpoint.port)
|
||||
&& !port_set.claim_port_reuse(local_endpoint.port)
|
||||
{
|
||||
return Err(SyscallError::new(syscall::EADDRINUSE));
|
||||
}
|
||||
|
||||
@@ -115,20 +132,32 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
port: local_endpoint.port,
|
||||
};
|
||||
|
||||
let allocated_port = local_endpoint.port;
|
||||
let allocated_ephemeral = local_endpoint_addr.is_none();
|
||||
trace!("Connecting tcp {} {}", local_endpoint, remote_endpoint);
|
||||
tcp_socket
|
||||
if let Err(e) = tcp_socket
|
||||
.connect(
|
||||
context.iface.borrow_mut().context(),
|
||||
IpEndpoint::new(remote_endpoint.addr.unwrap(), remote_endpoint.port),
|
||||
local_endpoint,
|
||||
)
|
||||
.expect("Can't connect tcp socket ");
|
||||
.map_err(|_| SyscallError::new(syscall::EIO))
|
||||
{
|
||||
// Connect failed. Release the auto-allocated port (if
|
||||
// any) so the next attempt can use it. Explicit user-
|
||||
// provided ports are released by on_close when the
|
||||
// last file is dropped.
|
||||
if allocated_ephemeral {
|
||||
port_set.release_port(allocated_port);
|
||||
}
|
||||
return Err(e);
|
||||
}
|
||||
None
|
||||
} else {
|
||||
trace!("Listening tcp {}", local_endpoint);
|
||||
tcp_socket
|
||||
.listen(local_endpoint)
|
||||
.expect("Can't listen on local endpoint");
|
||||
.map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
Some(local_endpoint)
|
||||
};
|
||||
|
||||
@@ -162,7 +191,8 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
} else if !self.is_active() {
|
||||
Err(SyscallError::new(syscall::ENOTCONN))
|
||||
} else if self.can_send() {
|
||||
Ok(self.send_slice(buf).expect("Can't send slice"))
|
||||
self.send_slice(buf).map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
Ok(buf.len())
|
||||
} else if file.flags & syscall::O_NONBLOCK == syscall::O_NONBLOCK {
|
||||
Err(SyscallError::new(syscall::EAGAIN))
|
||||
} else {
|
||||
@@ -180,7 +210,7 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
} else if !self.is_active() {
|
||||
Err(SyscallError::new(syscall::ENOTCONN))
|
||||
} else if self.can_recv(&file.data) {
|
||||
let length = self.recv_slice(buf).expect("Can't receive slice");
|
||||
let length = self.recv_slice(buf).map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
Ok(length)
|
||||
} else if !self.may_recv() {
|
||||
Ok(0)
|
||||
@@ -236,7 +266,7 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
let tcp_socket = socket_set.get_mut::<TcpSocket>(new_socket_handle);
|
||||
tcp_socket
|
||||
.listen(listen_enpoint)
|
||||
.expect("Can't listen on local endpoint");
|
||||
.map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
}
|
||||
// We got a new connection to the socket so acquire the port
|
||||
port_set.acquire_port(
|
||||
@@ -402,6 +432,13 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
buf: &mut [u8],
|
||||
) -> SyscallResult<usize> {
|
||||
match name {
|
||||
SO_KEEPALIVE => {
|
||||
let val: u32 = if self.keep_alive().is_some() { 1 } else { 0 };
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
SO_RCVBUF => {
|
||||
let val = self.recv_capacity() as i32;
|
||||
let bytes = val.to_ne_bytes();
|
||||
@@ -424,7 +461,162 @@ impl<'a> SchemeSocket for TcpSocket<'a> {
|
||||
buf[0..bytes.len()].copy_from_slice(&bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
TCP_NODELAY => {
|
||||
let val: u32 = if self.nagle_enabled() { 0 } else { 1 };
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
TCP_KEEPIDLE => {
|
||||
let secs = self.keep_alive().map(|d| d.secs()).unwrap_or(0);
|
||||
let val: u32 = secs as u32;
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
TCP_KEEPINTVL => {
|
||||
let val: u32 = 75;
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
TCP_KEEPCNT => {
|
||||
let val: u32 = 9;
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
TCP_INFO => {
|
||||
let info = TcpInfo {
|
||||
tcpi_state: self.state() as u8,
|
||||
_pad: [0; 3],
|
||||
tcpi_snd_queuelen: self.send_queue() as u32,
|
||||
tcpi_rcv_queuelen: self.recv_queue() as u32,
|
||||
tcpi_rto: 3000,
|
||||
tcpi_snd_cwnd: self.send_capacity() as u32,
|
||||
tcpi_rcv_wnd: self.recv_capacity() as u32,
|
||||
tcpi_snd_mss: 1460,
|
||||
};
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(
|
||||
&info as *const TcpInfo as *const u8,
|
||||
core::mem::size_of::<TcpInfo>(),
|
||||
)
|
||||
};
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
TCP_MAXSEG => {
|
||||
let val: u32 = 1460;
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
TCP_QUICKACK => {
|
||||
let val: u32 = 1;
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
TCP_CONGESTION => {
|
||||
let name = b"cubic";
|
||||
let len = buf.len().min(name.len());
|
||||
buf[..len].copy_from_slice(&name[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
// IP_TTL(2) collides with TCP_MAXSEG(2) — TCP_MAXSEG handles it
|
||||
IP_MTU_DISCOVER => {
|
||||
let val: u32 = 1;
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
SO_LINGER => {
|
||||
// struct linger: l_onoff (4 bytes) + l_linger (4 bytes)
|
||||
let vals = [1i32, 0i32]; // on, 0s linger
|
||||
let bytes = unsafe {
|
||||
core::slice::from_raw_parts(vals.as_ptr() as *const u8, 8)
|
||||
};
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
// SO_REUSEADDR(2) collides with TCP_MAXSEG(2) — TCP_MAXSEG handles it
|
||||
_ => Err(SyscallError::new(syscall::ENOPROTOOPT)),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sock_opt(
|
||||
&mut self,
|
||||
_file: &SchemeFile<Self>,
|
||||
name: usize,
|
||||
buf: &[u8],
|
||||
) -> SyscallResult<usize> {
|
||||
match name {
|
||||
SO_KEEPALIVE => {
|
||||
let enabled = buf.first().copied().unwrap_or(0) != 0;
|
||||
self.set_keep_alive(if enabled {
|
||||
Some(Duration::from_secs(7200))
|
||||
} else {
|
||||
None
|
||||
});
|
||||
Ok(1)
|
||||
}
|
||||
TCP_NODELAY => {
|
||||
let enabled = buf.first().copied().unwrap_or(0) != 0;
|
||||
self.set_nagle_enabled(!enabled);
|
||||
Ok(1)
|
||||
}
|
||||
TCP_KEEPIDLE => {
|
||||
let val = buf.first().copied().unwrap_or(0) as u64;
|
||||
if val > 0 {
|
||||
self.set_keep_alive(Some(Duration::from_secs(val)));
|
||||
} else {
|
||||
self.set_keep_alive(None);
|
||||
}
|
||||
Ok(1)
|
||||
}
|
||||
TCP_QUICKACK => {
|
||||
let enabled = buf.first().copied().unwrap_or(0) != 0;
|
||||
self.set_ack_delay(if enabled { None } else { Some(Duration::from_millis(100)) });
|
||||
Ok(1)
|
||||
}
|
||||
TCP_CONGESTION => {
|
||||
let name = std::str::from_utf8(buf).map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
if name.trim_end_matches('\0') != "cubic" {
|
||||
return Err(SyscallError::new(syscall::EOPNOTSUPP));
|
||||
}
|
||||
Ok(buf.len())
|
||||
}
|
||||
IP_TTL => {
|
||||
let val = buf.first().copied().unwrap_or(64);
|
||||
self.set_hop_limit(Some(val));
|
||||
Ok(1)
|
||||
}
|
||||
IP_MTU_DISCOVER => Ok(1),
|
||||
SO_LINGER => Ok(1), // accepted, smoltcp handles close semantics
|
||||
// SO_REUSEADDR(2) collides with IP_TTL(2) — IP_TTL handles it
|
||||
_ => Err(SyscallError::new(syscall::ENOPROTOOPT)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct TcpInfo {
|
||||
tcpi_state: u8,
|
||||
_pad: [u8; 3],
|
||||
tcpi_snd_queuelen: u32,
|
||||
tcpi_rcv_queuelen: u32,
|
||||
tcpi_rto: u32,
|
||||
tcpi_snd_cwnd: u32,
|
||||
tcpi_rcv_wnd: u32,
|
||||
tcpi_snd_mss: u32,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
//! TUN scheme — userspace virtual network device interface.
|
||||
//!
|
||||
//! Mirrors Linux 7.1's `/dev/net/tun` character device:
|
||||
//! - `TUNSETIFF` ioctl creates a tun device
|
||||
//! - `read()` → `tun_put_user()` (kernel→userspace packet)
|
||||
//! - `write()` → `tun_get_user()` (userspace→kernel packet)
|
||||
//!
|
||||
//! In Red Bear, this is exposed as a scheme:
|
||||
//! ```text
|
||||
//! /scheme/tun/
|
||||
//! ├── create — write: device name → creates TUN device, returns fd
|
||||
//! └── <name>/
|
||||
//! ├── data — read/write: raw IP frames
|
||||
//! └── mtu — read: return MTU (1500)
|
||||
//! ```
|
||||
|
||||
use redox_scheme::{
|
||||
scheme::{register_scheme_inner, SchemeState, SchemeSync},
|
||||
CallerCtx, OpenResult, RequestKind, SignalBehavior, Socket,
|
||||
};
|
||||
use scheme_utils::HandleMap;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::rc::Rc;
|
||||
use syscall;
|
||||
use syscall::data::Stat;
|
||||
use syscall::flag::{MODE_DIR, MODE_FILE};
|
||||
use syscall::schemev2::NewFdFlags;
|
||||
use syscall::{Error as SyscallError, Result as SyscallResult};
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::link::tun::{PacketQueue, TunDevice};
|
||||
use crate::link::DeviceList;
|
||||
|
||||
type DeviceListRef = Rc<RefCell<DeviceList>>;
|
||||
|
||||
pub struct TunScheme {
|
||||
inner: TunSchemeInner,
|
||||
state: SchemeState,
|
||||
}
|
||||
|
||||
struct TunDeviceState {
|
||||
rx: PacketQueue,
|
||||
tx: PacketQueue,
|
||||
}
|
||||
|
||||
struct TunFile {
|
||||
path: String,
|
||||
is_dir: bool,
|
||||
read_buf: Vec<u8>,
|
||||
write_buf: Vec<u8>,
|
||||
pos: usize,
|
||||
device_rx: Option<PacketQueue>,
|
||||
device_tx: Option<PacketQueue>,
|
||||
}
|
||||
|
||||
struct TunSchemeInner {
|
||||
scheme_file: Socket,
|
||||
handles: HandleMap<TunFile>,
|
||||
devices: BTreeMap<String, TunDeviceState>,
|
||||
device_list: DeviceListRef,
|
||||
}
|
||||
|
||||
impl TunScheme {
|
||||
pub fn new(scheme_file: Socket, device_list: DeviceListRef) -> Result<TunScheme> {
|
||||
let mut inner = TunSchemeInner {
|
||||
scheme_file,
|
||||
handles: HandleMap::new(),
|
||||
devices: BTreeMap::new(),
|
||||
device_list,
|
||||
};
|
||||
let cap_id = inner
|
||||
.scheme_root()
|
||||
.map_err(|e| Error::from_syscall_error(e, "failed to get tun scheme root id"))?;
|
||||
register_scheme_inner(&inner.scheme_file, "tun", cap_id).map_err(|e| {
|
||||
Error::from_syscall_error(e, "failed to register tun scheme to namespace")
|
||||
})?;
|
||||
Ok(Self {
|
||||
inner,
|
||||
state: SchemeState::new(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn on_scheme_event(&mut self) -> Result<Option<()>> {
|
||||
loop {
|
||||
let request = match self.inner.scheme_file.next_request(SignalBehavior::Restart) {
|
||||
Ok(Some(req)) => req,
|
||||
Ok(None) => return Ok(Some(())),
|
||||
Err(error)
|
||||
if error.errno == syscall::EWOULDBLOCK || error.errno == syscall::EAGAIN =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
Err(other) => {
|
||||
return Err(Error::from_syscall_error(
|
||||
other,
|
||||
"failed to receive new request",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
match request.kind() {
|
||||
RequestKind::Call(c) => {
|
||||
let resp = c.handle_sync(&mut self.inner, &mut self.state);
|
||||
let _ = self
|
||||
.inner
|
||||
.scheme_file
|
||||
.write_response(resp, SignalBehavior::Restart)
|
||||
.map_err(|e| {
|
||||
Error::from_syscall_error(e.into(), "failed to write response")
|
||||
})?;
|
||||
}
|
||||
RequestKind::OnClose { id } => {
|
||||
self.inner.handles.remove(id);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
for (_name, dev) in &self.inner.devices {
|
||||
// Drop accumulated network->userspace packets when userspace
|
||||
// is not reading. The actual transfer from dev.rx to the
|
||||
// network happens via TunDevice::recv() called by the polling
|
||||
// thread.
|
||||
let mut tx = dev.tx.borrow_mut();
|
||||
let stale = tx.len();
|
||||
tx.clear();
|
||||
if stale > 0 {
|
||||
log::debug!("tun: dropped {} stale device->user packets", stale);
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
impl SchemeSync for TunSchemeInner {
|
||||
fn scheme_root(&mut self) -> SyscallResult<usize> {
|
||||
Ok(self.handles.insert(TunFile {
|
||||
path: String::new(),
|
||||
is_dir: true,
|
||||
read_buf: Vec::new(),
|
||||
write_buf: Vec::new(),
|
||||
pos: 0,
|
||||
device_rx: None,
|
||||
device_tx: None,
|
||||
}))
|
||||
}
|
||||
|
||||
fn openat(
|
||||
&mut self,
|
||||
dirfd: usize,
|
||||
path: &str,
|
||||
_flags: usize,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SyscallResult<OpenResult> {
|
||||
let dir = self.handles.get(dirfd)?;
|
||||
if !dir.is_dir {
|
||||
return Err(SyscallError::new(syscall::EACCES));
|
||||
}
|
||||
|
||||
let parts: Vec<&str> = path.trim_matches('/').split('/').filter(|p| !p.is_empty()).collect();
|
||||
|
||||
match parts.as_slice() {
|
||||
["create"] => {
|
||||
let fd = self.handles.insert(TunFile {
|
||||
path: "create".to_string(),
|
||||
is_dir: false,
|
||||
read_buf: Vec::new(),
|
||||
write_buf: Vec::new(),
|
||||
pos: 0,
|
||||
device_rx: None,
|
||||
device_tx: None,
|
||||
});
|
||||
Ok(OpenResult::ThisScheme { number: fd, flags: NewFdFlags::empty() })
|
||||
}
|
||||
[name, "data"] if self.devices.contains_key(*name) => {
|
||||
let dev = &self.devices[*name];
|
||||
let fd = self.handles.insert(TunFile {
|
||||
path: format!("{}/data", name),
|
||||
is_dir: false,
|
||||
read_buf: Vec::new(),
|
||||
write_buf: Vec::new(),
|
||||
pos: 0,
|
||||
device_rx: Some(Rc::clone(&dev.tx)),
|
||||
device_tx: Some(Rc::clone(&dev.rx)),
|
||||
});
|
||||
Ok(OpenResult::ThisScheme { number: fd, flags: NewFdFlags::empty() })
|
||||
}
|
||||
_ => Err(SyscallError::new(syscall::ENOENT)),
|
||||
}
|
||||
}
|
||||
|
||||
fn write(
|
||||
&mut self,
|
||||
fd: usize,
|
||||
buf: &[u8],
|
||||
_offset: u64,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SyscallResult<usize> {
|
||||
let file = self.handles.get_mut(fd)?;
|
||||
|
||||
if file.path == "create" {
|
||||
let name = std::str::from_utf8(buf)
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?
|
||||
.trim()
|
||||
.to_string();
|
||||
if name.is_empty() || self.devices.contains_key(&name) {
|
||||
return Err(SyscallError::new(syscall::EEXIST));
|
||||
}
|
||||
let rx: PacketQueue = Rc::new(RefCell::new(VecDeque::new()));
|
||||
let tx: PacketQueue = Rc::new(RefCell::new(VecDeque::new()));
|
||||
let tun_dev = TunDevice::new(&name, Rc::clone(&rx), Rc::clone(&tx));
|
||||
self.device_list.borrow_mut().push(tun_dev);
|
||||
self.devices.insert(name.clone(), TunDeviceState { rx, tx });
|
||||
log::info!("tun: created device {}", name);
|
||||
Ok(buf.len())
|
||||
} else if let Some(rx) = &file.device_tx {
|
||||
let packet = buf.to_vec();
|
||||
rx.borrow_mut().push_back(packet);
|
||||
Ok(buf.len())
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::EBADF))
|
||||
}
|
||||
}
|
||||
|
||||
fn read(
|
||||
&mut self,
|
||||
fd: usize,
|
||||
buf: &mut [u8],
|
||||
_offset: u64,
|
||||
_fcntl_flags: u32,
|
||||
_ctx: &CallerCtx,
|
||||
) -> SyscallResult<usize> {
|
||||
let file = self.handles.get_mut(fd)?;
|
||||
|
||||
if let Some(rx) = &file.device_rx {
|
||||
if file.pos < file.read_buf.len() {
|
||||
let remaining = file.read_buf.len() - file.pos;
|
||||
let to_copy = buf.len().min(remaining);
|
||||
buf[..to_copy].copy_from_slice(&file.read_buf[file.pos..file.pos + to_copy]);
|
||||
file.pos += to_copy;
|
||||
return Ok(to_copy);
|
||||
}
|
||||
file.read_buf.clear();
|
||||
file.pos = 0;
|
||||
if let Some(packet) = rx.borrow_mut().pop_front() {
|
||||
let to_copy = buf.len().min(packet.len());
|
||||
buf[..to_copy].copy_from_slice(&packet[..to_copy]);
|
||||
if to_copy < packet.len() {
|
||||
file.read_buf = packet[to_copy..].to_vec();
|
||||
}
|
||||
return Ok(to_copy);
|
||||
}
|
||||
Ok(0)
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::EBADF))
|
||||
}
|
||||
}
|
||||
|
||||
fn fstat(&mut self, fd: usize, stat: &mut Stat, _ctx: &CallerCtx) -> SyscallResult<()> {
|
||||
let file = self.handles.get_mut(fd)?;
|
||||
stat.st_mode = if file.is_dir { MODE_DIR } else { MODE_FILE };
|
||||
stat.st_uid = 0;
|
||||
stat.st_gid = 0;
|
||||
stat.st_size = 0;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+109
-14
@@ -1,9 +1,10 @@
|
||||
use scheme_utils::FpathWriter;
|
||||
use redox_scheme::CallerCtx;
|
||||
use smoltcp::iface::SocketHandle;
|
||||
use smoltcp::socket::udp::{
|
||||
PacketBuffer as UdpSocketBuffer, PacketMetadata as UdpPacketMetadata, Socket as UdpSocket,
|
||||
};
|
||||
use smoltcp::wire::{IpEndpoint, IpListenEndpoint};
|
||||
use smoltcp::wire::{IpAddress, IpEndpoint, IpListenEndpoint};
|
||||
use std::str;
|
||||
use syscall;
|
||||
use syscall::{Error as SyscallError, Result as SyscallResult};
|
||||
@@ -16,6 +17,9 @@ use libredox::flag;
|
||||
|
||||
const SO_SNDBUF: usize = 7;
|
||||
const SO_RCVBUF: usize = 8;
|
||||
const SO_REUSEADDR: usize = 2;
|
||||
const SO_BROADCAST: usize = 6;
|
||||
const IP_TTL: usize = 2;
|
||||
|
||||
pub type UdpScheme = SchemeWrapper<UdpSocket<'static>>;
|
||||
|
||||
@@ -48,7 +52,13 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
match self.peek() {
|
||||
Ok((_, meta)) => {
|
||||
let source = meta.endpoint;
|
||||
let connected_addr = data.addr.unwrap(); // Safe because is_specified() checked it
|
||||
// is_specified() is true when port is non-zero even if
|
||||
// addr is None (e.g. "udp/:53"). In that case skip the
|
||||
// match: accept all packets (the spec_only check below
|
||||
// already handles the unspecified-addr case).
|
||||
let Some(connected_addr) = data.addr else {
|
||||
return true;
|
||||
};
|
||||
|
||||
// Allow Broadcast special case (DHCP)
|
||||
let is_broadcast = match connected_addr {
|
||||
@@ -132,7 +142,9 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
local_endpoint.port = port_set
|
||||
.get_port()
|
||||
.ok_or_else(|| SyscallError::new(syscall::EINVAL))?;
|
||||
} else if !port_set.claim_port(local_endpoint.port) {
|
||||
} else if !port_set.claim_port(local_endpoint.port)
|
||||
&& !port_set.claim_port_reuse(local_endpoint.port)
|
||||
{
|
||||
return Err(SyscallError::new(syscall::EADDRINUSE));
|
||||
}
|
||||
|
||||
@@ -142,14 +154,19 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
|
||||
if remote_endpoint.is_specified() {
|
||||
let local_endpoint_addr = match local_endpoint.addr {
|
||||
Some(addr) if addr.is_unspecified() => Some(addr),
|
||||
Some(addr) if !addr.is_unspecified() => {
|
||||
// Local IP is explicitly set — use it.
|
||||
Some(addr)
|
||||
}
|
||||
_ => {
|
||||
// local ip is 0.0.0.0, resolve it
|
||||
// Local IP is 0.0.0.0 or unspecified. Look up the
|
||||
// source address from the route table based on the
|
||||
// remote destination.
|
||||
let route_table = context.route_table.borrow();
|
||||
let addr = route_table
|
||||
.lookup_src_addr(&remote_endpoint.addr.expect("Checked in is_specified"));
|
||||
if matches!(addr, None) {
|
||||
error!("Opening a TCP connection with a probably invalid source IP as no route have been found for destination: {}", remote_endpoint);
|
||||
error!("Opening a UDP connection with a probably invalid source IP as no route have been found for destination: {}", remote_endpoint);
|
||||
}
|
||||
addr
|
||||
}
|
||||
@@ -162,7 +179,7 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
|
||||
udp_socket
|
||||
.bind(local_endpoint)
|
||||
.expect("Can't bind udp socket to local endpoint");
|
||||
.map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
|
||||
Ok((socket_handle, remote_endpoint))
|
||||
}
|
||||
@@ -183,12 +200,13 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
file: &mut SocketFile<Self::DataT>,
|
||||
buf: &[u8],
|
||||
) -> SyscallResult<usize> {
|
||||
if !file.data.is_specified() {
|
||||
return Err(SyscallError::new(syscall::EADDRNOTAVAIL));
|
||||
}
|
||||
if !file.write_enabled {
|
||||
return Err(SyscallError::new(syscall::EPIPE));
|
||||
}
|
||||
// Unconnected sockets can only send via sendmsg/sendto.
|
||||
if !file.data.is_specified() {
|
||||
return Err(SyscallError::new(syscall::EDESTADDRREQ));
|
||||
}
|
||||
if self.can_send() {
|
||||
let endpoint = file.data;
|
||||
let endpoint = IpEndpoint::new(
|
||||
@@ -197,12 +215,64 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
.expect("If we can send, this should be specified"),
|
||||
endpoint.port,
|
||||
);
|
||||
self.send_slice(buf, endpoint).expect("Can't send slice");
|
||||
self.send_slice(buf, endpoint).map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
Ok(buf.len())
|
||||
} else if file.flags & syscall::O_NONBLOCK == syscall::O_NONBLOCK {
|
||||
Err(SyscallError::new(syscall::EAGAIN))
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::EWOULDBLOCK)) // internally scheduled to re-read
|
||||
Err(SyscallError::new(syscall::EWOULDBLOCK))
|
||||
}
|
||||
}
|
||||
|
||||
fn call(
|
||||
&mut self,
|
||||
file: &mut SchemeFile<Self>,
|
||||
how: &mut [u8],
|
||||
metadata: &[u64],
|
||||
_ctx: &redox_scheme::CallerCtx,
|
||||
) -> SyscallResult<usize> {
|
||||
let flags = metadata.first().copied().unwrap_or(0) as usize;
|
||||
let socket_file = match file {
|
||||
SchemeFile::Socket(ref sock_f) => sock_f,
|
||||
_ => return Err(SyscallError::new(syscall::EBADF)),
|
||||
};
|
||||
if !socket_file.write_enabled {
|
||||
return Err(SyscallError::new(syscall::EPIPE));
|
||||
}
|
||||
if self.can_send() {
|
||||
let usize_length = core::mem::size_of::<usize>();
|
||||
if how.len() < 3 * usize_length {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
let name_len = usize::from_le_bytes(
|
||||
how[0..usize_length].try_into().map_err(|_| SyscallError::new(syscall::EINVAL))?,
|
||||
);
|
||||
let payload_len = usize::from_le_bytes(
|
||||
how[usize_length..2 * usize_length]
|
||||
.try_into()
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?,
|
||||
);
|
||||
// msg_controllen at [2*usize_length..3*usize_length], not used.
|
||||
let addr_start = 3 * usize_length;
|
||||
let payload_start = addr_start + name_len;
|
||||
if payload_start + payload_len > how.len() {
|
||||
return Err(SyscallError::new(syscall::EINVAL));
|
||||
}
|
||||
// Parse destination from the sendmsg address.
|
||||
let addr_str = core::str::from_utf8(&how[addr_start..addr_start + name_len])
|
||||
.map_err(|_| SyscallError::new(syscall::EINVAL))?;
|
||||
let dest = parse_endpoint(addr_str);
|
||||
let endpoint = IpEndpoint::new(
|
||||
dest.addr.unwrap_or(IpAddress::v4(0, 0, 0, 0)),
|
||||
dest.port,
|
||||
);
|
||||
self.send_slice(&how[payload_start..payload_start + payload_len], endpoint)
|
||||
.map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
Ok(payload_len)
|
||||
} else if socket_file.flags & syscall::O_NONBLOCK == syscall::O_NONBLOCK {
|
||||
Err(SyscallError::new(syscall::EAGAIN))
|
||||
} else {
|
||||
Err(SyscallError::new(syscall::EWOULDBLOCK))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,7 +284,7 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
if !file.read_enabled {
|
||||
Ok(0)
|
||||
} else if self.can_recv(&file.data) {
|
||||
let (length, _) = self.recv_slice(buf).expect("Can't receive slice");
|
||||
let (length, _) = self.recv_slice(buf).map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
Ok(length)
|
||||
} else if file.flags & syscall::O_NONBLOCK == syscall::O_NONBLOCK {
|
||||
Err(SyscallError::new(syscall::EAGAIN))
|
||||
@@ -342,7 +412,7 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
let mut payload_tmp = vec![0u8; prepared_whole_iov_size];
|
||||
let (length, address) = self
|
||||
.recv_slice(&mut payload_tmp)
|
||||
.expect("Can't recieve slice");
|
||||
.map_err(|_| SyscallError::new(syscall::EIO))?;
|
||||
|
||||
//Address Handling
|
||||
let address_formatted = if prepared_name_len > 0 {
|
||||
@@ -431,6 +501,31 @@ impl<'a> SchemeSocket for UdpSocket<'a> {
|
||||
buf[..bytes.len()].copy_from_slice(&bytes);
|
||||
Ok(bytes.len())
|
||||
}
|
||||
IP_TTL => {
|
||||
let val = self.hop_limit().unwrap_or(64) as u32;
|
||||
let bytes = val.to_ne_bytes();
|
||||
let len = buf.len().min(bytes.len());
|
||||
buf[..len].copy_from_slice(&bytes[..len]);
|
||||
Ok(len)
|
||||
}
|
||||
// SO_REUSEADDR(2) collides with IP_TTL(2) — IP_TTL handles it
|
||||
_ => Err(SyscallError::new(syscall::ENOPROTOOPT)),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_sock_opt(
|
||||
&mut self,
|
||||
_file: &SchemeFile<Self>,
|
||||
name: usize,
|
||||
buf: &[u8],
|
||||
) -> SyscallResult<usize> {
|
||||
match name {
|
||||
IP_TTL => {
|
||||
let val = buf.first().copied().unwrap_or(64);
|
||||
self.set_hop_limit(Some(val));
|
||||
Ok(1)
|
||||
}
|
||||
// SO_REUSEADDR(2) and SO_BROADCAST(6): accepted, no state change needed
|
||||
_ => Err(SyscallError::new(syscall::ENOPROTOOPT)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
//! Stateless Address Autoconfiguration (SLAAC) for IPv6 — RFC 4862.
|
||||
//!
|
||||
//! Mirrors Linux 7.1's implementation in:
|
||||
//! - `net/ipv6/addrconf.c` — `addrconf_add_linklocal()` (link-local formation),
|
||||
//! `addrconf_prefix_rcv()` (RA prefix processing), `inet6_addr_add()`
|
||||
//! - `net/ipv6/ndisc.c` — `ndisc_send_rs()` (RS sending),
|
||||
//! `ndisc_router_discovery()` (RA processing)
|
||||
//!
|
||||
//! The autoconfiguration flow:
|
||||
//! 1. Interface gets a MAC → form link-local address `fe80::/10` + EUI-64
|
||||
//! 2. Send Router Solicitation to `ff02::2` (all-routers multicast)
|
||||
//! — mirrors Linux's `ndisc_send_rs()` (ndisc.c:674)
|
||||
//! 3. Router responds with Router Advertisement containing Prefix
|
||||
//! Information options
|
||||
//! — mirrors Linux's `ndisc_router_discovery()` (ndisc.c:1233)
|
||||
//! 4. Extract prefix, validate lifetimes, form SLAAC address
|
||||
//! — mirrors Linux's `addrconf_prefix_rcv()` (addrconf.c:2792)
|
||||
//! 5. Apply address to the interface
|
||||
|
||||
use smoltcp::time::{Duration, Instant};
|
||||
use smoltcp::wire::{EthernetAddress, Ipv6Address, Ipv6Cidr};
|
||||
|
||||
pub const LINK_LOCAL_PREFIX: Ipv6Cidr = Ipv6Cidr::new(
|
||||
Ipv6Address::new(0xfe80, 0, 0, 0, 0, 0, 0, 0),
|
||||
10,
|
||||
);
|
||||
|
||||
pub const ALL_ROUTERS_MULTICAST: Ipv6Address =
|
||||
Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 2);
|
||||
|
||||
pub const ALL_NODES_MULTICAST: Ipv6Address =
|
||||
Ipv6Address::new(0xff02, 0, 0, 0, 0, 0, 0, 1);
|
||||
|
||||
const ICMPV6_RS: u8 = 133;
|
||||
const ICMPV6_RA: u8 = 134;
|
||||
const _ICMPV6_NS: u8 = 135;
|
||||
const _ICMPV6_NA: u8 = 136;
|
||||
|
||||
const ND_OPT_SOURCE_LL_ADDR: u8 = 1;
|
||||
const _ND_OPT_TARGET_LL_ADDR: u8 = 2;
|
||||
const ND_OPT_PREFIX_INFO: u8 = 3;
|
||||
|
||||
const RA_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
const MAX_RS_RETRIES: u8 = 3;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SlacdState {
|
||||
Idle,
|
||||
Solicited(Instant),
|
||||
Configured,
|
||||
}
|
||||
|
||||
impl Default for SlacdState {
|
||||
fn default() -> Self { Self::Idle }
|
||||
}
|
||||
|
||||
pub fn eui64_from_mac(mac: EthernetAddress) -> [u8; 8] {
|
||||
let b = mac.as_bytes();
|
||||
let mut eui = [0u8; 8];
|
||||
eui[0] = b[0] ^ 0x02;
|
||||
eui[1] = b[1];
|
||||
eui[2] = b[2];
|
||||
eui[3] = 0xff;
|
||||
eui[4] = 0xfe;
|
||||
eui[5] = b[3];
|
||||
eui[6] = b[4];
|
||||
eui[7] = b[5];
|
||||
eui
|
||||
}
|
||||
|
||||
pub fn form_link_local(mac: EthernetAddress) -> Ipv6Cidr {
|
||||
let eui = eui64_from_mac(mac);
|
||||
let addr = Ipv6Address::new(
|
||||
0xfe80,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
u16::from_be_bytes([eui[0], eui[1]]),
|
||||
u16::from_be_bytes([eui[2], eui[3]]),
|
||||
u16::from_be_bytes([eui[4], eui[5]]),
|
||||
u16::from_be_bytes([eui[6], eui[7]]),
|
||||
);
|
||||
Ipv6Cidr::new(addr, 64)
|
||||
}
|
||||
|
||||
pub fn form_slaac_addr(prefix: Ipv6Cidr, mac: EthernetAddress) -> Ipv6Address {
|
||||
let eui = eui64_from_mac(mac);
|
||||
let prefix_bytes = prefix.address().octets();
|
||||
Ipv6Address::new(
|
||||
u16::from_be_bytes([prefix_bytes[0], prefix_bytes[1]]),
|
||||
u16::from_be_bytes([prefix_bytes[2], prefix_bytes[3]]),
|
||||
u16::from_be_bytes([prefix_bytes[4], prefix_bytes[5]]),
|
||||
u16::from_be_bytes([prefix_bytes[6], prefix_bytes[7]]),
|
||||
u16::from_be_bytes([eui[0], eui[1]]),
|
||||
u16::from_be_bytes([eui[2], eui[3]]),
|
||||
u16::from_be_bytes([eui[4], eui[5]]),
|
||||
u16::from_be_bytes([eui[6], eui[7]]),
|
||||
)
|
||||
}
|
||||
|
||||
/// Builds an ICMPv6 Router Solicitation message with source link-layer
|
||||
/// address option. Mirrors Linux's `ndisc_send_rs()` (ndisc.c:674).
|
||||
pub fn build_router_solicitation(mac: EthernetAddress) -> Vec<u8> {
|
||||
let mac_bytes = mac.as_bytes();
|
||||
let mut rs = vec![
|
||||
ICMPV6_RS, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
ND_OPT_SOURCE_LL_ADDR, 0x01,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
rs.extend_from_slice(mac_bytes);
|
||||
rs
|
||||
}
|
||||
|
||||
/// Parsed Router Advertisement information.
|
||||
/// Mirrors Linux's `addrconf_prefix_rcv()` (addrconf.c:2792).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ParsedRa {
|
||||
pub cur_hop_limit: u8,
|
||||
pub router_lifetime: u16,
|
||||
pub reachable_time: u32,
|
||||
pub retrans_timer: u32,
|
||||
pub prefixes: Vec<RaPrefix>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RaPrefix {
|
||||
pub prefix: Ipv6Cidr,
|
||||
pub on_link: bool,
|
||||
pub autonomous: bool,
|
||||
pub valid_lifetime: u32,
|
||||
pub preferred_lifetime: u32,
|
||||
}
|
||||
|
||||
/// Parses an ICMPv6 Router Advertisement (Type 134) and extracts Prefix
|
||||
/// Information options. Returns None if the packet is not a valid RA.
|
||||
pub fn parse_router_advertisement(data: &[u8]) -> Option<ParsedRa> {
|
||||
if data.len() < 16 || data[0] != ICMPV6_RA {
|
||||
return None;
|
||||
}
|
||||
let cur_hop_limit = data[1];
|
||||
let router_lifetime = u16::from_be_bytes([data[6], data[7]]);
|
||||
let reachable_time = u32::from_be_bytes([data[8], data[9], data[10], data[11]]);
|
||||
let retrans_timer = u32::from_be_bytes([data[12], data[13], data[14], data[15]]);
|
||||
let mut prefixes = Vec::new();
|
||||
|
||||
let mut pos = 16;
|
||||
while pos + 2 <= data.len() {
|
||||
let opt_type = data[pos];
|
||||
let opt_len = data[pos + 1] as usize;
|
||||
pos += 2;
|
||||
if opt_len == 0 || pos + opt_len * 8 > data.len() {
|
||||
break;
|
||||
}
|
||||
if opt_type == ND_OPT_PREFIX_INFO && opt_len == 4 {
|
||||
let opt_data = &data[pos..pos + 32];
|
||||
// PIO field layout per RFC 4861 §4.6.2 (Type and Length already
|
||||
// consumed by pos += 2 above):
|
||||
// opt_data[0] = Prefix Length
|
||||
// opt_data[1] = Flags (L|A|Reserved1)
|
||||
// opt_data[2..6] = Valid Lifetime
|
||||
// opt_data[6..10] = Preferred Lifetime
|
||||
// opt_data[10..14] = Reserved2
|
||||
// opt_data[14..30] = Prefix (16 bytes)
|
||||
let prefix = Ipv6Cidr::new(
|
||||
Ipv6Address::new(
|
||||
u16::from_be_bytes([opt_data[14], opt_data[15]]),
|
||||
u16::from_be_bytes([opt_data[16], opt_data[17]]),
|
||||
u16::from_be_bytes([opt_data[18], opt_data[19]]),
|
||||
u16::from_be_bytes([opt_data[20], opt_data[21]]),
|
||||
u16::from_be_bytes([opt_data[22], opt_data[23]]),
|
||||
u16::from_be_bytes([opt_data[24], opt_data[25]]),
|
||||
u16::from_be_bytes([opt_data[26], opt_data[27]]),
|
||||
u16::from_be_bytes([opt_data[28], opt_data[29]]),
|
||||
),
|
||||
opt_data[0],
|
||||
);
|
||||
let flags = opt_data[1];
|
||||
let valid_lifetime = u32::from_be_bytes([opt_data[2], opt_data[3], opt_data[4], opt_data[5]]);
|
||||
let preferred_lifetime = u32::from_be_bytes([opt_data[6], opt_data[7], opt_data[8], opt_data[9]]);
|
||||
prefixes.push(RaPrefix {
|
||||
prefix,
|
||||
on_link: flags & 0x80 != 0,
|
||||
autonomous: flags & 0x40 != 0,
|
||||
valid_lifetime,
|
||||
preferred_lifetime,
|
||||
});
|
||||
}
|
||||
pos += opt_len * 8;
|
||||
}
|
||||
|
||||
Some(ParsedRa {
|
||||
cur_hop_limit,
|
||||
router_lifetime,
|
||||
reachable_time,
|
||||
retrans_timer,
|
||||
prefixes,
|
||||
})
|
||||
}
|
||||
|
||||
/// SLAAC daemon state machine. Drives RS → RA exchange and address
|
||||
/// configuration. Mirrors Linux's `addrconf_dad_start()` +
|
||||
/// `ndisc_router_discovery()` flow.
|
||||
#[derive(Debug)]
|
||||
pub struct Slacd {
|
||||
state: SlacdState,
|
||||
mac: EthernetAddress,
|
||||
ll_addr: Ipv6Cidr,
|
||||
retry_count: u8,
|
||||
}
|
||||
|
||||
impl Slacd {
|
||||
pub fn new(mac: EthernetAddress) -> Self {
|
||||
let ll_addr = form_link_local(mac);
|
||||
Self {
|
||||
state: SlacdState::Idle,
|
||||
mac,
|
||||
ll_addr,
|
||||
retry_count: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn state(&self) -> SlacdState {
|
||||
self.state
|
||||
}
|
||||
|
||||
pub fn link_local(&self) -> Ipv6Cidr {
|
||||
self.ll_addr
|
||||
}
|
||||
|
||||
/// Called periodically to drive the SLAAC state machine.
|
||||
/// Returns `Some(rs)` when a Router Solicitation should be sent.
|
||||
pub fn tick(&mut self, now: Instant) -> Option<Vec<u8>> {
|
||||
match self.state {
|
||||
SlacdState::Idle => {
|
||||
self.state = SlacdState::Solicited(now);
|
||||
self.retry_count = 0;
|
||||
Some(build_router_solicitation(self.mac))
|
||||
}
|
||||
SlacdState::Solicited(since) => {
|
||||
if now > since + RA_TIMEOUT {
|
||||
if self.retry_count < MAX_RS_RETRIES {
|
||||
self.retry_count += 1;
|
||||
self.state = SlacdState::Solicited(now);
|
||||
return Some(build_router_solicitation(self.mac));
|
||||
}
|
||||
self.state = SlacdState::Idle;
|
||||
}
|
||||
None
|
||||
}
|
||||
SlacdState::Configured => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a received Router Advertisement. Returns configured
|
||||
/// SLAAC addresses for autonomous prefixes.
|
||||
pub fn process_ra(&mut self, ra: &ParsedRa) -> Vec<Ipv6Cidr> {
|
||||
let mut addrs = Vec::new();
|
||||
for pfx in &ra.prefixes {
|
||||
if pfx.autonomous && pfx.valid_lifetime > 0 && pfx.prefix.prefix_len() == 64 {
|
||||
let addr = form_slaac_addr(pfx.prefix, self.mac);
|
||||
addrs.push(Ipv6Cidr::new(addr, pfx.prefix.prefix_len()));
|
||||
}
|
||||
}
|
||||
if !addrs.is_empty() {
|
||||
self.state = SlacdState::Configured;
|
||||
}
|
||||
addrs
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use smoltcp::wire::Ipv6Address;
|
||||
|
||||
fn make_pio_64(prefix_len: u8) -> Vec<u8> {
|
||||
// RA: 16-byte header + 32-byte PIO option.
|
||||
// The parser advances pos by 2 (type+length) before bounds check,
|
||||
// so pos + opt_len*8 = 18 + 32 = 50 must be <= data.len() = 50.
|
||||
let mut p = vec![0u8; 50];
|
||||
// RA header: type=134, code=0, cksum=0, hop_limit=64,
|
||||
// flags=0, router_lifetime=9000, reachable=0, retransmit=0
|
||||
p[0] = 134; // RA type
|
||||
p[1] = 0; // code
|
||||
p[2] = 0; p[3] = 0; // checksum
|
||||
p[4] = 64; // hop limit
|
||||
p[5] = 0; // flags
|
||||
p[6] = 0; p[7] = 0; // router lifetime (will be ignored)
|
||||
p[8] = 0; p[9] = 0; p[10] = 0; p[11] = 0; // reachable
|
||||
p[12] = 0; p[13] = 0; p[14] = 0; p[15] = 0; // retransmit
|
||||
// PIO option: type=3, length=4, prefix_len, flags, valid, preferred, reserved2, prefix
|
||||
p[16] = 3; // type = PIO
|
||||
p[17] = 4; // length = 4 (32 bytes)
|
||||
p[18] = prefix_len; // prefix length
|
||||
p[19] = 0xc0; // flags: L+A set
|
||||
p[20] = 0; p[21] = 0; p[22] = 0; p[23] = 1; // valid = 1s
|
||||
p[24] = 0; p[25] = 0; p[26] = 0; p[27] = 1; // preferred = 1s
|
||||
p[28] = 0; p[29] = 0; p[30] = 0; p[31] = 0; // reserved
|
||||
// Prefix 2001:db8:: (at opt_data[14..30] within the PIO = p[32..48] in full RA)
|
||||
let prefix = [0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
|
||||
for (i, b) in prefix.iter().enumerate() {
|
||||
p[32 + i] = *b;
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ra_with_pio_64_parses_correctly() {
|
||||
// Regression test: previous off-by-2 bug read prefix from
|
||||
// the wrong field and used wrong prefix length.
|
||||
let p = make_pio_64(64);
|
||||
let ra = parse_router_advertisement(&p);
|
||||
assert!(ra.is_some(), "Valid RA should parse");
|
||||
let ra = ra.unwrap();
|
||||
assert_eq!(ra.prefixes.len(), 1);
|
||||
let pfx = &ra.prefixes[0];
|
||||
// Verify the prefix length is 64 (not garbage from field 2)
|
||||
assert_eq!(pfx.prefix.prefix_len(), 64,
|
||||
"prefix_len should be 64, was {}", pfx.prefix.prefix_len());
|
||||
// Verify the prefix address is 2001:db8::
|
||||
let expected = Ipv6Address::new(0x2001, 0x0db8, 0, 0, 0, 0, 0, 0);
|
||||
assert_eq!(pfx.prefix.address().octets(), expected.octets());
|
||||
// Verify flags
|
||||
assert!(pfx.on_link, "L flag should be set");
|
||||
assert!(pfx.autonomous, "A flag should be set");
|
||||
// Verify lifetimes (1s = 1)
|
||||
assert_eq!(pfx.valid_lifetime, 1);
|
||||
assert_eq!(pfx.preferred_lifetime, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user