From bb3e36e4e0ee839ee7700144ea6f983c8d9cff13 Mon Sep 17 00:00:00 2001 From: Red Bear OS Date: Wed, 8 Jul 2026 13:27:49 +0300 Subject: [PATCH] restore: networking stack files from reflog (Phases 1-6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Cargo.lock | 16 + Cargo.toml | 3 + dhcpd/src/dhcp/mod.rs | 18 + dhcpd/src/main.rs | 537 +++++++++++----------------- dhcpv6d/Cargo.toml | 4 + dhcpv6d/src/main.rs | 286 +++++++++++++++ drivers/graphics/fbcond/src/text.rs | 4 + drivers/usb/xhcid/src/xhci/trb.rs | 105 +++--- netdiag/Cargo.toml | 6 + netdiag/src/main.rs | 138 +++++++ netstack/Cargo.toml | 1 + netstack/src/filter/conntrack.rs | 357 ++++++++++++++++++ netstack/src/filter/mod.rs | 175 +++++++++ netstack/src/filter/nat.rs | 361 +++++++++++++++++++ netstack/src/filter/rule.rs | 187 ++++++++++ netstack/src/filter/table.rs | 384 ++++++++++++++++++++ netstack/src/icmp_error.rs | 116 ++++++ netstack/src/link/bond.rs | 169 +++++++++ netstack/src/link/bridge.rs | 190 ++++++++++ netstack/src/link/ethernet.rs | 532 ++++++++++++++++++++++++--- netstack/src/link/gre.rs | 190 ++++++++++ netstack/src/link/ipip.rs | 125 +++++++ netstack/src/link/loopback.rs | 4 +- netstack/src/link/mod.rs | 35 +- netstack/src/link/qdisc.rs | 157 ++++++++ netstack/src/link/tun.rs | 73 ++++ netstack/src/link/vlan.rs | 164 +++++++++ netstack/src/link/vxlan.rs | 213 +++++++++++ netstack/src/main.rs | 24 +- netstack/src/port_set.rs | 5 + netstack/src/router/mod.rs | 372 +++++++++++++++++-- netstack/src/scheme/mod.rs | 41 ++- netstack/src/scheme/netcfg/mod.rs | 146 +++++++- netstack/src/scheme/netfilter.rs | 408 +++++++++++++++++++++ netstack/src/scheme/socket.rs | 31 +- netstack/src/scheme/tcp.rs | 163 ++++++++- netstack/src/scheme/tun.rs | 272 ++++++++++++++ netstack/src/slaac.rs | 75 ++++ redbear-ufw/Cargo.toml | 8 + redbear-ufw/src/main.rs | 414 +++++++++++++++++++++ 40 files changed, 6042 insertions(+), 467 deletions(-) create mode 100644 dhcpv6d/Cargo.toml create mode 100644 dhcpv6d/src/main.rs create mode 100644 netdiag/Cargo.toml create mode 100644 netdiag/src/main.rs create mode 100644 netstack/src/filter/conntrack.rs create mode 100644 netstack/src/filter/mod.rs create mode 100644 netstack/src/filter/nat.rs create mode 100644 netstack/src/filter/rule.rs create mode 100644 netstack/src/filter/table.rs create mode 100644 netstack/src/icmp_error.rs create mode 100644 netstack/src/link/bond.rs create mode 100644 netstack/src/link/bridge.rs create mode 100644 netstack/src/link/gre.rs create mode 100644 netstack/src/link/ipip.rs create mode 100644 netstack/src/link/qdisc.rs create mode 100644 netstack/src/link/tun.rs create mode 100644 netstack/src/link/vlan.rs create mode 100644 netstack/src/link/vxlan.rs create mode 100644 netstack/src/scheme/netfilter.rs create mode 100644 netstack/src/scheme/tun.rs create mode 100644 netstack/src/slaac.rs create mode 100644 redbear-ufw/Cargo.toml create mode 100644 redbear-ufw/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 05ec4f191b..b326157adc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 015b7ef008..a27c036a36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", diff --git a/dhcpd/src/dhcp/mod.rs b/dhcpd/src/dhcp/mod.rs index d57f380778..1fc2cd0b6d 100644 --- a/dhcpd/src/dhcp/mod.rs +++ b/dhcpd/src/dhcp/mod.rs @@ -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; diff --git a/dhcpd/src/main.rs b/dhcpd/src/main.rs index a95b703eea..064b33ddfd 100644 --- a/dhcpd/src/main.rs +++ b/dhcpd/src/main.rs @@ -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::(), - ) - }; - - 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::(), - ) - }; - - 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 { + 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::()) + }; + 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())); } -} +} \ No newline at end of file diff --git a/dhcpv6d/Cargo.toml b/dhcpv6d/Cargo.toml new file mode 100644 index 0000000000..c70392f410 --- /dev/null +++ b/dhcpv6d/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "dhcpv6d" +version = "0.1.0" +edition = "2021" \ No newline at end of file diff --git a/dhcpv6d/src/main.rs b/dhcpv6d/src/main.rs new file mode 100644 index 0000000000..cabaf08bcc --- /dev/null +++ b/dhcpv6d/src/main.rs @@ -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 { + 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, val: u16) { + buf.extend_from_slice(&val.to_be_bytes()); +} + +fn put_u32(buf: &mut Vec, val: u32) { + buf.extend_from_slice(&val.to_be_bytes()); +} + +fn put_option(buf: &mut Vec, code: u16, data: &[u8]) { + put_u16(buf, code); + put_u16(buf, data.len() as u16); + buf.extend_from_slice(data); +} + +struct Dhcp6Packet { + data: Vec, +} + +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>, + addresses: Vec<([u8; 16], u32, u32)>, + dns_servers: Vec<[u8; 16]>, + domains: Vec, +} + +fn parse_reply(data: &[u8]) -> Result { + 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(()) +} \ No newline at end of file diff --git a/drivers/graphics/fbcond/src/text.rs b/drivers/graphics/fbcond/src/text.rs index ddb77d657c..ca5162b00a 100644 --- a/drivers/graphics/fbcond/src/text.rs +++ b/drivers/graphics/fbcond/src/text.rs @@ -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"); diff --git a/drivers/usb/xhcid/src/xhci/trb.rs b/drivers/usb/xhcid/src/xhci/trb.rs index 9270731bc9..32773dd5fd 100644 --- a/drivers/usb/xhcid/src/xhci/trb.rs +++ b/drivers/usb/xhcid/src/xhci/trb.rs @@ -539,93 +539,90 @@ impl fmt::Display for Trb { #[cfg(test)] mod tests { use super::*; - use common::dma::Dma; - fn test_trb() -> (Dma, *mut Trb) { - unsafe { - let dma = Dma::::zeroed().unwrap().assume_init(); - let ptr = dma.virt_ptr(); - (dma, ptr) - } + fn test_trb() -> Trb { + unsafe { std::mem::zeroed::() } } #[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); } } diff --git a/netdiag/Cargo.toml b/netdiag/Cargo.toml new file mode 100644 index 0000000000..6ae147678d --- /dev/null +++ b/netdiag/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "redbear-netdiag" +version = "0.1.0" +edition = "2021" + +[dependencies] \ No newline at end of file diff --git a/netdiag/src/main.rs b/netdiag/src/main.rs new file mode 100644 index 0000000000..d61291c1f1 --- /dev/null +++ b/netdiag/src/main.rs @@ -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 = 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"); +} \ No newline at end of file diff --git a/netstack/Cargo.toml b/netstack/Cargo.toml index df6d9d82ba..e1f23af959 100644 --- a/netstack/Cargo.toml +++ b/netstack/Cargo.toml @@ -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" diff --git a/netstack/src/filter/conntrack.rs b/netstack/src/filter/conntrack.rs new file mode 100644 index 0000000000..0faf3f14a3 --- /dev/null +++ b/netstack/src/filter/conntrack.rs @@ -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(&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, + last_cleanup: Option, + rate_limits: BTreeMap, + 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 = 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 + } +} + diff --git a/netstack/src/filter/mod.rs b/netstack/src/filter/mod.rs new file mode 100644 index 0000000000..1a37f9f328 --- /dev/null +++ b/netstack/src/filter/mod.rs @@ -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 { + 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 { + 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>, + pub out_dev: Option>, + pub src_addr: smoltcp::wire::IpAddress, + pub dst_addr: smoltcp::wire::IpAddress, + pub protocol: u8, + pub src_port: Option, + pub dst_port: Option, + pub packet: &'a [u8], +} + +extern crate alloc; diff --git a/netstack/src/filter/nat.rs b/netstack/src/filter/nat.rs new file mode 100644 index 0000000000..2e9e9612d1 --- /dev/null +++ b/netstack/src/filter/nat.rs @@ -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, + pub dst_match: Option, + pub trans_addr: IpAddress, + pub trans_port: Option, + 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, + 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)> { + 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)> { + 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 { + 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 { + 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), +} \ No newline at end of file diff --git a/netstack/src/filter/rule.rs b/netstack/src/filter/rule.rs new file mode 100644 index 0000000000..8633cb21a1 --- /dev/null +++ b/netstack/src/filter/rule.rs @@ -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 = 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, + pub src_prefix_len: u8, + pub dst_addr: Option, + pub dst_prefix_len: u8, + pub protocol: Option, + pub src_port: Option, + pub dst_port: Option, + pub in_dev: Option>, + pub out_dev: Option>, + pub state_match: Option, + 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; \ No newline at end of file diff --git a/netstack/src/filter/table.rs b/netstack/src/filter/table.rs new file mode 100644 index 0000000000..7edf68d3a4 --- /dev/null +++ b/netstack/src/filter/table.rs @@ -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, + pub default_policy: BTreeMap, + pub next_id: u32, + pub conntrack: Option, + pub nat_table: NatTable, + pub chain_counters: BTreeMap, + pub log_buffer: Vec, +} + +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 = 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 { + 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::().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 { + let lower = value.to_lowercase(); + let num: Option = 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 { + let port_str = value.split(':').next().unwrap_or(value); + port_str + .parse::() + .map_err(|_| ParseError::BadPort(value.to_string())) +} \ No newline at end of file diff --git a/netstack/src/icmp_error.rs b/netstack/src/icmp_error.rs new file mode 100644 index 0000000000..de1f53537a --- /dev/null +++ b/netstack/src/icmp_error.rs @@ -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> { + 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> { + 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> { + 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; \ No newline at end of file diff --git a/netstack/src/link/bond.rs b/netstack/src/link/bond.rs new file mode 100644 index 0000000000..434b5cd0f3 --- /dev/null +++ b/netstack/src/link/bond.rs @@ -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, + up: bool, +} + +pub struct BondDevice { + name: Rc, + slaves: RefCell>, + active_slave: RefCell, + mode: BondMode, + rr_counter: RefCell, + recv_buffer: Vec, + ip_address: Option, +} + +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(&self, dev: T) { + self.slaves.borrow_mut().push(Slave { + dev: Box::new(dev), + up: true, + }); + } + + fn select_slave(&self) -> Option { + 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 { + &self.name + } + + fn can_recv(&self) -> bool { + self.slaves.borrow().iter().any(|s| s.up && s.dev.can_recv()) + } + + fn mac_address(&self) -> Option { + 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 { + 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 + } +} \ No newline at end of file diff --git a/netstack/src/link/bridge.rs b/netstack/src/link/bridge.rs new file mode 100644 index 0000000000..dd3de95b58 --- /dev/null +++ b/netstack/src/link/bridge.rs @@ -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, + ports: RefCell>>, + mac_table: RefCell>, + recv_buffer: Vec, + ip_address: Option, +} + +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(&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 { + self.mac_table.borrow().get(&mac).map(|e| e.port) + } + + fn flood(&self, packet: &[u8], now: Instant, except_port: Option) { + 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, 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 { + &self.name + } + + fn can_recv(&self) -> bool { + self.ports.borrow().iter().any(|p| p.can_recv()) + } + + fn mac_address(&self) -> Option { + None + } + + fn set_mac_address(&mut self, _addr: EthernetAddress) {} + + fn ip_address(&self) -> Option { + 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" } + } +} \ No newline at end of file diff --git a/netstack/src/link/ethernet.rs b/netstack/src/link/ethernet.rs index 5f32a479dd..fb6b06c828 100644 --- a/netstack/src/link/ethernet.rs +++ b/netstack/src/link/ethernet.rs @@ -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, neighbor_cache: BTreeMap, arp_state: ArpState, + ndp_state: NdpState, + slaac_state: SlaacState, waiting_packets: PacketBuffer, input_buffer: Vec, output_buffer: Vec, network_file: File, hardware_address: Option, - ip_address: Option, + ip_address: Option, + 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 { - 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" } + } } + diff --git a/netstack/src/link/gre.rs b/netstack/src/link/gre.rs new file mode 100644 index 0000000000..3970756478 --- /dev/null +++ b/netstack/src/link/gre.rs @@ -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, + parent_name: Rc, + local_ip: Ipv4Address, + remote_ip: Ipv4Address, + gre_key: Option, + gre_header: [u8; 8], + gre_header_len: usize, + send_buffer: Vec, + recv_buffer: Vec, + recv_queue: VecDeque>, + ip_address: Option, +} + +impl GreDevice { + pub fn new( + name: &str, + parent_name: &str, + local_ip: Ipv4Address, + remote_ip: Ipv4Address, + gre_key: Option, + ) -> 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) { + 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 { + &self.name + } + + fn can_recv(&self) -> bool { + !self.recv_queue.is_empty() + } + + fn mac_address(&self) -> Option { + None + } + + fn set_mac_address(&mut self, _addr: EthernetAddress) {} + + fn ip_address(&self) -> Option { + self.ip_address + } + + fn set_ip_address(&mut self, addr: IpCidr) { + self.ip_address = Some(addr); + } +} \ No newline at end of file diff --git a/netstack/src/link/ipip.rs b/netstack/src/link/ipip.rs new file mode 100644 index 0000000000..7c9be17d8f --- /dev/null +++ b/netstack/src/link/ipip.rs @@ -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, + parent_name: Rc, + local_ip: Ipv4Address, + remote_ip: Ipv4Address, + send_buffer: Vec, + recv_buffer: Vec, + recv_queue: VecDeque>, + ip_address: Option, +} + +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) { + 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 { + &self.name + } + + fn can_recv(&self) -> bool { + !self.recv_queue.is_empty() + } + + fn mac_address(&self) -> Option { + None + } + + fn set_mac_address(&mut self, _addr: EthernetAddress) {} + + fn ip_address(&self) -> Option { + self.ip_address + } + + fn set_ip_address(&mut self, addr: IpCidr) { + self.ip_address = Some(addr); + } +} \ No newline at end of file diff --git a/netstack/src/link/loopback.rs b/netstack/src/link/loopback.rs index 24bdb74eba..c980d3648e 100644 --- a/netstack/src/link/loopback.rs +++ b/netstack/src/link/loopback.rs @@ -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) {} } diff --git a/netstack/src/link/mod.rs b/netstack/src/link/mod.rs index 013989c212..c02a28ecd4 100644 --- a/netstack/src/link/mod.rs +++ b/netstack/src/link/mod.rs @@ -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; 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)] diff --git a/netstack/src/link/qdisc.rs b/netstack/src/link/qdisc.rs new file mode 100644 index 0000000000..4c190fc05f --- /dev/null +++ b/netstack/src/link/qdisc.rs @@ -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>; 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) -> 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> { + 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> { + 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, + now: Instant, + ) -> Option> { + 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() + } + } + } +} \ No newline at end of file diff --git a/netstack/src/link/tun.rs b/netstack/src/link/tun.rs new file mode 100644 index 0000000000..d9781ad9bb --- /dev/null +++ b/netstack/src/link/tun.rs @@ -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>>>; + +pub struct TunDevice { + name: Rc, + rx_queue: PacketQueue, + tx_queue: PacketQueue, + recv_buffer: Vec, + ip_address: Option, +} + +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 { + &self.name + } + + fn can_recv(&self) -> bool { + !self.rx_queue.borrow().is_empty() + } + + fn mac_address(&self) -> Option { + None + } + + fn set_mac_address(&mut self, _addr: EthernetAddress) {} + + fn ip_address(&self) -> Option { + self.ip_address + } + + fn set_ip_address(&mut self, addr: IpCidr) { + self.ip_address = Some(addr); + } +} \ No newline at end of file diff --git a/netstack/src/link/vlan.rs b/netstack/src/link/vlan.rs new file mode 100644 index 0000000000..c6824f26cd --- /dev/null +++ b/netstack/src/link/vlan.rs @@ -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, + parent_name: Rc, + vlan_id: u16, + priority: u8, + tag: [u8; 4], + send_buffer: Vec, + recv_buffer: Vec, + recv_queue: VecDeque>, + mac_address: Option, + ip_address: Option, +} + +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) { + 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 { + &self.name + } + + fn can_recv(&self) -> bool { + !self.recv_queue.is_empty() + } + + fn mac_address(&self) -> Option { + self.mac_address + } + + fn set_mac_address(&mut self, addr: EthernetAddress) { + self.mac_address = Some(addr); + } + + fn ip_address(&self) -> Option { + self.ip_address + } + + fn set_ip_address(&mut self, addr: IpCidr) { + self.ip_address = Some(addr); + } +} \ No newline at end of file diff --git a/netstack/src/link/vxlan.rs b/netstack/src/link/vxlan.rs new file mode 100644 index 0000000000..1383beb5a9 --- /dev/null +++ b/netstack/src/link/vxlan.rs @@ -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, + parent_name: Rc, + local_ip: Ipv4Address, + remote_ip: Ipv4Address, + vni: u32, + vxlan_header: [u8; 8], + send_buffer: Vec, + recv_buffer: Vec, + recv_queue: VecDeque>, + virtual_mac: EthernetAddress, + ip_address: Option, +} + +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) { + self.recv_queue.push_back(packet); + } + + fn build_encapsulated(&mut self, inner_packet: &[u8]) -> &[u8] { + self.send_buffer.clear(); + let inner_eth = { + let mut buf = [0u8; 14]; + buf[..6].copy_from_slice(&EthernetAddress::BROADCAST.0); + buf[6..12].copy_from_slice(&self.virtual_mac.0); + let ethtype = if !inner_packet.is_empty() && inner_packet[0] >> 4 == 6 { + EthernetProtocol::Ipv6 + } else { + EthernetProtocol::Ipv4 + }; + let proto_bytes: [u8; 2] = match ethtype { + EthernetProtocol::Ipv4 => [0x08, 0x00], + EthernetProtocol::Ipv6 => [0x86, 0xDD], + _ => [0x08, 0x00], + }; + buf[12..14].copy_from_slice(&proto_bytes); + buf + }; + let inner_frame_len = inner_eth.len() + inner_packet.len(); + + let udp_repr = UdpRepr { + src_port: VXLAN_PORT, + dst_port: VXLAN_PORT, + }; + let udp_payload_len = 8 + inner_frame_len; + let outer_ip_repr = Ipv4Repr { + src_addr: self.local_ip, + dst_addr: self.remote_ip, + next_header: IpProtocol::Udp, + payload_len: 8 + udp_payload_len, + hop_limit: 64, + }; + + let total_len = outer_ip_repr.buffer_len() + 8 + 8 + inner_frame_len; + self.send_buffer.resize(total_len, 0); + let mut ip = Ipv4Packet::new_unchecked(&mut self.send_buffer); + outer_ip_repr.emit(&mut ip, &smoltcp::phy::ChecksumCapabilities::ignored()); + + let ip_hdr_len = outer_ip_repr.buffer_len(); + let mut udp = UdpPacket::new_unchecked(&mut self.send_buffer[ip_hdr_len..]); + udp_repr.emit( + &mut udp, + &IpAddress::Ipv4(self.local_ip), + &IpAddress::Ipv4(self.remote_ip), + udp_payload_len, + |buf| { + buf[..8].copy_from_slice(&self.vxlan_header); + buf[8..8 + inner_eth.len()].copy_from_slice(&inner_eth); + buf[8 + inner_eth.len()..].copy_from_slice(inner_packet); + }, + &smoltcp::phy::ChecksumCapabilities::ignored(), + ); + &self.send_buffer + } + + fn matches_endpoint(&self, outer_packet: &[u8]) -> bool { + if outer_packet.len() < 50 { + return false; + } + let Ok(ipv4) = Ipv4Packet::new_checked(outer_packet) else { + return false; + }; + if u8::from(ipv4.next_header()) != 17 || ipv4.dst_addr() != self.local_ip { + return false; + } + let ip_hdr_len = 20; + let udp = &outer_packet[ip_hdr_len..]; + if udp.len() < 18 { + return false; + } + let dst_port = u16::from_be_bytes([udp[2], udp[3]]); + if dst_port != VXLAN_PORT { + return false; + } + if udp[8] != VXLAN_FLAGS { + return false; + } + let pkt_vni = u32::from_be_bytes([0, udp[12], udp[13], udp[14]]) & 0x00ffffff; + pkt_vni == self.vni + } + + fn decapsulate(&mut self, outer_packet: &[u8]) -> Option<&[u8]> { + if !self.matches_endpoint(outer_packet) { + return None; + } + let inner_frame_start = 20 + 8 + 8; + let inner_frame = &outer_packet[inner_frame_start..]; + if inner_frame.len() < 14 { + return None; + } + let eth = EthernetFrame::new_unchecked(inner_frame); + let Ok(repr) = EthernetRepr::parse(ð) else { + return None; + }; + if repr.ethertype != EthernetProtocol::Ipv4 && repr.ethertype != EthernetProtocol::Ipv6 { + return None; + } + self.recv_buffer.clear(); + self.recv_buffer.extend_from_slice(eth.payload()); + Some(&self.recv_buffer) + } +} + +impl LinkDevice for VxlanDevice { + fn send(&mut self, _next_hop: IpAddress, packet: &[u8], now: Instant) { + let enc = self.build_encapsulated(packet).to_vec(); + 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 { + &self.name + } + + fn can_recv(&self) -> bool { + !self.recv_queue.is_empty() + } + + fn mac_address(&self) -> Option { + Some(self.virtual_mac) + } + + fn set_mac_address(&mut self, addr: EthernetAddress) { + self.virtual_mac = addr; + } + + fn ip_address(&self) -> Option { + self.ip_address + } + + fn set_ip_address(&mut self, addr: IpCidr) { + self.ip_address = Some(addr); + } +} \ No newline at end of file diff --git a/netstack/src/main.rs b/netstack/src/main.rs index a441b946a3..a1990cf87d 100644 --- a/netstack/src/main.rs +++ b/netstack/src/main.rs @@ -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 { 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)); } diff --git a/netstack/src/port_set.rs b/netstack/src/port_set.rs index 88011ec337..df2bf1dbef 100644 --- a/netstack/src/port_set.rs +++ b/netstack/src/port_set.rs @@ -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; } diff --git a/netstack/src/router/mod.rs b/netstack/src/router/mod.rs index 3cfd0a2fea..447de11ac1 100644 --- a/netstack/src/router/mod.rs +++ b/netstack/src/router/mod.rs @@ -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>, now: Instant) { + let mut filtered: Vec> = 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>, now: Instant) { + let mut forwarded: Vec> = Vec::new(); + let mut local: Vec> = 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, Option)> = 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>, 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>, + ) { + 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>) { + let mut packets: Vec> = 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>, + 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, Option) { + 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)) +} diff --git a/netstack/src/scheme/mod.rs b/netstack/src/scheme/mod.rs index 87fc3f1b97..d4f5d1a27a 100644 --- a/netstack/src/scheme/mod.rs +++ b/netstack/src/scheme/mod.rs @@ -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>; +type FilterTableRef = Rc>; 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 { 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::() { @@ -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) { diff --git a/netstack/src/scheme/netcfg/mod.rs b/netstack/src/scheme/netcfg/mod.rs index 5d6a5f5368..aa858b5230 100644 --- a/netstack/src/scheme/netcfg/mod.rs +++ b/netstack/src/scheme/netcfg/mod.rs @@ -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 { 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, 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, } type DNSConfigRef = Rc>; @@ -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, diff --git a/netstack/src/scheme/netfilter.rs b/netstack/src/scheme/netfilter.rs new file mode 100644 index 0000000000..84ba4d5a48 --- /dev/null +++ b/netstack/src/scheme/netfilter.rs @@ -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>; + +pub struct NetFilterScheme { + inner: NetFilterSchemeInner, + state: SchemeState, +} + +impl NetFilterScheme { + pub fn new(scheme_file: Socket, table: FilterTableRef) -> Result { + 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> { + 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, + write_buf: Vec, + pos: usize, + done: bool, +} + +struct NetFilterSchemeInner { + scheme_file: Socket, + handles: HandleMap, + 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 { + 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 { + 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 { + Ok(self.handles.insert(Handle::SchemeRoot)) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + _flags: usize, + _fcntl_flags: u32, + ctx: &CallerCtx, + ) -> SyscallResult { + { + 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 { + 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 { + 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)) + } + } +} \ No newline at end of file diff --git a/netstack/src/scheme/socket.rs b/netstack/src/scheme/socket.rs index fc8e558b51..342d6df4d0 100644 --- a/netstack/src/scheme/socket.rs +++ b/netstack/src/scheme/socket.rs @@ -251,17 +251,24 @@ where flags: usize, ) -> SyscallResult; - fn get_sock_opt( +fn get_sock_opt( &self, file: &SchemeFile, name: usize, buf: &mut [u8], ) -> SyscallResult { - // Return Err for default implementation + Err(SyscallError::new(syscall::ENOPROTOOPT)) + } + + fn set_sock_opt( + &mut self, + _file: &SchemeFile, + _name: usize, + _buf: &[u8], + ) -> SyscallResult { Err(SyscallError::new(syscall::ENOPROTOOPT)) } } - pub enum Handle 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::(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)?; diff --git a/netstack/src/scheme/tcp.rs b/netstack/src/scheme/tcp.rs index 435553829d..a49391d27b 100644 --- a/netstack/src/scheme/tcp.rs +++ b/netstack/src/scheme/tcp.rs @@ -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>; @@ -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 { 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::(), + ) + }; + 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, + name: usize, + buf: &[u8], + ) -> SyscallResult { + 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, +} diff --git a/netstack/src/scheme/tun.rs b/netstack/src/scheme/tun.rs new file mode 100644 index 0000000000..f1a1c23e35 --- /dev/null +++ b/netstack/src/scheme/tun.rs @@ -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 +//! └── / +//! ├── 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>; + +pub struct TunScheme { + inner: TunSchemeInner, + state: SchemeState, +} + +struct TunDeviceState { + rx: PacketQueue, + tx: PacketQueue, +} + +struct TunFile { + path: String, + is_dir: bool, + read_buf: Vec, + write_buf: Vec, + pos: usize, + device_rx: Option, + device_tx: Option, +} + +struct TunSchemeInner { + scheme_file: Socket, + handles: HandleMap, + devices: BTreeMap, + device_list: DeviceListRef, +} + +impl TunScheme { + pub fn new(scheme_file: Socket, device_list: DeviceListRef) -> Result { + 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> { + 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 { + 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 { + 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 { + 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 { + 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(()) + } +} \ No newline at end of file diff --git a/netstack/src/slaac.rs b/netstack/src/slaac.rs new file mode 100644 index 0000000000..37bb7a547d --- /dev/null +++ b/netstack/src/slaac.rs @@ -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]]), + ) +} \ No newline at end of file diff --git a/redbear-ufw/Cargo.toml b/redbear-ufw/Cargo.toml new file mode 100644 index 0000000000..716e745e96 --- /dev/null +++ b/redbear-ufw/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "redbear-ufw" +version = "0.1.0" +edition = "2021" + +[[bin]] +name = "ufw" +path = "src/main.rs" \ No newline at end of file diff --git a/redbear-ufw/src/main.rs b/redbear-ufw/src/main.rs new file mode 100644 index 0000000000..772b0e13cd --- /dev/null +++ b/redbear-ufw/src/main.rs @@ -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 [/PROTO] +//! ufw delete +//! 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 { + 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 ")?; + 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 { + 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 |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 |update".into()), + } + Ok(()) +} + +fn parse_port_rule(verdict: &str, args: &[String]) -> Result { + 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 [/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 { + 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 = 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); + } +} \ No newline at end of file