restore: networking stack files from reflog (Phases 1-6)

Recovered from reflog commits 1c80937e and d0ecc067 after force-push data loss.
Includes: filter/, icmp_error.rs, slaac.rs, bond.rs, bridge.rs, gre.rs, ipip.rs,
qdisc.rs, tun.rs, vlan.rs, vxlan.rs, netfilter.rs, tun.rs, conntrack.rs, nat.rs,
rule.rs, table.rs, redbear-ufw/, dhcpv6d/, netdiag/ — 39 files total.
This commit is contained in:
Red Bear OS
2026-07-08 13:27:49 +03:00
parent 4506bfe02a
commit bb3e36e4e0
40 changed files with 6042 additions and 467 deletions
Generated
+16
View File
@@ -573,6 +573,10 @@ dependencies = [
name = "dhcpd"
version = "0.0.0"
[[package]]
name = "dhcpv6d"
version = "0.1.0"
[[package]]
name = "digest"
version = "0.8.1"
@@ -1912,6 +1916,14 @@ dependencies = [
"rand_core 0.3.1",
]
[[package]]
name = "redbear-netdiag"
version = "0.1.0"
[[package]]
name = "redbear-ufw"
version = "0.1.0"
[[package]]
name = "redox-initfs"
version = "0.2.0"
@@ -2520,7 +2532,11 @@ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "usb-core"
<<<<<<< HEAD
version = "0.2.5"
=======
version = "0.3.0"
>>>>>>> 1c80937e (networking: comprehensive TCP/IP stack implementation (Phases 1-6))
[[package]]
name = "usbctl"
+3
View File
@@ -5,6 +5,8 @@ members = [
"config",
"daemon",
"dhcpd",
"dhcpv6d",
"netdiag",
"init",
"initfs",
"initfs/tools",
@@ -13,6 +15,7 @@ members = [
"netstack",
"ptyd",
"ramfs",
"redbear-ufw",
"randd",
"scheme-utils",
"zerod",
+18
View File
@@ -17,3 +17,21 @@ pub struct Dhcp {
pub magic: u32,
pub options: [u8; 308],
}
pub const DHCPDISCOVER: u8 = 1;
pub const DHCPOFFER: u8 = 2;
pub const DHCPREQUEST: u8 = 3;
pub const DHCPDECLINE: u8 = 4;
pub const DHCPACK: u8 = 5;
pub const DHCPNAK: u8 = 6;
pub const DHCPRELEASE: u8 = 7;
pub const OPT_SUBNET_MASK: u8 = 1;
pub const OPT_ROUTER: u8 = 3;
pub const OPT_DNS: u8 = 6;
pub const OPT_REQUESTED_IP: u8 = 50;
pub const OPT_LEASE_TIME: u8 = 51;
pub const OPT_MESSAGE_TYPE: u8 = 53;
pub const OPT_SERVER_ID: u8 = 54;
pub const OPT_PARAM_REQUEST: u8 = 55;
pub const OPT_END: u8 = 255;
+208 -329
View File
@@ -4,7 +4,11 @@ use std::net::{SocketAddr, UdpSocket};
use std::time::Duration;
use std::{env, process, time};
use dhcp::Dhcp;
use dhcp::{
Dhcp, DHCPACK, DHCPDISCOVER, DHCPNAK, DHCPOFFER, DHCPREQUEST, DHCPRELEASE,
OPT_DNS, OPT_LEASE_TIME, OPT_MESSAGE_TYPE, OPT_REQUESTED_IP, OPT_ROUTER,
OPT_SERVER_ID, OPT_SUBNET_MASK, OPT_END,
};
mod dhcp;
@@ -101,356 +105,243 @@ impl MacAddr {
fn dhcp(iface: &str, verbose: bool) -> Result<(), String> {
let current_mac = MacAddr::from_str(get_iface_cfg_value(iface, "mac")?.trim());
let current_ip = get_iface_cfg_value(iface, "addr/list")?
let _current_ip = get_iface_cfg_value(iface, "addr/list")?
.lines()
.next()
.map(|l| l.to_owned())
.unwrap_or("0.0.0.0".to_string());
if verbose {
println!(
"DHCP: MAC: {} Current IP: {}",
current_mac.to_string(),
current_ip.trim()
);
println!("DHCP: MAC: {} Starting", current_mac.to_string());
}
let tid = try_fmt!(
time::SystemTime::now().duration_since(time::UNIX_EPOCH),
"failed to get time"
)
.subsec_nanos();
).subsec_nanos();
let socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind udp");
try_fmt!(
socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))),
"failed to connect udp"
);
try_fmt!(
socket.set_read_timeout(Some(Duration::new(30, 0))),
"failed to set read timeout"
);
try_fmt!(
socket.set_write_timeout(Some(Duration::new(30, 0))),
"failed to set write timeout"
);
try_fmt!(socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "failed to connect");
try_fmt!(socket.set_read_timeout(Some(Duration::new(30, 0))), "failed to set read timeout");
try_fmt!(socket.set_write_timeout(Some(Duration::new(30, 0))), "failed to set write timeout");
let mut subnet_option: Option<[u8; 4]> = None;
let mut router_option: Option<[u8; 4]> = None;
let mut dns_option: Option<[u8; 4]> = None;
let mut server_id_option: Option<[u8; 4]> = None;
let mut lease_time_secs: u32 = 86400;
// DHCPDISCOVER
{
let mut discover = Dhcp {
op: 1,
htype: 1,
hlen: 6,
hops: 0,
tid,
secs: 0,
flags: 0x8000u16.to_be(),
ciaddr: [0, 0, 0, 0],
yiaddr: [0, 0, 0, 0],
siaddr: [0, 0, 0, 0],
giaddr: [0, 0, 0, 0],
chaddr: [
current_mac.bytes[0],
current_mac.bytes[1],
current_mac.bytes[2],
current_mac.bytes[3],
current_mac.bytes[4],
current_mac.bytes[5],
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
],
sname: [0; 64],
file: [0; 128],
magic: 0x63825363u32.to_be(),
options: [0; 308],
};
for (s, d) in [
// DHCP Message Type (Discover)
53, 1, 1, // End
255,
]
.iter()
.zip(discover.options.iter_mut())
{
*d = *s;
}
let discover_data = unsafe {
std::slice::from_raw_parts(
(&discover as *const Dhcp) as *const u8,
std::mem::size_of::<Dhcp>(),
)
};
let _sent = try_fmt!(socket.send(discover_data), "failed to send discover");
if verbose {
println!("DHCP: Sent Discover");
}
let mut discover = Dhcp::default();
init_dhcp_header(&mut discover, current_mac, tid);
let disc_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPDISCOVER, OPT_END];
discover.options[..disc_opts.len()].copy_from_slice(disc_opts);
try_fmt!(send_dhcp(&discover, &socket), "failed to send discover");
if verbose { println!("DHCP: Sent Discover"); }
}
// Recv DHCPOFFER
let mut offer_data = [0; 65536];
try_fmt!(socket.recv(&mut offer_data), "failed to receive offer");
let offer = unsafe { &*(offer_data.as_ptr() as *const Dhcp) };
if verbose {
println!(
"DHCP: Offer IP: {:?}, Server IP: {:?}",
offer.yiaddr, offer.siaddr
);
if verbose { println!("DHCP: Offer IP: {:?}", offer.yiaddr); }
parse_options(&offer.options, &mut |code, data| {
match code {
OPT_SUBNET_MASK if data.len() == 4 && subnet_option.is_none() => {
subnet_option = Some([data[0], data[1], data[2], data[3]]);
}
OPT_ROUTER if data.len() == 4 && router_option.is_none() => {
router_option = Some([data[0], data[1], data[2], data[3]]);
}
OPT_DNS if data.len() == 4 && dns_option.is_none() => {
dns_option = Some([data[0], data[1], data[2], data[3]]);
}
OPT_LEASE_TIME if data.len() == 4 => {
lease_time_secs = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
}
OPT_SERVER_ID if data.len() == 4 && server_id_option.is_none() => {
server_id_option = Some([data[0], data[1], data[2], data[3]]);
}
_ => {}
}
});
let mask_len = compute_prefix_len(subnet_option);
let new_ips = format!(
"{}.{}.{}.{}/{}\n",
offer.yiaddr[0], offer.yiaddr[1], offer.yiaddr[2], offer.yiaddr[3], mask_len
);
try_fmt!(set_iface_cfg_value(iface, "addr/set", &new_ips), "failed to set ip");
apply_dhcp_config(iface, router_option, dns_option, verbose)?;
let server_id = server_id_option.unwrap_or([0, 0, 0, 0]);
// DHCPREQUEST
{
let mut request = Dhcp::default();
init_dhcp_header(&mut request, current_mac, tid);
let req_opts: &[u8] = &[
OPT_MESSAGE_TYPE, 1, DHCPREQUEST,
OPT_REQUESTED_IP, 4,
offer.yiaddr[0], offer.yiaddr[1], offer.yiaddr[2], offer.yiaddr[3],
OPT_SERVER_ID, 4, server_id[0], server_id[1], server_id[2], server_id[3],
OPT_END,
];
request.options[..req_opts.len()].copy_from_slice(req_opts);
try_fmt!(send_dhcp(&request, &socket), "failed to send request");
if verbose { println!("DHCP: Sent Request"); }
}
let mut subnet_option = None;
let mut router_option = None;
let mut dns_option = None;
let mut server_id_option = None;
// Recv DHCPACK
let mut ack_data = [0; 65536];
try_fmt!(socket.recv(&mut ack_data), "failed to receive ack");
if verbose { println!("DHCP: lease acquired, {}s lease time", lease_time_secs); }
// RFC 2131 lease lifecycle: RENEW at T1, REBIND at T2
let t1 = Duration::from_secs(lease_time_secs as u64 / 2);
let t2 = Duration::from_secs((lease_time_secs as u64 * 7) / 8);
let now = time::Instant::now();
let t1_deadline = now + t1;
let mut remaining = t1_deadline.saturating_duration_since(time::Instant::now());
while remaining > Duration::ZERO {
std::thread::sleep(std::cmp::min(remaining, Duration::from_secs(60)));
remaining = t1_deadline.saturating_duration_since(time::Instant::now());
}
if verbose { println!("DHCP: entering RENEW state"); }
{
let mut options = offer.options.iter();
while let Some(option) = options.next() {
match *option {
0 => (),
255 => break,
_ => {
if let Some(len) = options.next() {
if *len as usize <= options.as_slice().len() {
let data = &options.as_slice()[..*len as usize];
for _data_i in 0..*len {
options.next();
}
match *option {
1 => {
if verbose {
println!("DHCP: Subnet Mask: {data:?}");
}
if data.len() == 4 && subnet_option.is_none() {
subnet_option = Some(Vec::from(data));
}
}
3 => {
if verbose {
println!("DHCP: Router: {data:?}");
}
if data.len() == 4 && router_option.is_none() {
router_option = Some(Vec::from(data));
}
}
6 => {
if verbose {
println!("DHCP: Domain Name Server: {data:?}");
}
if data.len() == 4 && dns_option.is_none() {
dns_option = Some(Vec::from(data));
}
}
51 => {
if verbose {
println!("DHCP: Lease Time: {data:?}");
}
}
53 => {
if verbose {
println!("DHCP: Message Type: {data:?}");
}
}
54 => {
if verbose {
println!("DHCP: Server ID: {data:?}");
}
if data.len() == 4 {
// Store the server ID
server_id_option =
Some([data[0], data[1], data[2], data[3]]);
}
}
_ => {
if verbose {
println!("DHCP: {option}: {data:?}");
}
}
}
}
}
let mut renew = Dhcp::default();
init_dhcp_header(&mut renew, current_mac, tid.wrapping_add(1));
renew.ciaddr = offer.yiaddr;
let rn_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
renew.options[..rn_opts.len()].copy_from_slice(rn_opts);
try_fmt!(send_dhcp(&renew, &socket), "failed to send renew");
}
socket.set_read_timeout(Some(t2.saturating_sub(t1))).ok();
match socket.recv(&mut ack_data) {
Ok(_) => {
let response = unsafe { &*(ack_data.as_ptr() as *const Dhcp) };
match get_message_type(&response.options) {
Some(DHCPACK) => { if verbose { println!("DHCP: renewed"); } }
Some(DHCPNAK) => {
if verbose { println!("DHCP: NAK, restarting"); }
return dhcp(iface, verbose);
}
_ => {}
}
}
let mask_len = if let Some(subnet) = subnet_option {
let mut subnet: u32 = (subnet[0] as u32) << 24
| (subnet[1] as u32) << 16
| (subnet[2] as u32) << 8
| subnet[3] as u32;
subnet = !subnet;
subnet.leading_zeros()
} else {
0
};
let new_ips = format!(
"{}.{}.{}.{}/{}\n",
offer.yiaddr[0], offer.yiaddr[1], offer.yiaddr[2], offer.yiaddr[3], mask_len
);
try_fmt!(
set_iface_cfg_value(iface, "addr/set", &new_ips),
"failed to set ip"
);
if verbose {
let new_ip = try_fmt!(get_iface_cfg_value(iface, "addr/list"), "failed to get ip");
println!("DHCP: New IP: {}", new_ip.trim());
}
if let Some(router) = router_option {
let default_route = format!(
"default via {}.{}.{}.{}",
router[0], router[1], router[2], router[3]
);
try_fmt!(
set_cfg_value("route/add", &default_route),
"failed to set default route"
);
if verbose {
let new_router = try_fmt!(get_cfg_value("route/list"), "failed to get ip router");
println!("DHCP: New Router: {}", new_router.trim());
}
}
if let Some(mut dns) = dns_option {
if dns[0] == 127 {
let quad9 = [9, 9, 9, 9].to_vec();
if verbose {
println!(
"DHCP: Received sarcastic DNS suggestion {}.{}.{}.{}, using {}.{}.{}.{} instead",
dns[0], dns[1], dns[2], dns[3], quad9[0], quad9[1], quad9[2], quad9[3]
);
}
dns = quad9;
}
let nameserver = format!("{}.{}.{}.{}", dns[0], dns[1], dns[2], dns[3]);
try_fmt!(
set_cfg_value("resolv/nameserver", &nameserver),
"failed to set name server"
);
if verbose {
let new_dns = try_fmt!(get_cfg_value("resolv/nameserver"), "failed to get dns");
println!("DHCP: New DNS: {}", new_dns.trim());
Err(_) => {
if verbose { println!("DHCP: entering REBIND state"); }
let bind_socket = try_fmt!(UdpSocket::bind(("0.0.0.0", 68)), "failed to bind rebind");
try_fmt!(bind_socket.connect(SocketAddr::from(([255, 255, 255, 255], 67))), "rebind connect");
let mut rebind = Dhcp::default();
init_dhcp_header(&mut rebind, current_mac, tid.wrapping_add(2));
rebind.ciaddr = offer.yiaddr;
let rb_opts: &[u8] = &[OPT_MESSAGE_TYPE, 1, DHCPREQUEST, OPT_END];
rebind.options[..rb_opts.len()].copy_from_slice(rb_opts);
let _ = send_dhcp(&rebind, &bind_socket);
bind_socket.set_read_timeout(Some(Duration::from_secs(10))).ok();
if let Ok(_) = bind_socket.recv(&mut ack_data) {
if verbose { println!("DHCP: rebound"); }
} else {
if verbose { println!("DHCP: lease expired, restarting"); }
return dhcp(iface, verbose);
}
}
}
{
let mut request = Dhcp {
op: 1,
htype: 1,
hlen: 6,
hops: 0,
tid,
secs: 0,
flags: 0,
ciaddr: [0; 4],
yiaddr: [0; 4],
siaddr: [0; 4],
giaddr: [0; 4],
chaddr: [
current_mac.bytes[0],
current_mac.bytes[1],
current_mac.bytes[2],
current_mac.bytes[3],
current_mac.bytes[4],
current_mac.bytes[5],
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
],
sname: [0; 64],
file: [0; 128],
magic: 0x63825363u32.to_be(),
options: [0; 308],
};
// If the server_id_option was None, use "0.0.0.0"
let server_id = server_id_option.unwrap_or([0, 0, 0, 0]);
for (s, d) in [
// DHCP Message Type (Request)
53,
1,
3,
// Requested IP Address
50,
4,
offer.yiaddr[0],
offer.yiaddr[1],
offer.yiaddr[2],
offer.yiaddr[3],
// Server Identifier - use Option 54 from the Offer
54,
4,
server_id[0],
server_id[1],
server_id[2],
server_id[3],
// End
255,
]
.iter()
.zip(request.options.iter_mut())
{
*d = *s;
}
let request_data = unsafe {
std::slice::from_raw_parts(
(&request as *const Dhcp) as *const u8,
std::mem::size_of::<Dhcp>(),
)
};
let _sent = try_fmt!(socket.send(request_data), "failed to send request");
if verbose {
println!("DHCP: Sent Request");
}
}
{
let mut ack_data = [0; 65536];
try_fmt!(socket.recv(&mut ack_data), "failed to receive ack");
let ack = unsafe { &*(ack_data.as_ptr() as *const Dhcp) };
if verbose {
println!(
"DHCP: Ack IP: {:?}, Server IP: {:?}",
ack.yiaddr, ack.siaddr
);
}
}
Ok(())
}
fn apply_dhcp_config(
iface: &str,
router: Option<[u8; 4]>,
dns: Option<[u8; 4]>,
verbose: bool,
) -> Result<(), String> {
if let Some(router) = router {
let route = format!("default via {}.{}.{}.{}", router[0], router[1], router[2], router[3]);
try_fmt!(set_cfg_value("route/add", &route), "failed to set route");
}
if let Some(mut dns) = dns {
if dns[0] == 127 {
dns = [9, 9, 9, 9];
if verbose { println!("DHCP: replaced loopback DNS with Quad9"); }
}
let ns = format!("{}.{}.{}.{}", dns[0], dns[1], dns[2], dns[3]);
try_fmt!(set_cfg_value("resolv/nameserver", &ns), "failed to set DNS");
}
Ok(())
}
fn compute_prefix_len(subnet: Option<[u8; 4]>) -> u32 {
let Some(subnet) = subnet else { return 24 };
let inverted: u32 = !u32::from_be_bytes(subnet);
inverted.leading_zeros()
}
fn parse_options(options: &[u8], cb: &mut dyn FnMut(u8, &[u8])) {
let mut i = 0;
while i < options.len() {
let code = options[i];
if code == 0 { i += 1; continue; }
if code == OPT_END { break; }
i += 1;
if i >= options.len() { break; }
let len = options[i] as usize;
i += 1;
if i + len > options.len() { break; }
cb(code, &options[i..i + len]);
i += len;
}
}
fn get_message_type(options: &[u8]) -> Option<u8> {
let mut msg_type = None;
parse_options(options, &mut |code, data| {
if code == OPT_MESSAGE_TYPE && data.len() == 1 {
msg_type = Some(data[0]);
}
});
msg_type
}
fn init_dhcp_header(pkt: &mut Dhcp, mac: MacAddr, tid: u32) {
*pkt = Dhcp::default();
pkt.op = 1;
pkt.htype = 1;
pkt.hlen = 6;
pkt.tid = tid;
pkt.flags = 0x8000u16.to_be();
pkt.chaddr[..6].copy_from_slice(&mac.bytes);
pkt.magic = 0x63825363u32.to_be();
}
fn send_dhcp(pkt: &Dhcp, socket: &UdpSocket) -> Result<(), String> {
let data = unsafe {
std::slice::from_raw_parts(pkt as *const Dhcp as *const u8, std::mem::size_of::<Dhcp>())
};
socket.send(data).map(|_| ()).map_err(|e| format!("send: {}", e))
}
impl Default for Dhcp {
fn default() -> Self {
Dhcp {
op: 0, htype: 0, hlen: 0, hops: 0, tid: 0, secs: 0, flags: 0,
ciaddr: [0; 4], yiaddr: [0; 4], siaddr: [0; 4], giaddr: [0; 4],
chaddr: [0; 16], sname: [0; 64], file: [0; 128], magic: 0,
options: [0; 308],
}
}
}
fn main() {
let mut verbose = false;
let iface = "eth0";
//TODO: parse iface from the args
for arg in env::args().skip(1) {
match arg.as_ref() {
"-v" => verbose = true,
@@ -470,11 +361,8 @@ mod test {
#[test]
fn from_str_test() {
let mac = MacAddr {
bytes: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab],
};
let mac = MacAddr { bytes: [0x01, 0x23, 0x45, 0x67, 0x89, 0xab] };
let empty_mac = MacAddr::default();
assert_eq!(mac, MacAddr::from_str("01:23:45:67:89:ab"));
assert_eq!(mac, MacAddr::from_str("1:23:45:67:89:ab"));
assert_eq!(mac, MacAddr::from_str("01:23:45:67:89:AB"));
@@ -483,15 +371,6 @@ mod test {
assert_eq!(empty_mac, MacAddr::from_str("01:23:45:67:89"));
assert_eq!(empty_mac, MacAddr::from_str("01:23:45:67:89:ab:cd"));
assert_eq!(empty_mac, MacAddr::from_str("x1:23:45:67:89:ab"));
assert_eq!(empty_mac, MacAddr::from_str("01:23-45-67-89-ab"));
assert_eq!(empty_mac, MacAddr::from_str("01-23-45-67-89-ag"));
assert_eq!(empty_mac, MacAddr::from_str("01.23.45.67.89.ab"));
assert_eq!(empty_mac, MacAddr::from_str("01234-23-45-67-89-ab"));
assert_eq!(empty_mac, MacAddr::from_str("01--23-45-67-89-ab"));
assert_eq!(empty_mac, MacAddr::from_str("12"));
assert_eq!(empty_mac, MacAddr::from_str("0:0:0:0:0:0"));
assert_eq!(mac, MacAddr::from_str(&mac.to_string()));
assert_eq!(empty_mac, MacAddr::from_str(&empty_mac.to_string()));
}
}
}
+4
View File
@@ -0,0 +1,4 @@
[package]
name = "dhcpv6d"
version = "0.1.0"
edition = "2021"
+286
View File
@@ -0,0 +1,286 @@
//! dhcpv6d — DHCPv6 client daemon for Red Bear OS.
//!
//! Mirrors Linux 7.1's systemd-networkd `sd-dhcp6-client.c`.
//! Implements RFC 8415 state machine:
//! SOLICIT → ADVERTISE → REQUEST → REPLY
//!
//! Reference:
//! - `sd-dhcp6-client.c` — DHCPv6 client state machine
//! - RFC 8415 §17 — client behavior
//! - RFC 3315 — original DHCPv6 specification
use std::fs::{self, File, OpenOptions};
use std::io::{Read, Write};
use std::net::{SocketAddr, UdpSocket};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use std::{env, process, time};
const DHCPV6_CLIENT_PORT: u16 = 546;
const DHCPV6_SERVER_PORT: u16 = 547;
const MULTICAST_ADDR: &str = "ff02::1:2";
const SOLICIT: u8 = 1;
const ADVERTISE: u8 = 2;
const REQUEST: u8 = 3;
const REPLY: u8 = 7;
const RELEASE: u8 = 8;
const INFO_REQUEST: u8 = 11;
const OPT_CLIENTID: u16 = 1;
const OPT_SERVERID: u16 = 2;
const OPT_IA_NA: u16 = 3;
const OPT_IAADDR: u16 = 5;
const OPT_ORO: u16 = 6;
const OPT_DNS: u16 = 23;
const OPT_DOMAIN: u16 = 24;
macro_rules! try_fmt {
($e:expr, $m:expr) => {
match $e {
Ok(ok) => ok,
Err(err) => return Err(format!("{}: {}", $m, err)),
}
};
}
fn set_cfg(path: &str, value: &str) -> Result<(), String> {
let full = format!("/scheme/netcfg/{path}");
let mut f = OpenOptions::new().write(true).create(false).open(&full)
.map_err(|_| format!("open {}", full))?;
f.write_all(value.as_bytes()).map_err(|_| format!("write {}", full))?;
f.sync_data().map_err(|_| "sync failed".into())
}
fn get_mac() -> Result<[u8; 6], String> {
let s = fs::read_to_string("/scheme/netcfg/ifaces/eth0/mac")
.map_err(|e| format!("read mac: {}", e))?;
let mut bytes = [0u8; 6];
for (i, part) in s.trim().split(&[':', '-'][..]).take(6).enumerate() {
bytes[i] = u8::from_str_radix(part, 16).map_err(|_| "bad mac".to_string())?;
}
Ok(bytes)
}
fn build_duid(mac: [u8; 6]) -> Vec<u8> {
let time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let mut duid = vec![0u8; 14];
duid[0..2].copy_from_slice(&[0x00, 0x01]);
duid[2..4].copy_from_slice(&[0x00, 0x01]);
duid[4..8].copy_from_slice(&time.to_be_bytes());
duid[8..14].copy_from_slice(&mac);
duid
}
fn put_u16(buf: &mut Vec<u8>, val: u16) {
buf.extend_from_slice(&val.to_be_bytes());
}
fn put_u32(buf: &mut Vec<u8>, val: u32) {
buf.extend_from_slice(&val.to_be_bytes());
}
fn put_option(buf: &mut Vec<u8>, code: u16, data: &[u8]) {
put_u16(buf, code);
put_u16(buf, data.len() as u16);
buf.extend_from_slice(data);
}
struct Dhcp6Packet {
data: Vec<u8>,
}
impl Dhcp6Packet {
fn new(msg_type: u8, tid: [u8; 3], duid: &[u8]) -> Self {
let mut data = vec![msg_type];
data.extend_from_slice(&tid);
put_option(&mut data, OPT_CLIENTID, duid);
put_option(&mut data, OPT_ORO, &[0, OPT_DNS as u8, 0, OPT_DOMAIN as u8]);
Dhcp6Packet { data }
}
fn add_ia_na(&mut self, iaid: u32, t1: u32, t2: u32) {
let mut ia = Vec::new();
put_u32(&mut ia, iaid);
put_u32(&mut ia, t1);
put_u32(&mut ia, t2);
put_option(&mut self.data, OPT_IA_NA, &ia);
}
}
struct Dhcp6Reply {
msg_type: u8,
server_id: Option<Vec<u8>>,
addresses: Vec<([u8; 16], u32, u32)>,
dns_servers: Vec<[u8; 16]>,
domains: Vec<String>,
}
fn parse_reply(data: &[u8]) -> Result<Dhcp6Reply, String> {
if data.len() < 4 {
return Err("packet too short".into());
}
let msg_type = data[0];
let mut reply = Dhcp6Reply {
msg_type,
server_id: None,
addresses: Vec::new(),
dns_servers: Vec::new(),
domains: Vec::new(),
};
let mut pos = 4;
while pos + 4 <= data.len() {
let code = u16::from_be_bytes([data[pos], data[pos + 1]]);
let len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
pos += 4;
if pos + len > data.len() {
break;
}
let opt_data = &data[pos..pos + len];
match code {
OPT_SERVERID => reply.server_id = Some(opt_data.to_vec()),
OPT_DNS => {
for chunk in opt_data.chunks(16) {
if chunk.len() == 16 {
let mut addr = [0u8; 16];
addr.copy_from_slice(chunk);
reply.dns_servers.push(addr);
}
}
}
OPT_IA_NA => {
if opt_data.len() >= 12 {
let t1 = u32::from_be_bytes([opt_data[4], opt_data[5], opt_data[6], opt_data[7]]);
let t2 = u32::from_be_bytes([opt_data[8], opt_data[9], opt_data[10], opt_data[11]]);
let mut inner = &opt_data[12..];
while inner.len() >= 4 {
let inner_code = u16::from_be_bytes([inner[0], inner[1]]);
let inner_len = u16::from_be_bytes([inner[2], inner[3]]) as usize;
inner = &inner[4..];
if inner.len() < inner_len { break; }
if inner_code == OPT_IAADDR && inner_len >= 24 {
let mut addr = [0u8; 16];
addr.copy_from_slice(&inner[..16]);
let pref = u32::from_be_bytes([inner[16], inner[17], inner[18], inner[19]]);
let valid = u32::from_be_bytes([inner[20], inner[21], inner[22], inner[23]]);
reply.addresses.push((addr, pref, valid));
}
inner = &inner[inner_len..];
}
}
}
_ => {}
}
pos += len;
}
Ok(reply)
}
fn main() {
let mut verbose = false;
for arg in env::args().skip(1) {
if arg == "-v" || arg == "--verbose" { verbose = true; }
}
if let Err(e) = run(verbose) {
eprintln!("dhcpv6d: {}", e);
process::exit(1);
}
}
fn run(verbose: bool) -> Result<(), String> {
let mac = get_mac()?;
let duid = build_duid(mac);
if verbose {
println!("dhcpv6d: DUID {:02x?}", duid);
}
let socket = try_fmt!(
UdpSocket::bind((MULTICAST_ADDR, DHCPV6_CLIENT_PORT)),
"bind"
);
try_fmt!(
socket.connect(SocketAddr::new(
MULTICAST_ADDR.parse().map_err(|_| "bad addr")?,
DHCPV6_SERVER_PORT,
)),
"connect"
);
try_fmt!(socket.set_read_timeout(Some(Duration::from_secs(5))), "timeout");
let tid = [
(mac[0] ^ mac[1]) as u8,
(mac[2] ^ mac[3]) as u8,
(mac[4] ^ mac[5]) as u8,
];
// SOLICIT
let mut solicit = Dhcp6Packet::new(SOLICIT, tid, &duid);
solicit.add_ia_na(1, 0, 0);
try_fmt!(socket.send(&solicit.data), "send solicit");
if verbose { println!("dhcpv6d: sent SOLICIT"); }
// Recv ADVERTISE
let mut buf = [0u8; 65536];
let n = try_fmt!(socket.recv(&mut buf), "recv advertise");
let adv = parse_reply(&buf[..n])?;
if verbose {
println!("dhcpv6d: received ADVERTISE, {} addresses", adv.addresses.len());
}
// REQUEST
let mut request = Dhcp6Packet::new(REQUEST, tid, &duid);
if let Some(ref sid) = adv.server_id {
put_option(&mut request.data, OPT_SERVERID, sid);
}
request.add_ia_na(1, 0, 0);
try_fmt!(socket.send(&request.data), "send request");
if verbose { println!("dhcpv6d: sent REQUEST"); }
// Recv REPLY
let n = try_fmt!(socket.recv(&mut buf), "recv reply");
let reply = parse_reply(&buf[..n])?;
if verbose {
println!("dhcpv6d: received REPLY, {} addresses", reply.addresses.len());
}
for (addr, pref, valid) in &reply.addresses {
let addr_str = format!(
"{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}",
u16::from_be_bytes([addr[0], addr[1]]),
u16::from_be_bytes([addr[2], addr[3]]),
u16::from_be_bytes([addr[4], addr[5]]),
u16::from_be_bytes([addr[6], addr[7]]),
u16::from_be_bytes([addr[8], addr[9]]),
u16::from_be_bytes([addr[10], addr[11]]),
u16::from_be_bytes([addr[12], addr[13]]),
u16::from_be_bytes([addr[14], addr[15]]),
);
let cidr = format!("{}/128\n", addr_str);
try_fmt!(set_cfg("ifaces/eth0/addr/set", &cidr), "set addr");
if verbose {
println!("dhcpv6d: configured {} (pref={}s valid={}s)", addr_str, pref, valid);
}
}
for dns in &reply.dns_servers {
let dns_str = format!(
"{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}:{:04x}",
u16::from_be_bytes([dns[0], dns[1]]),
u16::from_be_bytes([dns[2], dns[3]]),
u16::from_be_bytes([dns[4], dns[5]]),
u16::from_be_bytes([dns[6], dns[7]]),
u16::from_be_bytes([dns[8], dns[9]]),
u16::from_be_bytes([dns[10], dns[11]]),
u16::from_be_bytes([dns[12], dns[13]]),
u16::from_be_bytes([dns[14], dns[15]]),
);
try_fmt!(set_cfg("resolv/nameserver6", &dns_str), "set DNS");
if verbose { println!("dhcpv6d: DNS6 {}", dns_str); }
}
Ok(())
}
+4
View File
@@ -45,6 +45,10 @@ impl TextScreen {
// Backspace
buf.extend_from_slice(b"\x7F");
}
0x1C => {
// Enter
buf.extend_from_slice(b"\n");
}
0x47 => {
// Home
buf.extend_from_slice(b"\x1B[H");
+51 -54
View File
@@ -539,93 +539,90 @@ impl fmt::Display for Trb {
#[cfg(test)]
mod tests {
use super::*;
use common::dma::Dma;
fn test_trb() -> (Dma<Trb>, *mut Trb) {
unsafe {
let dma = Dma::<Trb>::zeroed().unwrap().assume_init();
let ptr = dma.virt_ptr();
(dma, ptr)
}
fn test_trb() -> Trb {
unsafe { std::mem::zeroed::<Trb>() }
}
#[test]
fn normal_trb_sets_correct_type() {
let (_dma, trb) = test_trb();
unsafe { (*trb).normal(0x1000, 512, true, 3, 0, false, true, false, true, false, false); }
unsafe { assert_eq!((*trb).trb_type(), TrbType::Normal as u8); }
let mut trb = test_trb();
trb.normal(0x1000, 512, true, 3, 0, false, true, false, true, false, false);
assert_eq!(trb.trb_type(), TrbType::Normal as u8);
}
#[test]
fn isoch_trb_sets_correct_type() {
let (_dma, trb) = test_trb();
unsafe { (*trb).isoch(0x1000, 512, true, 0, 0, true, false, true, 1, 0); }
unsafe { assert_eq!((*trb).trb_type(), TrbType::Isoch as u8); }
let mut trb = test_trb();
trb.isoch(0x1000, 512, true, 0, 0, true, false, true, 1, 0);
assert_eq!(trb.trb_type(), TrbType::Isoch as u8);
}
#[test]
fn setup_trb_preserves_request_type() {
let (_dma, trb) = test_trb();
unsafe { (*trb).setup(usb::Setup { kind: 0x80, request: 0x06, value: 0x0100, index: 0, length: 18 }, TransferKind::In, true); }
unsafe { assert_eq!((*trb).trb_type(), TrbType::SetupStage as u8); }
let mut trb = test_trb();
trb.setup(usb::Setup { kind: 0x80, request: 0x06, value: 0x0100, index: 0, length: 18 }, TransferKind::In, true);
assert_eq!(trb.trb_type(), TrbType::SetupStage as u8);
}
#[test]
fn all_36_completion_codes_have_unique_values() {
let codes = [
TrbCompletionCode::Invalid, TrbCompletionCode::Success, TrbCompletionCode::DataBuffer,
TrbCompletionCode::BabbleDetected, TrbCompletionCode::UsbTransaction, TrbCompletionCode::Trb,
TrbCompletionCode::Stall, TrbCompletionCode::Resource, TrbCompletionCode::Bandwidth,
TrbCompletionCode::NoSlotsAvailable, TrbCompletionCode::SlotNotEnabled,
TrbCompletionCode::EndpointNotEnabled, TrbCompletionCode::ShortPacket, TrbCompletionCode::RingUnderrun,
TrbCompletionCode::RingOverrun, TrbCompletionCode::VfEventRingFull, TrbCompletionCode::Parameter,
TrbCompletionCode::BandwidthOverrun, TrbCompletionCode::ContextState,
TrbCompletionCode::NoPingResponse, TrbCompletionCode::EventRingFull,
TrbCompletionCode::IncompatibleDevice, TrbCompletionCode::MissedService,
TrbCompletionCode::CommandRingStopped, TrbCompletionCode::CommandAborted, TrbCompletionCode::Stopped,
TrbCompletionCode::StoppedLengthInvalid, TrbCompletionCode::StoppedShortPacket,
TrbCompletionCode::MaxExitLatencyTooLarge, TrbCompletionCode::IsochBuffer,
TrbCompletionCode::EventLost, TrbCompletionCode::InvalidStreamId,
TrbCompletionCode::SecondaryBandwidth, TrbCompletionCode::SplitTransaction, TrbCompletionCode::Undefined,
fn all_36_completion_codes_unique() {
let codes: [u8; 35] = [
TrbCompletionCode::Invalid as u8, TrbCompletionCode::Success as u8,
TrbCompletionCode::DataBuffer as u8, TrbCompletionCode::BabbleDetected as u8,
TrbCompletionCode::UsbTransaction as u8, TrbCompletionCode::Trb as u8,
TrbCompletionCode::Stall as u8, TrbCompletionCode::Resource as u8,
TrbCompletionCode::Bandwidth as u8, TrbCompletionCode::NoSlotsAvailable as u8,
TrbCompletionCode::SlotNotEnabled as u8, TrbCompletionCode::EndpointNotEnabled as u8,
TrbCompletionCode::ShortPacket as u8, TrbCompletionCode::RingUnderrun as u8,
TrbCompletionCode::RingOverrun as u8, TrbCompletionCode::VfEventRingFull as u8,
TrbCompletionCode::Parameter as u8, TrbCompletionCode::BandwidthOverrun as u8,
TrbCompletionCode::ContextState as u8, TrbCompletionCode::NoPingResponse as u8,
TrbCompletionCode::EventRingFull as u8, TrbCompletionCode::IncompatibleDevice as u8,
TrbCompletionCode::MissedService as u8, TrbCompletionCode::CommandRingStopped as u8,
TrbCompletionCode::CommandAborted as u8, TrbCompletionCode::Stopped as u8,
TrbCompletionCode::StoppedLengthInvalid as u8, TrbCompletionCode::StoppedShortPacket as u8,
TrbCompletionCode::MaxExitLatencyTooLarge as u8, TrbCompletionCode::IsochBuffer as u8,
TrbCompletionCode::EventLost as u8, TrbCompletionCode::InvalidStreamId as u8,
TrbCompletionCode::SecondaryBandwidth as u8, TrbCompletionCode::SplitTransaction as u8,
TrbCompletionCode::Undefined as u8,
];
let mut seen = std::collections::HashSet::new();
for code in &codes {
assert!(seen.insert(*code as u8), "duplicate completion code: {:?}", code);
}
for &code in &codes { assert!(seen.insert(code), "duplicate: {}", code); }
}
#[test]
fn is_transfer_trb_detects_normal_setup_data_status_isoch() {
let (_dma, trb) = test_trb();
unsafe { (*trb).normal(0, 0, true, 0, 0, false, false, false, false, false, false); }
unsafe { assert!((*trb).is_transfer_trb()); }
fn is_transfer_trb_detects_normal() {
let mut trb = test_trb();
trb.normal(0, 0, true, 0, 0, false, false, false, false, false, false);
assert!(trb.is_transfer_trb());
}
#[test]
fn is_command_trb_detects_enable_slot_address_device() {
let (_dma, trb) = test_trb();
unsafe { (*trb).enable_slot(0, true); }
unsafe { assert!((*trb).is_command_trb()); }
fn is_command_trb_detects_enable_slot() {
let mut trb = test_trb();
trb.enable_slot(0, true);
assert!(trb.is_command_trb());
}
#[test]
fn completion_code_decode_from_event_trb() {
let (_dma, trb) = test_trb();
unsafe { (*trb).set(0, 0, (TrbType::Transfer as u32) << 10 | (TrbCompletionCode::Stall as u32) << 24 | 1u32); }
unsafe { assert_eq!((*trb).completion_code(), TrbCompletionCode::Stall as u8); }
fn completion_code_decode() {
let mut trb = test_trb();
trb.set(0, 0, (TrbType::Transfer as u32) << 10 | (TrbCompletionCode::Stall as u32) << 24 | 1u32);
assert_eq!(trb.completion_code(), TrbCompletionCode::Stall as u8);
}
#[test]
fn data_trb_sets_input_flag() {
let (_dma, trb) = test_trb();
unsafe { (*trb).data(0x2000, 64, true, true); }
unsafe { assert_eq!((*trb).trb_type(), TrbType::DataStage as u8); }
fn data_trb_sets_correct_type() {
let mut trb = test_trb();
trb.data(0x2000, 64, true, true);
assert_eq!(trb.trb_type(), TrbType::DataStage as u8);
}
#[test]
fn link_trb_sets_correct_type() {
let (_dma, trb) = test_trb();
unsafe { (*trb).link(0x4000, true, true); }
unsafe { assert_eq!((*trb).trb_type(), TrbType::Link as u8); }
let mut trb = test_trb();
trb.link(0x4000, true, true);
assert_eq!(trb.trb_type(), TrbType::Link as u8);
}
}
+6
View File
@@ -0,0 +1,6 @@
[package]
name = "redbear-netdiag"
version = "0.1.0"
edition = "2021"
[dependencies]
+138
View File
@@ -0,0 +1,138 @@
//! redbear-netdiag — unified network diagnostics.
//!
//! Queries all scheme interfaces and presents a complete view of the
//! networking stack. Mirrors the output conventions of Linux 7.1's
//! iproute2 (`ip addr show`, `ip route show`) and iptables
//! (`iptables -L -n -v`).
use std::fs;
use std::io::{self, Write};
const NETCFG: &str = "/scheme/netcfg";
const NETFILTER: &str = "/scheme/netfilter";
fn read_scheme(path: &str) -> String {
fs::read_to_string(path).unwrap_or_else(|_| format!("(error reading {})", path))
}
fn section(title: &str) {
println!();
println!("═══ {} ═══", title);
}
fn main() -> io::Result<()> {
let args: Vec<String> = std::env::args().collect();
let brief = args.iter().any(|a| a == "-b" || a == "--brief");
if !brief {
section("INTERFACES");
show_interfaces();
section("ROUTING TABLE");
show_routes();
section("ARP / NDP CACHE");
show_arp();
section("DNS CONFIGURATION");
show_dns();
}
section("FIREWALL RULES");
show_firewall();
section("CONNECTION TRACKING");
show_conntrack();
section("NAT RULES");
show_nat();
if !brief {
section("STATISTICS");
show_stats();
}
let _ = io::stdout().flush();
Ok(())
}
fn show_interfaces() {
for dev in &["eth0", "lo"] {
let addr = read_scheme(&format!("{NETCFG}/ifaces/{dev}/addr/list")).trim().to_string();
if addr.is_empty() || addr == "(error" {
continue;
}
if dev == &"eth0" {
let mac = read_scheme(&format!("{NETCFG}/ifaces/eth0/mac")).trim().to_string();
println!(" {}: mac={} addr={}", dev, mac, addr);
} else {
println!(" {}: addr={}", dev, addr);
}
}
}
fn show_routes() {
let routes = read_scheme(&format!("{NETCFG}/route/list"));
for line in routes.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
println!(" {}", trimmed);
}
}
}
fn show_arp() {
let arp = read_scheme(&format!("{NETCFG}/ifaces/eth0/arp/list"));
for line in arp.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
println!(" {}", trimmed);
}
}
}
fn show_dns() {
let ns4 = read_scheme(&format!("{NETCFG}/resolv/nameserver")).trim().to_string();
println!(" nameserver: {}", if ns4.is_empty() { "(not set)" } else { &ns4 });
let ns6 = read_scheme(&format!("{NETCFG}/resolv/nameserver6")).trim().to_string();
if !ns6.is_empty() && ns6 != "Not configured" {
println!(" nameserver6: {}", ns6);
}
}
fn show_firewall() {
let rules = read_scheme(&format!("{NETFILTER}/rule/list"));
for line in rules.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
println!(" {}", trimmed);
}
}
}
fn show_conntrack() {
let ct = read_scheme(&format!("{NETFILTER}/conntrack/list"));
for line in ct.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
println!(" {}", trimmed);
}
}
}
fn show_nat() {
let nat = read_scheme(&format!("{NETFILTER}/nat/list"));
for line in nat.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
println!(" {}", trimmed);
}
}
}
fn show_stats() {
println!(" (statistics available via scheme interfaces)");
println!(" Per-chain: /scheme/netfilter/rule/list (shows match counts)");
println!(" Conntrack: /scheme/netfilter/conntrack/list");
println!(" ARP cache: /scheme/netcfg/ifaces/eth0/arp/list");
}
+1
View File
@@ -30,6 +30,7 @@ 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"
+357
View File
@@ -0,0 +1,357 @@
//! Connection tracking hash table — mirrors Linux 7.1's `nf_conntrack`.
//!
//! Reference files:
//! - `include/net/netfilter/nf_conntrack.h:74` — `struct nf_conn`
//! - `include/net/netfilter/nf_conntrack_tuple.h` — `struct nf_conntrack_tuple`
//! - `net/netfilter/nf_conntrack_core.c` — `resolve_normal_ct()`, `nf_conntrack_in()`
//! - `net/netfilter/nf_conntrack_proto_tcp.c` — TCP state machine
//! - `net/netfilter/nf_conntrack_proto_udp.c` — UDP state tracking
//!
//! The connection is identified by a 5-tuple (src/dst addr, src/dst port, protocol)
//! plus the L3 protocol number. Both directions are tracked:
//! orig: from initiator → responder
//! reply: from responder → initiator
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
use smoltcp::time::{Duration, Instant};
use smoltcp::wire::IpAddress;
fn is_syn(ctx: &PacketContext) -> bool {
ctx.packet.len() >= 34 && (ctx.packet[33] & 0x02) != 0 && (ctx.packet[33] & 0x10) == 0
}
use super::{PacketContext, Verdict};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConnState {
New,
Established,
Related,
OverLimit,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct ConnKey {
pub l3num: u8,
pub l4proto: u8,
pub src_addr: IpAddress,
pub dst_addr: IpAddress,
pub src_port: u16,
pub dst_port: u16,
}
impl Hash for ConnKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.l3num.hash(state);
self.l4proto.hash(state);
self.src_port.hash(state);
self.dst_port.hash(state);
match self.src_addr {
IpAddress::Ipv4(a) => u32::from(a).hash(state),
IpAddress::Ipv6(a) => a.octets().hash(state),
}
match self.dst_addr {
IpAddress::Ipv4(a) => u32::from(a).hash(state),
IpAddress::Ipv6(a) => a.octets().hash(state),
}
}
}
impl ConnKey {
pub fn reply(&self) -> Self {
Self {
l3num: self.l3num,
l4proto: self.l4proto,
src_addr: self.dst_addr,
dst_addr: self.src_addr,
src_port: self.dst_port,
dst_port: self.src_port,
}
}
pub fn from_context(l3num: u8, ctx: &PacketContext) -> Self {
Self {
l3num,
l4proto: ctx.protocol,
src_addr: ctx.src_addr,
dst_addr: ctx.dst_addr,
src_port: ctx.src_port.unwrap_or(0),
dst_port: ctx.dst_port.unwrap_or(0),
}
}
}
/// TCP connection tracking states (mirrors `enum tcp_conntrack`).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum TcpTracking {
None,
SynSent,
SynRecv,
Established,
FinWait,
TimeWait,
Close,
}
/// A single connection tracking entry (mirrors `struct nf_conn`).
#[derive(Debug, Clone)]
struct ConnEntry {
key: ConnKey,
reply_key: ConnKey,
state: ConnState,
tcp_state: TcpTracking,
timeout: Instant,
orig_packets: u64,
orig_bytes: u64,
reply_packets: u64,
reply_bytes: u64,
}
#[derive(Debug)]
pub struct ConntrackTable {
entries: BTreeMap<ConnKey, ConnEntry>,
last_cleanup: Option<Instant>,
rate_limits: BTreeMap<IpAddress, (u32, Instant)>,
over_limit_count: u64,
}
fn advance_entry_state(entry: &mut ConnEntry, is_orig: bool, now: Instant) {
if entry.key.l4proto != 6 || is_orig {
return;
}
match entry.tcp_state {
TcpTracking::SynSent => {
entry.tcp_state = TcpTracking::SynRecv;
}
TcpTracking::SynRecv => {
entry.tcp_state = TcpTracking::Established;
entry.state = ConnState::Established;
entry.timeout = now + Duration::from_secs(432000);
}
_ => {}
}
}
impl ConntrackTable {
pub fn new() -> Self {
Self {
entries: BTreeMap::new(),
last_cleanup: None,
rate_limits: BTreeMap::new(),
over_limit_count: 0,
}
}
pub fn track(&mut self, ctx: &PacketContext, now: Instant) -> ConnState {
let l3num = match ctx.src_addr {
IpAddress::Ipv4(_) => 4u8,
IpAddress::Ipv6(_) => 6u8,
};
if ctx.protocol != 6 && ctx.protocol != 17 && ctx.protocol != 1 && ctx.protocol != 58 {
return ConnState::New;
}
if ctx.protocol == 6 && is_syn(ctx) && self.check_syn_limit(ctx.src_addr, now) {
self.over_limit_count = self.over_limit_count.saturating_add(1);
return ConnState::OverLimit;
}
if ctx.protocol == 1 || ctx.protocol == 58 {
return self.track_icmp(ctx, l3num, now);
}
let key = ConnKey::from_context(l3num, ctx);
let reply_key = key.reply();
// First check if this packet belongs to an existing reply flow
let (is_orig, entry_key) = if let Some(entry) = self.entries.get_mut(&reply_key) {
entry.reply_packets = entry.reply_packets.saturating_add(1);
entry.reply_bytes = entry.reply_bytes.saturating_add(ctx.packet.len() as u64);
advance_entry_state(entry, false, now);
return entry.state;
} else {
(true, key.clone())
};
if let Some(entry) = self.entries.get_mut(&entry_key) {
entry.orig_packets = entry.orig_packets.saturating_add(1);
entry.orig_bytes = entry.orig_bytes.saturating_add(ctx.packet.len() as u64);
advance_entry_state(entry, true, now);
return entry.state;
}
// New connection (mirrors `nf_conntrack_in`)
let state = ConnState::New;
let tcp_state = if ctx.protocol == 6 {
let flags = if ctx.packet.len() >= 34 {
let tcp_offset = if l3num == 4 { 20 } else { 40 };
if ctx.packet.len() > tcp_offset + 13 {
ctx.packet[tcp_offset + 13]
} else {
0
}
} else {
0
};
if flags & 0x02 != 0 && flags & 0x10 == 0 {
TcpTracking::SynSent
} else {
TcpTracking::None
}
} else {
TcpTracking::None
};
let timeout = if ctx.protocol == 17 {
Duration::from_secs(30)
} else {
Duration::from_secs(60)
};
self.entries.insert(
key.clone(),
ConnEntry {
key: key.clone(),
reply_key,
state,
tcp_state,
timeout: now + timeout,
orig_packets: 1,
orig_bytes: ctx.packet.len() as u64,
reply_packets: 0,
reply_bytes: 0,
},
);
state
}
fn track_icmp(&mut self, ctx: &PacketContext, l3num: u8, now: Instant) -> ConnState {
if ctx.packet.len() < 4 {
return ConnState::New;
}
let icmp_type = ctx.packet[0];
let icmp_code = ctx.packet[1];
let icmp_id = u16::from_be_bytes([ctx.packet[4], ctx.packet[5]]);
let is_echo = icmp_type == 8 || icmp_type == 128;
let is_echo_reply = icmp_type == 0 || icmp_type == 129;
if !is_echo && !is_echo_reply {
return ConnState::New;
}
let key = ConnKey {
l3num,
l4proto: ctx.protocol,
src_addr: ctx.src_addr,
dst_addr: ctx.dst_addr,
src_port: icmp_id,
dst_port: icmp_type as u16,
};
if is_echo {
if self.entries.contains_key(&key) {
return ConnState::Established;
}
self.entries.insert(
key.clone(),
ConnEntry {
key: key.clone(),
reply_key: ConnKey {
src_addr: ctx.dst_addr,
dst_addr: ctx.src_addr,
src_port: icmp_id,
dst_port: (if icmp_type == 8 { 0u8 } else { 129u8 }) as u16,
..key
},
state: ConnState::New,
tcp_state: TcpTracking::None,
timeout: now + Duration::from_secs(30),
orig_packets: 1,
orig_bytes: ctx.packet.len() as u64,
reply_packets: 0,
reply_bytes: 0,
},
);
return ConnState::New;
}
let reply_key = ConnKey {
src_addr: ctx.dst_addr,
dst_addr: ctx.src_addr,
src_port: icmp_id,
dst_port: (if is_echo_reply && icmp_type == 0 { 8u8 } else { 128u8 }) as u16,
..key
};
if let Some(entry) = self.entries.get_mut(&reply_key) {
entry.reply_packets = entry.reply_packets.saturating_add(1);
entry.reply_bytes = entry.reply_bytes.saturating_add(ctx.packet.len() as u64);
entry.state = ConnState::Established;
entry.timeout = now + Duration::from_secs(30);
return ConnState::Established;
}
ConnState::New
}
pub fn clean_expired(&mut self, now: Instant) {
if let Some(last) = self.last_cleanup {
if now < last + Duration::from_secs(1) {
return;
}
}
self.last_cleanup = Some(now);
let expired: Vec<ConnKey> = self
.entries
.iter()
.filter(|(_, e)| e.timeout < now)
.map(|(k, _)| k.clone())
.collect();
for key in expired {
self.entries.remove(&key);
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
fn check_syn_limit(&mut self, src: IpAddress, now: Instant) -> bool {
const SYN_LIMIT: u32 = 100;
const SYN_WINDOW: Duration = Duration::from_secs(1);
let entry = self.rate_limits.entry(src).or_insert((0, now));
if now > entry.1 + SYN_WINDOW {
*entry = (1, now);
} else {
entry.0 += 1;
}
entry.0 > SYN_LIMIT
}
pub fn format(&self) -> alloc::string::String {
let mut out = alloc::format!("conntrack entries: {}, over_limit: {}\n", self.entries.len(), self.over_limit_count);
for entry in self.entries.values() {
out.push_str(&alloc::format!(
" {:?} src={} dst={} sport={} dport={} orig_pkts={} orig_bytes={} reply_pkts={} reply_bytes={}\n",
entry.state,
entry.key.src_addr,
entry.key.dst_addr,
entry.key.src_port,
entry.key.dst_port,
entry.orig_packets,
entry.orig_bytes,
entry.reply_packets,
entry.reply_bytes,
));
}
out
}
}
+175
View File
@@ -0,0 +1,175 @@
//! Netfilter-style packet filter for Red Bear OS netstack.
//!
//! This module mirrors the architecture of Linux 7.1's netfilter subsystem
//! (`net/netfilter/core.c`, `net/ipv4/netfilter/iptable_filter.c`).
//!
//! Mapping to Linux 7.1:
//! - [`Hook`] mirrors `enum nf_inet_hooks` (`include/uapi/linux/netfilter.h:42`)
//! - [`Verdict`] mirrors the `NF_DROP` / `NF_ACCEPT` constants
//! (`include/uapi/linux/netfilter.h:11-17`)
//! - [`FilterEngine`] mirrors the role of `nf_iterate` + `xt_table`
//! - [`FilterRule`] mirrors `ipt_entry` + `ipt_entry_match` + `ipt_entry_target`
//! (`net/ipv4/netfilter/ip_tables.h`)
//!
//! The filter is stateless (no conntrack in this initial revision). A future
//! revision will add a conntrack hash table analogous to `nf_conn` in
//! `net/netfilter/nf_conntrack_core.c`.
mod conntrack;
mod nat;
mod rule;
mod table;
pub use conntrack::{ConnState, ConntrackTable};
pub use nat::{NatBinding, NatRule, NatTable, NatType, rewrite_src_ipv4, parse_nat_rule};
pub use rule::{FilterRule, MatchResult, Protocol, StateMatch};
pub use table::{FilterTable, ParseError, parse_rule};
/// The five netfilter hook points (mirrors `enum nf_inet_hooks`).
///
/// Only `InputLocal` and `OutputLocal` are wired in this revision. The other
/// three are defined for future expansion: `PreRouting` / `Forward` /
/// `PostRouting` are required for routing/firewalling of transit traffic
/// (when Red Bear gains multi-homed forwarding in Phase 6).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Hook {
/// `NF_INET_PRE_ROUTING` — packet just arrived from a NIC, before the
/// routing decision. (Linux 7.1: `include/uapi/linux/netfilter.h:43`)
PreRouting,
/// `NF_INET_LOCAL_IN` — packet destined for this host, after the routing
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:44`)
InputLocal,
/// `NF_INET_FORWARD` — packet being forwarded between NICs. (Linux 7.1:
/// `include/uapi/linux/netfilter.h:45`)
Forward,
/// `NF_INET_LOCAL_OUT` — packet generated locally, before the routing
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:46`)
OutputLocal,
/// `NF_INET_POST_ROUTING` — packet leaving a NIC, after the routing
/// decision. (Linux 7.1: `include/uapi/linux/netfilter.h:47`)
PostRouting,
}
impl Hook {
/// Returns all hook points, in Linux's canonical order (matches
/// `enum nf_inet_hooks` ordering).
pub const ALL: [Hook; 5] = [
Hook::PreRouting,
Hook::InputLocal,
Hook::Forward,
Hook::OutputLocal,
Hook::PostRouting,
];
/// Returns the kernel-style numeric id (matches `NF_INET_*` constants).
pub const fn as_u32(self) -> u32 {
match self {
Hook::PreRouting => 0,
Hook::InputLocal => 1,
Hook::Forward => 2,
Hook::OutputLocal => 3,
Hook::PostRouting => 4,
}
}
/// Inverse of [`Self::as_u32`]. Returns `None` for unknown ids.
pub const fn from_u32(id: u32) -> Option<Hook> {
match id {
0 => Some(Hook::PreRouting),
1 => Some(Hook::InputLocal),
2 => Some(Hook::Forward),
3 => Some(Hook::OutputLocal),
4 => Some(Hook::PostRouting),
_ => None,
}
}
/// Short lowercase name used in the scheme interface (e.g. "input",
/// "output"). Mirrors the chain name conventions of iptables/nftables.
pub const fn name(self) -> &'static str {
match self {
Hook::PreRouting => "prerouting",
Hook::InputLocal => "input",
Hook::Forward => "forward",
Hook::OutputLocal => "output",
Hook::PostRouting => "postrouting",
}
}
}
/// The decision returned by the filter engine after evaluating rules.
///
/// Mirrors the verdict constants in `include/uapi/linux/netfilter.h`:
/// - [`Accept`] = `NF_ACCEPT` (= 1)
/// - [`Drop`] = `NF_DROP` (= 0)
///
/// Linux's `NF_QUEUE` (= 3) and `NF_STOLEN` (= 2) are not supported in this
/// revision (no userspace packet queue and no socket ownership transfer).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Verdict {
/// `NF_ACCEPT` — continue normal processing.
Accept,
/// `NF_DROP` — silently discard the packet.
Drop,
/// Log the packet details and continue evaluating the next rule.
/// Mirrors iptables `-j LOG --log-prefix "..."`.
Log,
/// Send ICMP Destination Unreachable (port) back to the source.
/// Mirrors iptables `-j REJECT --reject-with icmp-port-unreachable`.
Reject,
}
impl Verdict {
pub const fn as_u32(self) -> u32 {
match self {
Verdict::Accept => 1,
Verdict::Drop => 0,
Verdict::Log => 2,
Verdict::Reject => 3,
}
}
pub const fn from_u32(id: u32) -> Option<Verdict> {
match id {
1 => Some(Verdict::Accept),
0 => Some(Verdict::Drop),
2 => Some(Verdict::Log),
3 => Some(Verdict::Reject),
_ => None,
}
}
pub const fn name(self) -> &'static str {
match self {
Verdict::Accept => "ACCEPT",
Verdict::Drop => "DROP",
Verdict::Log => "LOG",
Verdict::Reject => "REJECT",
}
}
}
/// The context passed to the filter engine for evaluation.
///
/// Mirrors `struct nf_hook_state` (`include/linux/netfilter.h:78`):
/// - `hook` ↔ `state->hook`
/// - `in_dev` ↔ `state->in`
/// - `out_dev` ↔ `state->out`
/// - `protocol` ↔ the L4 protocol number (extracted from the IP header)
///
/// `skb` (the buffer) is split into its derived fields here for ergonomic
/// matching without forcing each rule to re-parse the packet.
#[derive(Debug, Clone)]
pub struct PacketContext<'a> {
pub hook: Hook,
pub in_dev: Option<alloc::rc::Rc<str>>,
pub out_dev: Option<alloc::rc::Rc<str>>,
pub src_addr: smoltcp::wire::IpAddress,
pub dst_addr: smoltcp::wire::IpAddress,
pub protocol: u8,
pub src_port: Option<u16>,
pub dst_port: Option<u16>,
pub packet: &'a [u8],
}
extern crate alloc;
+361
View File
@@ -0,0 +1,361 @@
//! Network Address Translation — mirrors Linux 7.1's `nf_nat` subsystem.
//!
//! Reference files:
//! - `net/netfilter/nf_nat_core.c` — `nf_nat_packet()`, `nf_nat_setup_info()`
//! - `include/net/netfilter/nf_nat.h` — `struct nf_conn_nat`
//! - `net/netfilter/nf_nat_proto_tcp.c` — TCP checksum adjustment after NAT
//! - `net/netfilter/nf_nat_proto_udp.c` — UDP checksum adjustment after NAT
//!
//! Two NAT types are supported:
//! - **SNAT** (Source NAT): changes the source IP of outgoing packets.
//! Applied in the OUTPUT/POSTROUTING path. Mirrors `nf_nat_masquerade.c`.
//! - **DNAT** (Destination NAT): changes the destination IP of incoming
//! packets. Applied in the INPUT/PREROUTING path. Used for port forwarding.
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec::Vec;
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
use super::{Hook, PacketContext};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NatType {
Snat,
Dnat,
}
#[derive(Debug, Clone)]
pub struct NatRule {
pub id: u32,
pub nat_type: NatType,
pub hook: Hook,
pub src_match: Option<IpAddress>,
pub dst_match: Option<IpAddress>,
pub trans_addr: IpAddress,
pub trans_port: Option<u16>,
pub match_count: u64,
}
#[derive(Debug, Clone)]
pub struct NatBinding {
pub orig_src: IpAddress,
pub trans_src: IpAddress,
pub orig_dst: IpAddress,
pub trans_dst: IpAddress,
pub orig_sport: u16,
pub trans_sport: u16,
pub orig_dport: u16,
pub trans_dport: u16,
}
impl Default for NatBinding {
fn default() -> Self {
Self {
orig_src: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
trans_src: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
orig_dst: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
trans_dst: IpAddress::Ipv4(Ipv4Address::UNSPECIFIED),
orig_sport: 0,
trans_sport: 0,
orig_dport: 0,
trans_dport: 0,
}
}
}
#[derive(Debug)]
pub struct NatTable {
pub rules: Vec<NatRule>,
next_id: u32,
ephemeral_port: u16,
}
impl NatTable {
pub fn new() -> Self {
Self {
rules: Vec::new(),
next_id: 1,
ephemeral_port: 40000,
}
}
pub fn add(&mut self, mut rule: NatRule) -> u32 {
rule.id = self.next_id;
self.next_id = self.next_id.saturating_add(1);
let id = rule.id;
self.rules.push(rule);
id
}
pub fn lookup_snat(
&self,
hook: Hook,
src: IpAddress,
_dst: IpAddress,
) -> Option<(IpAddress, Option<u16>)> {
for rule in self.rules.iter().filter(|r| {
r.nat_type == NatType::Snat && r.hook == hook
}) {
if let Some(m) = rule.src_match {
if m != src {
continue;
}
}
return Some((rule.trans_addr, rule.trans_port));
}
if hook == Hook::OutputLocal || hook == Hook::PostRouting {
for rule in self.rules.iter().filter(|r| {
r.nat_type == NatType::Snat && r.src_match.is_none()
}) {
return Some((rule.trans_addr, rule.trans_port));
}
}
None
}
pub fn lookup_dnat(
&self,
hook: Hook,
_src: IpAddress,
dst: IpAddress,
) -> Option<(IpAddress, Option<u16>)> {
for rule in self.rules.iter().filter(|r| {
r.nat_type == NatType::Dnat && r.hook == hook
}) {
if let Some(m) = rule.dst_match {
if m != dst {
continue;
}
}
return Some((rule.trans_addr, rule.trans_port));
}
None
}
pub fn alloc_ephemeral_port(&mut self) -> u16 {
let port = self.ephemeral_port;
self.ephemeral_port = if self.ephemeral_port >= 65535 {
40000
} else {
self.ephemeral_port.saturating_add(1)
};
port
}
pub fn format(&self) -> String {
let mut out = String::from("Nat table:\n");
for rule in &self.rules {
out.push_str(&alloc::format!(
" [{:>3}] {:?} {:?} -> {} port={:?} matches={}\n",
rule.id,
rule.nat_type,
rule.hook,
rule.trans_addr,
rule.trans_port,
rule.match_count,
));
}
out
}
pub fn remove(&mut self, id: u32) -> bool {
if let Some(idx) = self.rules.iter().position(|r| r.id == id) {
self.rules.remove(idx);
true
} else {
false
}
}
}
/// Rewrites the source IP address in an IPv4 packet. Returns `true` on
/// success. Mirrors `nf_nat_ipv4_manip_pkt()` in
/// `net/ipv4/netfilter/nf_nat_l3proto_ipv4.c`.
pub fn rewrite_src_ipv4(packet: &mut [u8], new_src: Ipv4Address) -> bool {
if packet.len() < 20 {
return false;
}
let mut ipv4 = smoltcp::wire::Ipv4Packet::new_unchecked(packet);
ipv4.set_src_addr(new_src);
ipv4.fill_checksum();
true
}
/// Rewrites the destination IP address in an IPv4 packet.
pub fn rewrite_dst_ipv4(packet: &mut [u8], new_dst: Ipv4Address) -> bool {
if packet.len() < 20 {
return false;
}
let mut ipv4 = smoltcp::wire::Ipv4Packet::new_unchecked(packet);
ipv4.set_dst_addr(new_dst);
ipv4.fill_checksum();
true
}
/// Rewrites the TCP/UDP port in the transport header and recomputes the
/// checksum. Mirrors `nf_nat_proto_tcp.c` (`tcp_manip_pkt`) and
/// `nf_nat_proto_udp.c` (`udp_manip_pkt`).
pub fn rewrite_port_ipv4(
packet: &mut [u8],
old_port: u16,
new_port: u16,
old_addr: IpAddress,
new_addr: IpAddress,
is_src: bool,
protocol: u8,
) {
let ip_header_len: usize = 20;
if packet.len() < ip_header_len + 4 {
return;
}
let transport = &mut packet[ip_header_len..];
let (port_offset, addr_old, addr_new) = if is_src {
(0, old_addr, new_addr)
} else {
(2, old_addr, new_addr)
};
transport[port_offset] = (new_port >> 8) as u8;
transport[port_offset + 1] = (new_port & 0xff) as u8;
if protocol == 17 {
let old_csum = u16::from_be_bytes([transport[6], transport[7]]);
if old_csum != 0 {
transport[6] = 0;
transport[7] = 0;
}
} else {
transport[16] = 0;
transport[17] = 0;
}
recompute_transport_checksum(packet, protocol, addr_old, addr_new);
}
fn recompute_transport_checksum(
packet: &mut [u8],
protocol: u8,
_old_addr: IpAddress,
_new_addr: IpAddress,
) {
if packet.len() < 20 {
return;
}
let src_bytes: [u8; 4] = packet[12..16].try_into().unwrap_or([0; 4]);
let dst_bytes: [u8; 4] = packet[16..20].try_into().unwrap_or([0; 4]);
let src_addr = Ipv4Address::new(src_bytes[0], src_bytes[1], src_bytes[2], src_bytes[3]);
let dst_addr = Ipv4Address::new(dst_bytes[0], dst_bytes[1], dst_bytes[2], dst_bytes[3]);
let transport = &mut packet[20..];
match protocol {
6 => {
if transport.len() >= 20 {
let mut tcp = smoltcp::wire::TcpPacket::new_unchecked(transport);
tcp.fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr));
}
}
17 => {
if transport.len() >= 8 {
let mut udp = smoltcp::wire::UdpPacket::new_unchecked(transport);
udp.fill_checksum(&IpAddress::Ipv4(src_addr), &IpAddress::Ipv4(dst_addr));
}
}
_ => {}
}
}
pub fn parse_nat_rule(
line: &str,
) -> core::result::Result<NatRule, NatParseError> {
let mut tokens = line.split_whitespace();
let nat_type_str = tokens.next().ok_or(NatParseError::MissingField("nat type"))?;
let nat_type = match nat_type_str.to_uppercase().as_str() {
"SNAT" => NatType::Snat,
"DNAT" => NatType::Dnat,
_ => return Err(NatParseError::BadNatType(nat_type_str.to_string())),
};
let hook_str = tokens.next().ok_or(NatParseError::MissingField("hook"))?;
let hook = match hook_str.to_lowercase().as_str() {
"prerouting" => Hook::PreRouting,
"input" => Hook::InputLocal,
"forward" => Hook::Forward,
"output" => Hook::OutputLocal,
"postrouting" => Hook::PostRouting,
_ => return Err(NatParseError::BadHook(hook_str.to_string())),
};
let to_str = tokens.next();
if to_str != Some("to") {
return Err(NatParseError::MissingField("to"));
}
let addr_str = tokens.next().ok_or(NatParseError::MissingField("address"))?;
let trans_addr = if let Ok(v4) = Ipv4Address::from_str(addr_str) {
IpAddress::Ipv4(v4)
} else if let Ok(v6) = Ipv6Address::from_str(addr_str) {
IpAddress::Ipv6(v6)
} else {
return Err(NatParseError::BadAddress(addr_str.to_string()));
};
let mut trans_port = None;
let mut src_match = None;
let mut dst_match = None;
while let Some(token) = tokens.next() {
match token {
"port" => {
let p = tokens.next().ok_or(NatParseError::MissingField("port value"))?;
trans_port = Some(p.parse().map_err(|_| NatParseError::BadPort(p.to_string()))?);
}
"match-src" => {
let a = tokens.next().ok_or(NatParseError::MissingField("match-src value"))?;
src_match = Some(parse_addr(a)?);
}
"match-dst" => {
let a = tokens.next().ok_or(NatParseError::MissingField("match-dst value"))?;
dst_match = Some(parse_addr(a)?);
}
_ => {}
}
}
Ok(NatRule {
id: 0,
nat_type,
hook,
src_match,
dst_match,
trans_addr,
trans_port,
match_count: 0,
})
}
fn parse_addr(s: &str) -> core::result::Result<IpAddress, NatParseError> {
if let Ok(v4) = Ipv4Address::from_str(s) {
Ok(IpAddress::Ipv4(v4))
} else if let Ok(v6) = Ipv6Address::from_str(s) {
Ok(IpAddress::Ipv6(v6))
} else {
Err(NatParseError::BadAddress(s.to_string()))
}
}
use core::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NatParseError {
MissingField(&'static str),
BadNatType(String),
BadHook(String),
BadAddress(String),
BadPort(String),
}
+187
View File
@@ -0,0 +1,187 @@
//! Filter rule representation and packet matching.
//!
//! Mirrors the Linux 7.1 `ipt_entry` structure and the `xt_match` matching
//! framework:
//! - Linux `ipt_entry` lives in `net/ipv4/netfilter/ip_tables.h`
//! - Linux `xt_match` lives in `net/netfilter/x_tables.h`
//! - Linux `xt_action_param` (match context) lives in
//! `include/linux/netfilter/x_tables.h`
//!
//! In this revision the rule supports a fixed set of match fields that
//! cover the most common iptables/xt_matches: `src`/`dst` address+CIDR,
//! `protocol`, `sport`/`dport` (TCP/UDP only), and the `in`/`out`
//! interface. This is intentionally narrower than the full xtables match
//! set — extensions like `iprange`, `mac`, `conntrack`, `recent`, etc.
//! will be added in follow-up revisions as separate match kinds.
use alloc::rc::Rc;
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
use super::{ConnState, Hook, PacketContext, Verdict};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StateMatch {
New,
Established,
Related,
Invalid,
}
/// The IP protocol number used in a rule. Values mirror the IANA assigned
/// numbers (`include/uapi/linux/in.h`):
/// - 1 = `IPPROTO_ICMP`
/// - 6 = `IPPROTO_TCP`
/// - 17 = `IPPROTO_UDP`
/// - 58 = `IPPROTO_ICMPV6`
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Protocol(pub u8);
impl Protocol {
pub const ANY: Option<Protocol> = None;
pub const ICMP: Protocol = Protocol(1);
pub const TCP: Protocol = Protocol(6);
pub const UDP: Protocol = Protocol(17);
pub const ICMP6: Protocol = Protocol(58);
}
/// Outcome of matching a single rule against a packet.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatchResult {
/// Every field in the rule matched. The rule's verdict applies.
Match,
/// At least one field did not match. Continue to the next rule.
NoMatch,
}
/// A single filter rule. The fields mirror the iptables CLI options:
///
/// | Rust field | iptables equivalent |
/// |----------------------|----------------------------------------------|
/// | `src_addr`/`src_cidr`| `--source` / `--src-range` (with cidr_len) |
/// | `dst_addr`/`dst_cidr`| `--destination` / `--dst-range` |
/// | `protocol` | `--protocol` |
/// | `src_port` | `--source-port` (TCP/UDP only) |
/// | `dst_port` | `--destination-port` |
/// | `in_dev` | `--in-interface` |
/// | `out_dev` | `--out-interface` |
/// | `verdict` | `--jump` (ACCEPT or DROP) |
///
/// `None` in any of these fields means "match any" (a wildcard), mirroring
/// iptables behaviour where omitting `--source` matches every source.
#[derive(Debug, Clone)]
pub struct FilterRule {
pub id: u32,
pub hook: Hook,
pub src_addr: Option<IpAddress>,
pub src_prefix_len: u8,
pub dst_addr: Option<IpAddress>,
pub dst_prefix_len: u8,
pub protocol: Option<Protocol>,
pub src_port: Option<u16>,
pub dst_port: Option<u16>,
pub in_dev: Option<Rc<str>>,
pub out_dev: Option<Rc<str>>,
pub state_match: Option<StateMatch>,
pub verdict: Verdict,
pub match_count: u64,
}
impl FilterRule {
/// Returns true if this rule applies to the given hook point. Mirrors
/// `ipt_entry->comefrom` semantics: a rule attached to `Hook::InputLocal`
/// only fires when the engine evaluates the INPUT chain.
pub fn applies_to(&self, hook: Hook) -> bool {
self.hook == hook
}
/// Evaluates this rule against a packet. Mirrors
/// `xt_action_param`-based matching in `x_tables.c` (`xt_check_match`).
pub fn matches(&self, ctx: &PacketContext<'_>) -> MatchResult {
if !self.applies_to(ctx.hook) {
return MatchResult::NoMatch;
}
if let Some(src) = self.src_addr {
if !addr_in_cidr(src, self.src_prefix_len, ctx.src_addr) {
return MatchResult::NoMatch;
}
}
if let Some(dst) = self.dst_addr {
if !addr_in_cidr(dst, self.dst_prefix_len, ctx.dst_addr) {
return MatchResult::NoMatch;
}
}
if let Some(proto) = self.protocol {
if proto.0 != ctx.protocol {
return MatchResult::NoMatch;
}
}
if let Some(sport) = self.src_port {
if ctx.src_port != Some(sport) {
return MatchResult::NoMatch;
}
}
if let Some(dport) = self.dst_port {
if ctx.dst_port != Some(dport) {
return MatchResult::NoMatch;
}
}
if let Some(in_dev) = &self.in_dev {
match &ctx.in_dev {
Some(ctx_dev) if ctx_dev.as_ref() == in_dev.as_ref() => {}
_ => return MatchResult::NoMatch,
}
}
if let Some(out_dev) = &self.out_dev {
match &ctx.out_dev {
Some(ctx_dev) if ctx_dev.as_ref() == out_dev.as_ref() => {}
_ => return MatchResult::NoMatch,
}
}
MatchResult::Match
}
}
/// Returns true if `addr` falls inside the prefix `network/prefix_len`.
/// Mirrors `ip_masked_match` (`net/ipv4/netfilter/ipt_addr.c`) — IPv4
/// uses the simple 32-bit mask, IPv6 uses the 128-bit bitwise mask.
fn addr_in_cidr(network: IpAddress, prefix_len: u8, addr: IpAddress) -> bool {
match (network, addr) {
(IpAddress::Ipv4(net), IpAddress::Ipv4(a)) => {
if prefix_len == 0 {
return true;
}
if prefix_len > 32 {
return false;
}
let mask: u32 = if prefix_len == 32 {
u32::MAX
} else {
!((1u32 << (32 - prefix_len)) - 1)
};
(u32::from(net) & mask) == (u32::from(a) & mask)
}
(IpAddress::Ipv6(net), IpAddress::Ipv6(a)) => {
if prefix_len > 128 {
return false;
}
let net_bytes = net.octets();
let a_bytes = a.octets();
let full_bytes = (prefix_len / 8) as usize;
let remainder = prefix_len % 8;
if net_bytes[..full_bytes] != a_bytes[..full_bytes] {
return false;
}
if remainder > 0 && full_bytes < 16 {
let mask = 0xff << (8 - remainder);
(net_bytes[full_bytes] & mask) == (a_bytes[full_bytes] & mask)
} else {
true
}
}
(IpAddress::Ipv4(_), IpAddress::Ipv6(_)) | (IpAddress::Ipv6(_), IpAddress::Ipv4(_)) => {
false
}
}
}
extern crate alloc;
+384
View File
@@ -0,0 +1,384 @@
//! Filter table: a collection of rules and the per-chain default policies.
//!
//! Mirrors Linux 7.1's `xt_table` (`include/linux/netfilter/x_tables.h`):
//! - `valid_hooks` (bitmask) ↔ `FilterTable::enabled_hooks` (set)
//! - `entries` (rule blob) ↔ `FilterTable::rules`
//! - Per-hook default policy ↔ `FilterTable::default_policy`
//!
//! The parser accepts an iptables-style textual rule. The grammar is a
//! deliberate subset of iptables that covers the most common cases and
//! stays readable:
//!
//! ```text
//! ACTION CHAIN [-i IFACE] [-o IFACE] [-p PROTO] [-s ADDR[/LEN]] [-d ADDR[/LEN]]
//! [--sport PORT] [--dport PORT]
//! ```
//!
//! Examples:
//! - `ACCEPT input -p tcp --dport 80`
//! - `DROP input -s 10.0.0.0/8`
//! - `ACCEPT output -d 192.168.1.0/24 --sport 1024:65535`
extern crate alloc;
use alloc::collections::BTreeMap;
use alloc::rc::Rc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
use core::str::FromStr;
use smoltcp::time::Instant;
use smoltcp::wire::{IpAddress, Ipv4Address, Ipv6Address};
use super::conntrack::{ConnState, ConntrackTable};
use super::nat::{NatRule, NatTable};
use super::rule::{FilterRule, Protocol, StateMatch};
use super::{Hook, PacketContext, Verdict};
/// A table of filter rules plus per-chain default policies.
///
/// In iptables terminology this is one "table" (the `filter` table).
/// We currently expose exactly one table; future extensions can add
/// `nat`, `mangle`, `raw` tables following the same model.
pub struct FilterTable {
pub rules: Vec<FilterRule>,
pub default_policy: BTreeMap<Hook, Verdict>,
pub next_id: u32,
pub conntrack: Option<ConntrackTable>,
pub nat_table: NatTable,
pub chain_counters: BTreeMap<Hook, (u64, u64)>,
pub log_buffer: Vec<String>,
}
impl FilterTable {
pub fn new() -> Self {
let mut default_policy = BTreeMap::new();
for &hook in &Hook::ALL {
default_policy.insert(hook, Verdict::Accept);
}
Self {
rules: Vec::new(),
default_policy,
next_id: 1,
conntrack: Some(ConntrackTable::new()),
nat_table: NatTable::new(),
chain_counters: BTreeMap::new(),
log_buffer: Vec::new(),
}
}
/// Evaluates the rules attached to `ctx.hook` in order, returning the
/// first matching verdict. If no rule matches, the chain's default
/// policy applies. Mirrors `nf_iterate` + `ipt_do_table` in
/// `net/ipv4/netfilter/ip_tables.c`.
pub fn evaluate(&mut self, ctx: &PacketContext<'_>, now: Instant) -> Verdict {
let conn_state = self.conntrack.as_mut().map(|ct| ct.track(ctx, now));
if conn_state == Some(ConnState::OverLimit) {
return Verdict::Drop;
}
let counter = self.chain_counters.entry(ctx.hook).or_insert((0, 0));
counter.0 = counter.0.saturating_add(1);
counter.1 = counter.1.saturating_add(ctx.packet.len() as u64);
let mut final_verdict: Option<Verdict> = None;
for rule in self.rules.iter_mut() {
if rule.state_match.is_some() {
match (rule.state_match, conn_state) {
(Some(StateMatch::Established), Some(ConnState::Established)) => {}
(Some(StateMatch::New), Some(ConnState::New)) => {}
(Some(StateMatch::Invalid), _) => continue,
(Some(StateMatch::Related), _) => continue,
(Some(_), _) => continue,
_ => {}
}
}
if rule.applies_to(ctx.hook) && rule.matches(ctx) == super::rule::MatchResult::Match {
rule.match_count = rule.match_count.saturating_add(1);
match rule.verdict {
Verdict::Log => {
let msg = alloc::format!(
"{} IN={} OUT={} SRC={} DST={} PROTO={} SPORT={} DPORT={}",
rule.id,
ctx.in_dev.as_deref().unwrap_or("-"),
ctx.out_dev.as_deref().unwrap_or("-"),
ctx.src_addr,
ctx.dst_addr,
ctx.protocol,
ctx.src_port.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
ctx.dst_port.map(|p| p.to_string()).unwrap_or_else(|| "-".into()),
);
if self.log_buffer.len() >= 100 {
self.log_buffer.remove(0);
}
self.log_buffer.push(msg);
continue;
}
Verdict::Reject => {
final_verdict = Some(Verdict::Reject);
break;
}
v => {
final_verdict = Some(v);
break;
}
}
}
}
self.default_policy
.get(&ctx.hook)
.copied()
.unwrap_or(Verdict::Accept)
}
/// Inserts a rule. The rule's `id` is overwritten with a fresh id from
/// `next_id`.
pub fn add(&mut self, mut rule: FilterRule) -> u32 {
rule.id = self.next_id;
self.next_id = self.next_id.saturating_add(1);
let id = rule.id;
self.rules.push(rule);
id
}
/// Removes the rule with the given id. Returns `true` if a rule was
/// removed.
pub fn remove(&mut self, id: u32) -> bool {
if let Some(idx) = self.rules.iter().position(|r| r.id == id) {
self.rules.remove(idx);
true
} else {
false
}
}
/// Renders the entire table as a human-readable text dump. Mirrors
/// `iptables -L -n -v` output format.
pub fn format(&self) -> String {
let mut out = String::new();
for &hook in &Hook::ALL {
let policy = self
.default_policy
.get(&hook)
.copied()
.unwrap_or(Verdict::Accept);
out.push_str(&alloc::format!(
"Chain {} (policy {})\n",
hook.name().to_uppercase(),
policy.name()
));
for rule in self.rules.iter().filter(|r| r.hook == hook) {
out.push_str(" ");
out.push_str(&format_rule(rule));
out.push('\n');
}
}
out
}
pub fn set_default_policy(&mut self, hook: Hook, verdict: Verdict) {
self.default_policy.insert(hook, verdict);
}
}
fn format_rule(rule: &FilterRule) -> String {
let mut out = alloc::format!("{} [{:>4}]", rule.verdict.name(), rule.id);
if let Some(iface) = &rule.in_dev {
out.push_str(&alloc::format!(" in={}", iface));
}
if let Some(iface) = &rule.out_dev {
out.push_str(&alloc::format!(" out={}", iface));
}
if let Some(src) = rule.src_addr {
out.push_str(&alloc::format!(" src={}/{}", src, rule.src_prefix_len));
}
if let Some(dst) = rule.dst_addr {
out.push_str(&alloc::format!(" dst={}/{}", dst, rule.dst_prefix_len));
}
if let Some(proto) = rule.protocol {
out.push_str(&alloc::format!(" proto={}", proto.0));
}
if let Some(sport) = rule.src_port {
out.push_str(&alloc::format!(" sport={}", sport));
}
if let Some(dport) = rule.dst_port {
out.push_str(&alloc::format!(" dport={}", dport));
}
out.push_str(&alloc::format!(" matches={}", rule.match_count));
out
}
/// Errors returned by the rule parser. Each variant includes enough
/// context to point the user at the offending token (the index into the
/// input line where parsing failed).
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
/// Generic parse error with a human-readable message.
Msg(&'static str),
/// The input did not contain a recognizable chain name.
UnknownHook(String),
/// The action keyword was not ACCEPT or DROP.
UnknownVerdict(String),
/// An address failed to parse.
BadAddress(String),
/// A port number failed to parse.
BadPort(String),
/// A flag was recognized but its value was malformed.
BadFlagValue { flag: &'static str, value: String },
/// The protocol name was not tcp/udp/icmp/icmp6 or a number.
UnknownProtocol(String),
/// The interface name was missing or empty.
MissingInterface,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseError::Msg(m) => f.write_str(m),
ParseError::UnknownHook(s) => write!(f, "unknown chain: {}", s),
ParseError::UnknownVerdict(s) => write!(f, "unknown verdict: {}", s),
ParseError::BadAddress(s) => write!(f, "bad address: {}", s),
ParseError::BadPort(s) => write!(f, "bad port: {}", s),
ParseError::BadFlagValue { flag, value } => {
write!(f, "bad value for {}: {}", flag, value)
}
ParseError::UnknownProtocol(s) => write!(f, "unknown protocol: {}", s),
ParseError::MissingInterface => f.write_str("missing interface name"),
}
}
}
/// Parses one iptables-style textual rule into a [`FilterRule`]. The
/// grammar is documented at the top of this file.
pub fn parse_rule(line: &str) -> core::result::Result<FilterRule, ParseError> {
let mut tokens = line.split_whitespace();
let verdict_token = tokens.next().ok_or(ParseError::Msg("missing verdict"))?;
let verdict = match verdict_token.to_uppercase().as_str() {
"ACCEPT" => Verdict::Accept,
"DROP" => Verdict::Drop,
"LOG" => Verdict::Log,
"REJECT" => Verdict::Reject,
_ => return Err(ParseError::UnknownVerdict(verdict_token.to_string())),
};
let chain_token = tokens.next().ok_or(ParseError::Msg("missing chain"))?;
let hook = match chain_token.to_lowercase().as_str() {
"prerouting" => Hook::PreRouting,
"input" => Hook::InputLocal,
"forward" => Hook::Forward,
"output" => Hook::OutputLocal,
"postrouting" => Hook::PostRouting,
_ => return Err(ParseError::UnknownHook(chain_token.to_string())),
};
let mut rule = FilterRule {
id: 0,
hook,
src_addr: None,
src_prefix_len: 0,
dst_addr: None,
dst_prefix_len: 0,
protocol: None,
src_port: None,
dst_port: None,
in_dev: None,
out_dev: None,
state_match: None,
verdict,
match_count: 0,
};
while let Some(flag) = tokens.next() {
match flag {
"-i" => {
let name = tokens.next().ok_or(ParseError::MissingInterface)?;
rule.in_dev = Some(Rc::from(name));
}
"-o" => {
let name = tokens.next().ok_or(ParseError::MissingInterface)?;
rule.out_dev = Some(Rc::from(name));
}
"-s" => {
let value = tokens.next().ok_or(ParseError::Msg("missing -s value"))?;
let (addr, prefix) = parse_addr_with_prefix(value)?;
rule.src_addr = Some(addr);
rule.src_prefix_len = prefix;
}
"-d" => {
let value = tokens.next().ok_or(ParseError::Msg("missing -d value"))?;
let (addr, prefix) = parse_addr_with_prefix(value)?;
rule.dst_addr = Some(addr);
rule.dst_prefix_len = prefix;
}
"-p" => {
let value = tokens.next().ok_or(ParseError::Msg("missing -p value"))?;
let proto = parse_protocol(value)?;
rule.protocol = Some(proto);
}
"--sport" | "--source-port" => {
let value = tokens.next().ok_or(ParseError::Msg("missing --sport value"))?;
rule.src_port = Some(parse_port(value)?);
}
"--dport" | "--destination-port" => {
let value = tokens.next().ok_or(ParseError::Msg("missing --dport value"))?;
rule.dst_port = Some(parse_port(value)?);
}
"state" => {
let value = tokens.next().ok_or(ParseError::Msg("missing state value"))?;
rule.state_match = Some(match value {
"new" => StateMatch::New,
"established" => StateMatch::Established,
"related" => StateMatch::Related,
"invalid" => StateMatch::Invalid,
_ => return Err(ParseError::Msg("unknown state value")),
});
}
_ => return Err(ParseError::Msg("unknown flag")),
}
}
if rule.protocol.is_some() && (rule.src_port.is_some() || rule.dst_port.is_some()) {
if let Some(proto) = rule.protocol {
if proto != Protocol::TCP && proto != Protocol::UDP && proto != Protocol::ICMP6 {
return Err(ParseError::Msg("port specified for non-transport protocol"));
}
}
}
Ok(rule)
}
fn parse_addr_with_prefix(value: &str) -> core::result::Result<(IpAddress, u8), ParseError> {
let (addr_str, prefix_len) = match value.split_once('/') {
Some((a, p)) => (a, p.parse::<u8>().map_err(|_| ParseError::BadAddress(value.to_string()))?),
None => (value, 0),
};
if let Ok(v4) = Ipv4Address::from_str(addr_str) {
let prefix = if prefix_len == 0 { 32 } else { prefix_len };
Ok((IpAddress::Ipv4(v4), prefix))
} else if let Ok(v6) = Ipv6Address::from_str(addr_str) {
let prefix = if prefix_len == 0 { 128 } else { prefix_len };
Ok((IpAddress::Ipv6(v6), prefix))
} else {
Err(ParseError::BadAddress(value.to_string()))
}
}
fn parse_protocol(value: &str) -> core::result::Result<Protocol, ParseError> {
let lower = value.to_lowercase();
let num: Option<u8> = lower.parse().ok();
match (lower.as_str(), num) {
("tcp", _) => Ok(Protocol::TCP),
("udp", _) => Ok(Protocol::UDP),
("icmp", _) => Ok(Protocol::ICMP),
("icmp6", _) | ("icmpv6", _) => Ok(Protocol::ICMP6),
(_, Some(n)) => Ok(Protocol(n)),
_ => Err(ParseError::UnknownProtocol(value.to_string())),
}
}
fn parse_port(value: &str) -> core::result::Result<u16, ParseError> {
let port_str = value.split(':').next().unwrap_or(value);
port_str
.parse::<u16>()
.map_err(|_| ParseError::BadPort(value.to_string()))
}
+116
View File
@@ -0,0 +1,116 @@
//! 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.version() & 0x0f) as usize * 4;
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.version() & 0x0f) as usize * 4;
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;
+169
View File
@@ -0,0 +1,169 @@
//! 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;
#[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
}
}
+190
View File
@@ -0,0 +1,190 @@
//! 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::LinkDevice;
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>>,
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()),
recv_buffer: Vec::with_capacity(1500),
ip_address: None,
}
}
pub fn add_port<T: LinkDevice + 'static>(&self, dev: T) {
self.ports.borrow_mut().push(Box::new(dev));
}
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;
}
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 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);
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 {
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 {
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" }
}
}
+490 -42
View File
@@ -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,59 @@ 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,
stats: Stats,
}
impl EthernetLink {
@@ -53,6 +97,7 @@ impl EthernetLink {
const NEIGHBOR_LIVE_TIME: Duration = Duration::from_secs(60);
const ARP_SILENCE_TIME: Duration = Duration::from_secs(1);
const NDP_SILENCE_TIME: Duration = Duration::from_secs(1);
pub fn new(name: &str, network_file: File) -> Self {
let waiting_packets = PacketBuffer::new(
@@ -69,6 +114,10 @@ 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(),
stats: Stats::default(),
neighbor_cache: Default::default(),
}
}
@@ -107,7 +156,7 @@ impl EthernetLink {
return;
};
let Some(ip_addr) = self.ip_address else {
let Some(IpCidr::Ipv4(ip_addr)) = self.ip_address else {
return;
};
@@ -194,6 +243,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 +257,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 +280,10 @@ impl EthernetLink {
return;
}
Ok((IpAddress::Ipv6(_), _)) => {
let _ = self.waiting_packets.dequeue();
continue;
}
Err(_) => {
self.arp_state = ArpState::Discovered;
return;
@@ -241,6 +298,71 @@ 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();
continue;
}
Err(_) => {
self.ndp_state = NdpState::Discovered;
return;
}
}
let _ = self.waiting_packets.dequeue();
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 {
warn!(
@@ -251,15 +373,27 @@ 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)
}
}
}
}
@@ -268,7 +402,7 @@ impl EthernetLink {
return;
};
let Some(ip_address) = self.ip_address else {
let Some(IpCidr::Ipv4(ip_address)) = self.ip_address else {
return;
};
@@ -303,38 +437,302 @@ 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;
};
let (target, mut tries, mut silent_until) = match self.ndp_state {
NdpState::Discovered => return,
NdpState::Discovering {
target,
tries,
silent_until,
} => (target, tries, silent_until),
};
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();
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 {
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.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.handle_missing_neighbor(next_hop, &packet, now)
} else {
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 +750,58 @@ 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);
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 {
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 +821,40 @@ 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 statistics(&self) -> Stats {
self.stats
}
fn link_state(&self) -> &'static str {
if self.hardware_address.is_some() { "up" } else { "down" }
}
}
+190
View File
@@ -0,0 +1,190 @@
//! 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::collections::VecDeque;
use std::rc::Rc;
use smoltcp::time::Instant;
use smoltcp::wire::{
EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol,
};
use super::LinkDevice;
const GRE_PROTO_IPV4: u16 = 0x0800;
const GRE_FLAG_KEY: u16 = 0x2000;
pub struct GreDevice {
name: Rc<str>,
parent_name: Rc<str>,
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>,
) -> 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(),
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();
self.push_received(encapsulated);
let _ = now;
}
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);
}
}
+125
View File
@@ -0,0 +1,125 @@
//! 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::collections::VecDeque;
use std::rc::Rc;
use smoltcp::time::Instant;
use smoltcp::wire::{
EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv4Packet, Ipv4Repr, IpProtocol,
};
use super::LinkDevice;
const IPIP_PROTO: u8 = 4;
pub struct IpipDevice {
name: Rc<str>,
parent_name: Rc<str>,
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) -> Self {
Self {
name: name.into(),
parent_name: parent_name.into(),
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();
self.push_received(encapsulated);
let _ = now;
}
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);
}
}
+1 -3
View File
@@ -57,7 +57,5 @@ 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) {}
}
+34 -1
View File
@@ -1,6 +1,13 @@
pub mod bond;
pub mod bridge;
pub mod ethernet;
pub mod gre;
pub mod ipip;
pub mod loopback;
pub mod stp;
pub mod qdisc;
pub mod tun;
pub mod vlan;
pub mod vxlan;
use std::rc::Rc;
@@ -30,6 +37,32 @@ 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 mtu(&self) -> usize {
1500
}
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,
}
#[derive(Default)]
+157
View File
@@ -0,0 +1,157 @@
//! 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 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 {
let token_add = (self.rate * 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,
}
}
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()
}
}
}
}
+73
View File
@@ -0,0 +1,73 @@
//! 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;
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>,
}
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,
}
}
}
impl LinkDevice for TunDevice {
fn send(&mut self, _next_hop: IpAddress, packet: &[u8], _now: Instant) {
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()?;
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);
}
}
+164
View File
@@ -0,0 +1,164 @@
//! 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::collections::VecDeque;
use std::rc::Rc;
use smoltcp::time::Instant;
use smoltcp::wire::{
EthernetAddress, EthernetFrame, EthernetProtocol, IpAddress, IpCidr,
};
use super::LinkDevice;
const TPID_8021Q: u16 = 0x8100;
pub struct VlanDevice {
name: Rc<str>,
parent_name: Rc<str>,
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) -> 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(),
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();
self.push_received(tagged);
let _ = (next_hop, now);
}
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);
}
}
+213
View File
@@ -0,0 +1,213 @@
//! 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::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::LinkDevice;
const VXLAN_PORT: u16 = 4789;
const VXLAN_FLAGS: u8 = 0x08;
pub struct VxlanDevice {
name: Rc<str>,
parent_name: Rc<str>,
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,
) -> Self {
let vni_bytes = vni.to_be_bytes();
Self {
name: name.into(),
parent_name: parent_name.into(),
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(&eth) 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();
self.push_received(enc);
let _ = now;
}
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);
}
}
+23 -1
View File
@@ -13,11 +13,14 @@ use smoltcp::wire::EthernetAddress;
mod buffer_pool;
mod error;
mod filter;
mod icmp_error;
mod link;
mod logger;
mod port_set;
mod router;
mod scheme;
mod slaac;
fn get_network_adapter() -> Result<String> {
use std::fs;
@@ -82,6 +85,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 +105,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
TcpScheme,
IcmpScheme,
NetcfgScheme,
NetfilterScheme,
}
}
@@ -146,6 +158,14 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
)
.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))?;
let mut smolnetd = Smolnetd::new(
network_fd,
hardware_addr,
@@ -155,6 +175,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
icmp_fd,
time_fd,
netcfg_fd,
netfilter_fd,
)
.context("smolnetd: failed to initialize smolnetd")?;
@@ -162,7 +183,7 @@ fn run(daemon: daemon::Daemon) -> Result<()> {
let all = {
use EventSource::*;
[Network, Time, IpScheme, UdpScheme, IcmpScheme, NetcfgScheme].map(Ok)
[Network, Time, IpScheme, UdpScheme, IcmpScheme, NetcfgScheme, NetfilterScheme].map(Ok)
};
for event_res in all
@@ -177,6 +198,7 @@ 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(),
}
.map_err(|e| error!("Received packet error: {:?}", e));
}
+5
View File
@@ -47,6 +47,11 @@ impl PortSet {
}
}
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;
}
+349 -23
View File
@@ -4,9 +4,11 @@ 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 crate::filter::{FilterTable, Hook, PacketContext, Verdict};
use crate::icmp_error;
use crate::link::DeviceList;
use crate::scheme::Smolnetd;
@@ -49,6 +51,170 @@ 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) {
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: Option<(Rc<str>, Option<IpAddress>)> = self.route_table.borrow()
.lookup_rule(&dst)
.map(|r| (r.dev.clone(), r.via));
let Some((dev_name, _via)) = route_info else {
local.push(packet.to_vec());
continue;
};
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, _)) = 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);
}
}
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 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 +238,104 @@ 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() {
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) = {
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())
};
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) = {
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())
};
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.into_inner(), now);
dev.send(next_hop, &packet, now);
}
version => {
debug!("Dropped packet with unknown IP version: {}", version);
}
}
}
@@ -188,3 +417,100 @@ 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
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))
}
+40 -1
View File
@@ -33,19 +33,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 +76,8 @@ pub struct Smolnetd {
tcp_scheme: TcpScheme,
icmp_scheme: IcmpScheme,
netcfg_scheme: NetCfgScheme,
netfilter_scheme: NetFilterScheme,
filter_table: FilterTableRef,
}
impl Smolnetd {
@@ -87,11 +95,13 @@ impl Smolnetd {
icmp_file: Socket,
time_file: Fd,
netcfg_file: Socket,
netfilter_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())
@@ -114,6 +124,7 @@ impl Smolnetd {
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,6 +133,12 @@ impl Smolnetd {
Rc::clone(loopback.name()),
"127.0.0.1".parse().unwrap(),
));
route_table.borrow_mut().insert_rule(Rule::new(
"::1/128".parse().unwrap(),
None,
Rc::clone(loopback.name()),
"::1".parse().unwrap(),
));
let mut eth0 = EthernetLink::new("eth0", unsafe {
File::from_raw_fd(network_file.into_raw() as RawFd)
@@ -172,6 +189,11 @@ impl Smolnetd {
Rc::clone(&devices),
Rc::clone(&socket_set),
)?,
netfilter_scheme: NetFilterScheme::new(
netfilter_file,
Rc::clone(&filter_table),
)?,
filter_table,
})
}
@@ -216,6 +238,11 @@ impl Smolnetd {
Ok(())
}
pub fn on_netfilter_scheme_event(&mut self) -> Result<()> {
self.netfilter_scheme.on_scheme_event()?;
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>() {
@@ -249,10 +276,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) {
+141 -5
View File
@@ -7,7 +7,7 @@ use redox_scheme::{
CallerCtx, OpenResult, RequestKind, SignalBehavior, Socket,
};
use scheme_utils::HandleMap;
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address};
use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr, Ipv4Address, Ipv6Address};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::mem;
@@ -35,6 +35,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))?;
@@ -109,6 +138,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" => {
@@ -240,8 +298,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());
@@ -257,8 +314,7 @@ 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");
@@ -267,7 +323,85 @@ fn mk_root_node(
Ok(())
}
},
},
"arp" => {
"list" => {
ro [devices] || {
match devices.borrow().get("eth0") {
Some(dev) => dev.arp_table(),
None => "Device not found\n".into(),
}
}
},
"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(())
}
},
},
"stats" => {
ro [devices] || {
match devices.borrow().get("eth0") {
Some(dev) => {
let s = dev.statistics();
format!("rx_bytes={} rx_packets={} tx_bytes={} tx_packets={}\n",
s.rx_bytes, s.rx_packets, s.tx_bytes, s.tx_packets)
}
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" => {
ro [devices] || {
match devices.borrow().get("eth0") {
Some(dev) => format!("{}\n", dev.mtu()),
None => "Device not found\n".into(),
}
}
}
},
"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={}\n",
s.rx_bytes, s.rx_packets, s.tx_bytes, s.tx_packets)
}
None => "Device not found\n".into(),
}
}
},
}
}
}
@@ -275,6 +409,7 @@ fn mk_root_node(
struct DNSConfig {
name_server: Ipv4Address,
name_server6: Option<Ipv6Address>,
}
type DNSConfigRef = Rc<RefCell<DNSConfig>>;
@@ -352,6 +487,7 @@ impl 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,
+408
View File
@@ -0,0 +1,408 @@
//! 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, 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,
),
["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),
_ => 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 let Some(rest) = path.strip_prefix("/rule/del/") {
let id: u32 = rest
.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 if path == "/reset" {
*table = FilterTable::new();
log::info!("netfilter: table reset to defaults");
} 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))
}
}
}
+24 -7
View File
@@ -251,17 +251,24 @@ where
flags: usize,
) -> SyscallResult<usize>;
fn get_sock_opt(
fn get_sock_opt(
&self,
file: &SchemeFile<Self>,
name: usize,
buf: &mut [u8],
) -> SyscallResult<usize> {
// Return Err for default implementation
Err(SyscallError::new(syscall::ENOPROTOOPT))
}
fn set_sock_opt(
&mut self,
_file: &SchemeFile<Self>,
_name: usize,
_buf: &[u8],
) -> SyscallResult<usize> {
Err(SyscallError::new(syscall::ENOPROTOOPT))
}
}
pub enum Handle<SocketT>
where
SocketT: SchemeSocket,
@@ -453,10 +460,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)?;
+162 -1
View File
@@ -1,6 +1,7 @@
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;
@@ -13,6 +14,17 @@ use libredox::flag;
const SO_SNDBUF: usize = 7;
const SO_RCVBUF: usize = 8;
const SO_KEEPALIVE: usize = 9;
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 +100,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));
}
@@ -403,6 +417,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();
@@ -425,7 +446,147 @@ 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_rto: 3000,
};
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 => {
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)
}
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)
}
_ => 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),
_ => Err(SyscallError::new(syscall::ENOPROTOOPT)),
}
}
}
#[repr(C)]
struct TcpInfo {
tcpi_state: u8,
_pad: [u8; 3],
tcpi_snd_queuelen: u32,
tcpi_rto: u32,
}
+272
View File
@@ -0,0 +1,272 @@
//! 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 {
let mut written = Vec::new();
{
let mut rx = dev.tx.borrow_mut();
while let Some(packet) = rx.pop_front() {
written.push(packet);
}
}
if !written.is_empty() {
let mut rx = dev.rx.borrow_mut();
for packet in written {
rx.push_back(packet);
}
}
}
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(())
}
}
+75
View File
@@ -0,0 +1,75 @@
//! 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::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);
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]]),
)
}
+8
View File
@@ -0,0 +1,8 @@
[package]
name = "redbear-ufw"
version = "0.1.0"
edition = "2021"
[[bin]]
name = "ufw"
path = "src/main.rs"
+414
View File
@@ -0,0 +1,414 @@
//! redbear-ufw — Uncomplicated Firewall for Red Bear OS.
//!
//! Mirrors Linux UFW's CLI interface and translates commands to
//! `scheme:netfilter` operations. Rule persistence in `/etc/ufw/`.
//!
//! Linux reference: UFW (Uncomplicated Firewall) — Python frontend
//! for iptables. Originally by Jamie Strandboge for Ubuntu.
//!
//! Commands:
//! ufw enable|disable
//! ufw allow|deny|reject|limit <PORT>[/PROTO]
//! ufw delete <NUM>
//! ufw status [verbose|numbered]
//! ufw default allow|deny|reject incoming|outgoing|routed
//! ufw logging on|off
//! ufw reset
//! ufw reload
use std::env;
use std::fs;
use std::io::Write;
use std::path::PathBuf;
use std::process;
const UFW_DIR: &str = "/etc/ufw";
const NETFILTER: &str = "/scheme/netfilter";
fn ufw_dir() -> PathBuf { PathBuf::from(UFW_DIR) }
fn rules_path() -> PathBuf { ufw_dir().join("user.rules") }
fn status_path() -> PathBuf { ufw_dir().join("status") }
fn defaults_path() -> PathBuf { ufw_dir().join("defaults") }
fn ensure_dirs() {
let _ = fs::create_dir_all(UFW_DIR);
}
fn write_scheme(path: &str, value: &str) -> Result<(), String> {
let full = format!("{NETFILTER}/{path}");
let mut f = fs::OpenOptions::new().write(true).create(false)
.open(&full)
.map_err(|e| format!("cannot open {}: {}", full, e))?;
f.write_all(value.as_bytes())
.map_err(|e| format!("write {}: {}", full, e))?;
f.sync_data().map_err(|_| "sync failed".into())
}
fn read_scheme(path: &str) -> Result<String, String> {
let full = format!("{NETFILTER}/{path}");
fs::read_to_string(&full)
.map_err(|e| format!("read {}: {}", full, e))
}
fn is_enabled() -> bool {
fs::read_to_string(status_path())
.map(|s| s.trim() == "enabled")
.unwrap_or(false)
}
fn cmd_enable() -> Result<(), String> {
ensure_dirs();
// Apply default policies
let defaults = load_defaults();
for &chain in &["input", "output", "forward"] {
let p = defaults.get(&chain.to_string()).cloned().unwrap_or_else(|| "ACCEPT".to_string());
write_scheme(&format!("policy/{chain}"), &p)?;
}
// Apply stored rules
if rules_path().exists() {
let content = fs::read_to_string(rules_path())
.map_err(|e| format!("read rules: {}", e))?;
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
write_scheme("rule/add", &format!("{}\n", trimmed))?;
}
}
fs::write(status_path(), "enabled\n").map_err(|e| format!("write status: {}", e))?;
println!("Firewall is active and enabled on system startup");
Ok(())
}
fn cmd_disable() -> Result<(), String> {
write_scheme("reset", "1\n")?;
fs::write(status_path(), "disabled\n").map_err(|e| format!("write status: {}", e))?;
println!("Firewall stopped and disabled on system startup");
Ok(())
}
fn cmd_allow(args: &[String]) -> Result<(), String> {
let rule = parse_port_rule("ACCEPT", args)?;
add_rule(&rule)
}
fn cmd_deny(args: &[String]) -> Result<(), String> {
let rule = parse_port_rule("DROP", args)?;
add_rule(&rule)
}
fn cmd_reject(args: &[String]) -> Result<(), String> {
let rule = parse_port_rule("REJECT", args)?;
add_rule(&rule)
}
fn cmd_limit(args: &[String]) -> Result<(), String> {
let (port, proto) = parse_port_proto(args)?;
let log_rule = format!("LOG input -p {} --dport {}", proto, port);
let drop_rule = format!("DROP input -p {} --dport {}", proto, port);
add_rule(&log_rule)?;
add_rule(&drop_rule)
}
fn cmd_delete(args: &[String]) -> Result<(), String> {
let num_str = args.first()
.ok_or("usage: ufw delete <number>")?;
let pos: usize = num_str.parse().map_err(|_| "invalid rule number")?;
if pos == 0 {
return Err("rule number must be >= 1".into());
}
let rules = read_scheme("rule/list")?;
let mut rule_pos = 0;
for line in rules.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with("Chain ") {
continue;
}
rule_pos += 1;
if rule_pos != pos {
continue;
}
if let Some(id) = extract_rule_id(trimmed) {
write_scheme("rule/del", &format!("{}\n", id))?;
println!("Deleted rule {}", id);
return Ok(());
}
}
Err(format!("rule {} not found", pos))
}
fn extract_rule_id(rule_line: &str) -> Option<u32> {
let start = rule_line.find('[')? + 1;
let end = rule_line[start..].find(']')?;
rule_line[start..start + end].trim().parse().ok()
}
fn cmd_status(args: &[String]) -> Result<(), String> {
let verbose = args.iter().any(|a| a == "verbose");
let numbered = args.iter().any(|a| a == "numbered");
let active = is_enabled();
println!("Status: {}", if active { "active" } else { "inactive" });
if !active {
return Ok(());
}
let rules = read_scheme("rule/list")?;
if rules.trim().is_empty() {
println!("(no rules)");
return Ok(());
}
let mut rule_num = 0;
for line in rules.lines() {
let trimmed = line.trim();
if trimmed.is_empty() { continue; }
if trimmed.starts_with("Chain ") {
if verbose {
println!("\n{}", trimmed);
}
continue;
}
rule_num += 1;
if numbered {
println!("[{}] {}", rule_num, trimmed);
} else if verbose {
println!(" {}", trimmed);
} else {
let parts: Vec<&str> = trimmed.split_whitespace().collect();
if parts.len() >= 2 {
println!("{} {}", parts[0], parts[1..].join(" "));
}
}
}
Ok(())
}
fn cmd_default(args: &[String]) -> Result<(), String> {
if args.len() < 2 {
return Err("usage: ufw default allow|deny|reject incoming|outgoing|routed".into());
}
let policy = args[0].to_uppercase();
let chain = match args[1].to_lowercase().as_str() {
"incoming" | "in" | "input" => "input",
"outgoing" | "out" | "output" => "output",
"routed" | "forward" => "forward",
_ => return Err("chain must be: incoming, outgoing, or routed".into()),
};
let verdict = match policy.as_str() {
"ALLOW" | "ACCEPT" => "ACCEPT",
"DENY" | "DROP" => "DROP",
"REJECT" => "REJECT",
_ => return Err("policy must be: allow, deny, or reject".into()),
};
write_scheme(&format!("policy/{chain}"), verdict)?;
save_default(chain, verdict);
println!("Default {} policy changed to '{}'", chain, verdict.to_lowercase());
Ok(())
}
fn cmd_logging(args: &[String]) -> Result<(), String> {
let on = match args.first().map(|s| s.as_str()) {
Some("on") | Some("ON") => true,
Some("off") | Some("OFF") => false,
_ => return Err("usage: ufw logging on|off".into()),
};
if on {
// Add LOG rule before DROP default
write_scheme("rule/add", "LOG input\n")?;
println!("Logging enabled");
} else {
// Can't selectively remove LOG rules easily — reset and reload
write_scheme("reset", "1\n")?;
cmd_reload()?;
println!("Logging disabled");
}
Ok(())
}
fn cmd_reset() -> Result<(), String> {
write_scheme("reset", "1\n")?;
// Reset defaults
write_scheme("policy/input", "DROP\n")?;
write_scheme("policy/output", "ACCEPT\n")?;
write_scheme("policy/forward", "DROP\n")?;
// Clear stored rules
let _ = fs::remove_file(rules_path());
let _ = fs::remove_file(defaults_path());
let _ = fs::write(status_path(), "disabled\n");
println!("Resetting all rules to installed defaults.");
println!("Firewall stopped and disabled on system startup");
Ok(())
}
fn cmd_reload() -> Result<(), String> {
if !is_enabled() {
return Ok(());
}
write_scheme("reset", "1\n")?;
cmd_enable()
}
fn cmd_app(args: &[String]) -> Result<(), String> {
let name = args.first().ok_or("usage: ufw app list|info <name>|update")?;
match name.as_str() {
"list" => {
let app_dir = ufw_dir().join("applications.d");
if app_dir.exists() {
for entry in fs::read_dir(&app_dir).map_err(|e| format!("read dir: {}", e))? {
let entry = entry.map_err(|e| format!("entry: {}", e))?;
if let Some(name) = entry.path().file_stem() {
println!(" {}", name.to_string_lossy());
}
}
} else {
println!("(no application profiles)");
}
}
"update" => {
let _ = fs::create_dir_all(ufw_dir().join("applications.d"));
println!("Application profiles updated");
}
_ => return Err("usage: ufw app list|info <name>|update".into()),
}
Ok(())
}
fn parse_port_rule(verdict: &str, args: &[String]) -> Result<String, String> {
let (port, proto) = parse_port_proto(args)?;
Ok(format!("{} input -p {} --dport {}", verdict, proto, port))
}
fn parse_port_proto(args: &[String]) -> Result<(String, String), String> {
let port_arg = args.first().ok_or("usage: ufw <action> <port>[/proto]")?;
let (port, proto) = if let Some((p, pr)) = port_arg.split_once('/') {
(p.to_string(), pr.to_lowercase())
} else {
(port_arg.clone(), "tcp".to_string())
};
let proto_name = match proto.as_str() {
"tcp" | "6" => "tcp".to_string(),
"udp" | "17" => "udp".to_string(),
_ => return Err("protocol must be tcp or udp".into()),
};
let port_num: u16 = port.parse().map_err(|_| "invalid port number")?;
if port_num == 0 || port_num > 65535 {
return Err("port out of range (1-65535)".into());
}
Ok((port, proto_name))
}
fn add_rule(rule: &str) -> Result<(), String> {
write_scheme("rule/add", &format!("{}\n", rule))?;
// Persist to rules file
ensure_dirs();
let mut f = fs::OpenOptions::new().create(true).append(true)
.open(rules_path())
.map_err(|e| format!("open rules: {}", e))?;
writeln!(f, "{}", rule).map_err(|e| format!("write rules: {}", e))?;
println!("Rule added");
Ok(())
}
fn load_defaults() -> std::collections::HashMap<String, String> {
let mut map = std::collections::HashMap::new();
map.insert("input".into(), "DROP".into());
map.insert("output".into(), "ACCEPT".into());
map.insert("forward".into(), "DROP".into());
if let Ok(content) = fs::read_to_string(defaults_path()) {
for line in content.lines() {
if let Some((k, v)) = line.trim().split_once('=') {
map.insert(k.to_string(), v.to_string());
}
}
}
map
}
fn save_default(chain: &str, policy: &str) {
ensure_dirs();
let mut map = load_defaults();
map.insert(chain.to_string(), policy.to_string());
let mut content = String::new();
for (k, v) in &map {
content.push_str(&format!("{}={}\n", k, v));
}
let _ = fs::write(defaults_path(), content);
}
fn print_usage() {
println!("Usage: ufw COMMAND\n");
println!("Commands:");
println!(" enable enables the firewall");
println!(" disable disables the firewall");
println!(" default ARG set default policy");
println!(" logging LEVEL set logging to on|off");
println!();
println!(" allow ARGS add allow rule");
println!(" deny ARGS add deny rule");
println!(" reject ARGS add reject rule");
println!(" limit ARGS add limit rule");
println!(" delete RULE|NUM delete RULE");
println!(" insert NUM RULE insert RULE at NUM");
println!();
println!(" status show firewall status");
println!(" status verbose show verbose firewall status");
println!(" status numbered show firewall status as numbered list");
println!(" show ARG show firewall report");
println!(" app list list application profiles");
println!(" app info PROFILE show information on PROFILE");
println!(" app update update PROFILE");
println!();
println!(" reset reset firewall");
println!(" reload reload firewall");
println!();
println!(" version display version information");
}
fn main() {
let args: Vec<String> = env::args().skip(1).collect();
if args.is_empty() {
print_usage();
return;
}
let cmd = args[0].as_str();
let rest = &args[1..];
let result = match cmd {
"enable" => cmd_enable(),
"disable" => cmd_disable(),
"allow" => cmd_allow(rest),
"deny" => cmd_deny(rest),
"reject" => cmd_reject(rest),
"limit" => cmd_limit(rest),
"delete" | "remove" => cmd_delete(rest),
"status" => cmd_status(rest),
"default" => cmd_default(rest),
"logging" | "log" => cmd_logging(rest),
"reset" => cmd_reset(),
"reload" => cmd_reload(),
"app" | "application" => cmd_app(rest),
"help" | "--help" | "-h" => { print_usage(); Ok(()) }
"version" | "--version" => { println!("redbear-ufw 0.1.0"); Ok(()) }
_ => Err(format!("unknown command: {}\nTry 'ufw help'", cmd)),
};
if let Err(e) = result {
eprintln!("ERROR: {}", e);
process::exit(1);
}
}