//! Native Linux Netlink and kernel networking execution plane. //! //! Provides genuine Linux kernel networking operations through RTNETLINK: //! - Interface lifecycle (list, get, up, down) //! - IPv4 & IPv6 Address management (list, add, delete) //! - IPv4 & IPv6 Route management (list, add, delete, deterministic reconciliation) //! - IP forwarding status and mutation via procfs //! - Dedicated nftables ruleset generation and caching //! //! Zero subprocesses or shell commands are invoked. use crate::engine::NetworkEngine; use crate::error::{NetworkError, Result}; use crate::forwarding::IpForwardingStatus; use crate::nftables::NftablesRulesetBuilder; use futures::stream::TryStreamExt; use ipnet::IpNet; use netlink_packet_route::AddressFamily; use netlink_packet_route::address::AddressAttribute; use netlink_packet_route::link::{LinkAttribute, LinkFlags}; use netlink_packet_route::route::{RouteAddress, RouteAttribute, RouteMessage}; use nx9_wg_core::types::firewall::FirewallRule; use nx9_wg_core::types::network::Route; use rtnetlink::{Handle, LinkUnspec, RouteMessageBuilder, new_connection}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; use std::sync::Arc; use tokio::sync::RwLock; /// Summary information for a network interface discovered via RTNETLINK. #[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceInfo { pub index: u32, pub name: String, pub is_up: bool, pub mtu: Option, pub oper_state: Option, } /// Address record attached to an interface discovered via RTNETLINK. #[derive(Debug, Clone, PartialEq, Eq)] pub struct AddressInfo { pub index: u32, pub address: IpAddr, pub prefix_len: u8, } /// Routing table entry discovered via RTNETLINK. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RouteInfo { pub destination: IpNet, pub gateway: Option, pub oif: Option, pub table: u32, pub metric: Option, } /// Connect to RTNETLINK and spawn background event loop. fn connect_rtnetlink() -> Result<(Handle, tokio::task::JoinHandle<()>)> { let (conn, handle, _) = new_connection().map_err(|e| { NetworkError::Netlink(format!("Failed to establish RTNETLINK connection: {e}")) })?; let join_handle = tokio::spawn(conn); Ok((handle, join_handle)) } /// List all network interfaces using RTNETLINK link dump. pub async fn list_interfaces() -> Result> { let (handle, _join) = connect_rtnetlink()?; let mut links = handle.link().get().execute(); let mut results = Vec::new(); while let Some(msg) = links .try_next() .await .map_err(|e| NetworkError::Netlink(format!("RTNETLINK link dump failed: {e}")))? { let index = msg.header.index; let is_up = msg.header.flags.contains(LinkFlags::Up); let mut name = String::new(); let mut mtu = None; let mut oper_state = None; for attr in msg.attributes { match attr { LinkAttribute::IfName(n) => name = n, LinkAttribute::Mtu(m) => mtu = Some(m), LinkAttribute::OperState(s) => oper_state = Some(format!("{s:?}")), _ => {} } } if !name.is_empty() { results.push(InterfaceInfo { index, name, is_up, mtu, oper_state, }); } } Ok(results) } /// Query a single interface by name using RTNETLINK. pub async fn get_interface(name: &str) -> Result { let (handle, _join) = connect_rtnetlink()?; let mut links = handle.link().get().match_name(name.to_string()).execute(); while let Some(msg) = links.try_next().await.map_err(|e| { NetworkError::Netlink(format!("RTNETLINK get link failed for '{name}': {e}")) })? { let index = msg.header.index; let is_up = msg.header.flags.contains(LinkFlags::Up); let mut if_name = String::new(); let mut mtu = None; let mut oper_state = None; for attr in msg.attributes { match attr { LinkAttribute::IfName(n) => if_name = n, LinkAttribute::Mtu(m) => mtu = Some(m), LinkAttribute::OperState(s) => oper_state = Some(format!("{s:?}")), _ => {} } } if if_name == name { return Ok(InterfaceInfo { index, name: if_name, is_up, mtu, oper_state, }); } } Err(NetworkError::InterfaceNotFound(name.to_string())) } /// Bring an interface UP using RTNETLINK. pub async fn interface_up(name: &str) -> Result<()> { let iface = get_interface(name).await?; let (handle, _join) = connect_rtnetlink()?; let msg = LinkUnspec::new_with_index(iface.index).up().build(); handle.link().change(msg).execute().await.map_err(|e| { NetworkError::Netlink(format!("Failed to bring interface '{name}' UP: {e}")) })?; tracing::info!(interface = name, "Interface brought UP via RTNETLINK"); Ok(()) } /// Bring an interface DOWN using RTNETLINK. pub async fn interface_down(name: &str) -> Result<()> { let iface = get_interface(name).await?; let (handle, _join) = connect_rtnetlink()?; let msg = LinkUnspec::new_with_index(iface.index).down().build(); handle.link().change(msg).execute().await.map_err(|e| { NetworkError::Netlink(format!("Failed to bring interface '{name}' DOWN: {e}")) })?; tracing::info!(interface = name, "Interface brought DOWN via RTNETLINK"); Ok(()) } /// List all IP addresses on all interfaces using RTNETLINK. pub async fn list_addresses() -> Result> { let (handle, _join) = connect_rtnetlink()?; let mut addrs = handle.address().get().execute(); let mut results = Vec::new(); while let Some(msg) = addrs .try_next() .await .map_err(|e| NetworkError::Netlink(format!("RTNETLINK address dump failed: {e}")))? { let index = msg.header.index; let prefix_len = msg.header.prefix_len; for attr in msg.attributes { if let AddressAttribute::Address(ip) = attr { results.push(AddressInfo { index, address: ip, prefix_len, }); } } } Ok(results) } /// List IP addresses associated with a specific interface index. pub async fn get_addresses_for_interface(index: u32) -> Result> { let all = list_addresses().await?; Ok(all.into_iter().filter(|a| a.index == index).collect()) } /// Add an IP address to an interface using RTNETLINK. pub async fn add_address(interface_name: &str, ip: IpNet) -> Result<()> { let iface = get_interface(interface_name).await?; let existing = get_addresses_for_interface(iface.index).await?; // Idempotency: skip if exact address/prefix already exists on interface if existing .iter() .any(|a| a.address == ip.addr() && a.prefix_len == ip.prefix_len()) { tracing::debug!( interface = interface_name, address = %ip, "Address already assigned to interface; skipping addition" ); return Ok(()); } let (handle, _join) = connect_rtnetlink()?; handle .address() .add(iface.index, ip.addr(), ip.prefix_len()) .execute() .await .map_err(|e| { NetworkError::Netlink(format!( "Failed to add address '{ip}' to interface '{interface_name}': {e}" )) })?; tracing::info!(interface = interface_name, address = %ip, "Address added via RTNETLINK"); Ok(()) } /// Delete an IP address from an interface using RTNETLINK. pub async fn delete_address(interface_name: &str, ip: IpNet) -> Result<()> { let iface = get_interface(interface_name).await?; let (handle, _join) = connect_rtnetlink()?; let mut addrs = handle.address().get().execute(); while let Some(msg) = addrs .try_next() .await .map_err(|e| NetworkError::Netlink(format!("RTNETLINK address query failed: {e}")))? { if msg.header.index != iface.index || msg.header.prefix_len != ip.prefix_len() { continue; } let has_matching_addr = msg.attributes.iter().any(|attr| match attr { AddressAttribute::Address(a) | AddressAttribute::Local(a) => *a == ip.addr(), _ => false, }); if has_matching_addr { handle.address().del(msg).execute().await.map_err(|e| { NetworkError::Netlink(format!( "Failed to delete address '{ip}' from '{interface_name}': {e}" )) })?; tracing::info!(interface = interface_name, address = %ip, "Address deleted via RTNETLINK"); return Ok(()); } } Ok(()) } /// List all IPv4 and IPv6 routes using RTNETLINK route dump. pub async fn list_routes() -> Result> { let (handle, _join) = connect_rtnetlink()?; let mut results = Vec::new(); // 1. IPv4 Routes let mut v4_req = RouteMessage::default(); v4_req.header.address_family = AddressFamily::Inet; let mut v4_stream = handle.route().get(v4_req).execute(); while let Some(msg) = v4_stream .try_next() .await .map_err(|e| NetworkError::Netlink(format!("RTNETLINK IPv4 route dump failed: {e}")))? { if let Some(r) = parse_route_message(&msg, AddressFamily::Inet) { results.push(r); } } // 2. IPv6 Routes let mut v6_req = RouteMessage::default(); v6_req.header.address_family = AddressFamily::Inet6; let mut v6_stream = handle.route().get(v6_req).execute(); while let Some(msg) = v6_stream .try_next() .await .map_err(|e| NetworkError::Netlink(format!("RTNETLINK IPv6 route dump failed: {e}")))? { if let Some(r) = parse_route_message(&msg, AddressFamily::Inet6) { results.push(r); } } Ok(results) } /// Helper to parse a raw RTNETLINK `RouteMessage` into domain `RouteInfo`. fn parse_route_message(msg: &RouteMessage, family: AddressFamily) -> Option { let prefix_len = msg.header.destination_prefix_length; let mut dest_ip = match family { AddressFamily::Inet => IpAddr::V4(Ipv4Addr::UNSPECIFIED), AddressFamily::Inet6 => IpAddr::V6(Ipv6Addr::UNSPECIFIED), _ => return None, }; let mut gateway = None; let mut oif = None; let mut metric = None; let mut table = msg.header.table as u32; for attr in &msg.attributes { match attr { RouteAttribute::Destination(RouteAddress::Inet(v4)) => dest_ip = IpAddr::V4(*v4), RouteAttribute::Destination(RouteAddress::Inet6(v6)) => dest_ip = IpAddr::V6(*v6), RouteAttribute::Gateway(RouteAddress::Inet(v4)) => gateway = Some(IpAddr::V4(*v4)), RouteAttribute::Gateway(RouteAddress::Inet6(v6)) => gateway = Some(IpAddr::V6(*v6)), RouteAttribute::Oif(idx) => oif = Some(*idx), RouteAttribute::Priority(p) => metric = Some(*p), RouteAttribute::Table(t) => table = *t, _ => {} } } let destination = match IpNet::new(dest_ip, prefix_len) { Ok(net) => net, Err(_) => return None, }; Some(RouteInfo { destination, gateway, oif, table, metric, }) } /// Add an IPv4 or IPv6 route to the kernel routing table via RTNETLINK. pub async fn add_route(route: &Route) -> Result<()> { let (handle, _join) = connect_rtnetlink()?; let oif_index = if let Some(ref ifname) = route.interface_name { match get_interface(ifname).await { Ok(info) => Some(info.index), Err(e) => { tracing::warn!( interface = ifname, "Could not resolve interface for route: {e}" ); None } } } else { None }; match route.destination { IpNet::V4(v4) => { let mut builder = RouteMessageBuilder::::new(); builder = builder.destination_prefix(v4.addr(), v4.prefix_len()); if let Some(IpAddr::V4(gw)) = route.gateway { builder = builder.gateway(gw); } if let Some(idx) = oif_index { builder = builder.output_interface(idx); } if let Some(metric) = route.metric { builder = builder.priority(metric); } let msg = builder.build(); if let Err(e) = handle.route().add(msg).execute().await { // If route already exists (EEXIST), handle idempotently let err_str = e.to_string(); if !err_str.contains("File exists") && !err_str.contains("17") { return Err(NetworkError::Netlink(format!( "Failed to add IPv4 route '{}': {e}", route.destination ))); } } } IpNet::V6(v6) => { let mut builder = RouteMessageBuilder::::new(); builder = builder.destination_prefix(v6.addr(), v6.prefix_len()); if let Some(IpAddr::V6(gw)) = route.gateway { builder = builder.gateway(gw); } if let Some(idx) = oif_index { builder = builder.output_interface(idx); } if let Some(metric) = route.metric { builder = builder.priority(metric); } let msg = builder.build(); if let Err(e) = handle.route().add(msg).execute().await { let err_str = e.to_string(); if !err_str.contains("File exists") && !err_str.contains("17") { return Err(NetworkError::Netlink(format!( "Failed to add IPv6 route '{}': {e}", route.destination ))); } } } } tracing::info!( destination = %route.destination, gateway = ?route.gateway, interface = ?route.interface_name, "Route added via RTNETLINK" ); Ok(()) } /// Delete a route from the kernel routing table via RTNETLINK. pub async fn delete_route(route: &Route) -> Result<()> { // Safety check: Never delete default routes unless explicitly verified as an nx9 managed route let is_default = route.destination.addr().is_unspecified() && route.destination.prefix_len() == 0; if is_default && route.interface_name.is_none() { return Err(NetworkError::Routing( "Refusing to delete global default route without specific interface binding" .to_string(), )); } let (handle, _join) = connect_rtnetlink()?; let oif_index = if let Some(ref ifname) = route.interface_name { get_interface(ifname).await.ok().map(|i| i.index) } else { None }; let mut get_msg = RouteMessage::default(); get_msg.header.address_family = match route.destination { IpNet::V4(_) => AddressFamily::Inet, IpNet::V6(_) => AddressFamily::Inet6, }; let mut stream = handle.route().get(get_msg).execute(); while let Some(msg) = stream .try_next() .await .map_err(|e| NetworkError::Netlink(format!("RTNETLINK route query failed: {e}")))? { if msg.header.destination_prefix_length != route.destination.prefix_len() { continue; } let mut dest_match = false; let mut gw_match = route.gateway.is_none(); let mut oif_match = oif_index.is_none(); for attr in &msg.attributes { match attr { RouteAttribute::Destination(RouteAddress::Inet(v4)) if IpAddr::V4(*v4) == route.destination.addr() => { dest_match = true; } RouteAttribute::Destination(RouteAddress::Inet6(v6)) if IpAddr::V6(*v6) == route.destination.addr() => { dest_match = true; } RouteAttribute::Gateway(RouteAddress::Inet(v4)) if Some(IpAddr::V4(*v4)) == route.gateway => { gw_match = true; } RouteAttribute::Gateway(RouteAddress::Inet6(v6)) if Some(IpAddr::V6(*v6)) == route.gateway => { gw_match = true; } RouteAttribute::Oif(idx) if Some(*idx) == oif_index => { oif_match = true; } _ => {} } } // For default prefix /0, dest_match is true if destination is unspecified if route.destination.prefix_len() == 0 { dest_match = true; } if dest_match && gw_match && oif_match { handle.route().del(msg).execute().await.map_err(|e| { NetworkError::Netlink(format!( "Failed to delete route '{}': {e}", route.destination )) })?; tracing::info!(destination = %route.destination, "Route deleted via RTNETLINK"); return Ok(()); } } Ok(()) } /// Real Native Linux Network Engine communicating directly with kernel RTNETLINK. #[derive(Debug, Clone, Default)] pub struct NativeLinuxNetworkEngine { active_ruleset: Arc>, } impl NativeLinuxNetworkEngine { /// Create a new NativeLinuxNetworkEngine instance. pub fn new() -> Self { Self { active_ruleset: Arc::new(RwLock::new(String::new())), } } /// Helper for inspecting network interfaces. pub async fn list_interfaces(&self) -> Result> { list_interfaces().await } /// Helper for inspecting a single interface. pub async fn get_interface(&self, name: &str) -> Result { get_interface(name).await } /// Helper for bringing an interface UP. pub async fn interface_up(&self, name: &str) -> Result<()> { interface_up(name).await } /// Helper for bringing an interface DOWN. pub async fn interface_down(&self, name: &str) -> Result<()> { interface_down(name).await } /// Helper for listing IP addresses. pub async fn list_addresses(&self) -> Result> { list_addresses().await } /// Helper for adding an IP address. pub async fn add_address(&self, interface_name: &str, ip: IpNet) -> Result<()> { add_address(interface_name, ip).await } /// Helper for deleting an IP address. pub async fn delete_address(&self, interface_name: &str, ip: IpNet) -> Result<()> { delete_address(interface_name, ip).await } /// Helper for listing live routes. pub async fn list_routes(&self) -> Result> { list_routes().await } /// Helper for setting IP forwarding. pub async fn set_forwarding_status(&self, status: IpForwardingStatus) -> Result<()> { IpForwardingStatus::set_ipv4(status.ipv4_enabled)?; IpForwardingStatus::set_ipv6(status.ipv6_enabled)?; Ok(()) } } // ============================================================================ // Native Linux nftables Execution Engine (In-Process Netlink via libnftables) // ============================================================================ #[cfg(target_os = "linux")] #[link(name = "nftables")] unsafe extern "C" { fn nft_ctx_new(flags: u32) -> *mut std::ffi::c_void; fn nft_ctx_free(ctx: *mut std::ffi::c_void); fn nft_ctx_buffer_output(ctx: *mut std::ffi::c_void) -> std::ffi::c_int; fn nft_ctx_buffer_error(ctx: *mut std::ffi::c_void) -> std::ffi::c_int; fn nft_ctx_get_output_buffer(ctx: *mut std::ffi::c_void) -> *const std::ffi::c_char; fn nft_ctx_get_error_buffer(ctx: *mut std::ffi::c_void) -> *const std::ffi::c_char; fn nft_run_cmd_from_buffer( ctx: *mut std::ffi::c_void, buf: *const std::ffi::c_char, ) -> std::ffi::c_int; } /// Safe RAII wrapper around `struct nft_ctx*`. pub struct NftContext { raw: *mut std::ffi::c_void, } unsafe impl Send for NftContext {} unsafe impl Sync for NftContext {} impl NftContext { /// Create a new in-process nftables Netlink context with buffered I/O. pub fn new() -> Result { let raw = unsafe { nft_ctx_new(0) }; if raw.is_null() { return Err(NetworkError::Nftables( "Failed to allocate nftables context".to_string(), )); } unsafe { nft_ctx_buffer_output(raw); nft_ctx_buffer_error(raw); } Ok(Self { raw }) } /// Execute a command buffer directly against the kernel via Netlink. pub fn run_cmd(&mut self, cmd: &str) -> std::result::Result { let c_cmd = std::ffi::CString::new(cmd) .map_err(|e| (-1, format!("CString conversion failed: {e}")))?; let rc = unsafe { nft_run_cmd_from_buffer(self.raw, c_cmd.as_ptr()) }; let output = unsafe { let ptr = nft_ctx_get_output_buffer(self.raw); if ptr.is_null() { String::new() } else { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() } }; let error = unsafe { let ptr = nft_ctx_get_error_buffer(self.raw); if ptr.is_null() { String::new() } else { std::ffi::CStr::from_ptr(ptr).to_string_lossy().into_owned() } }; if rc == 0 { Ok(output) } else { Err((rc, error)) } } } impl Drop for NftContext { fn drop(&mut self) { if !self.raw.is_null() { unsafe { nft_ctx_free(self.raw) }; self.raw = std::ptr::null_mut(); } } } /// Structured diagnostic telemetry for Linux nftables kernel state. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct FirewallDiagnostics { pub table_exists: bool, pub table_name: String, pub family: String, pub chain_count: usize, pub chains: Vec, pub rule_count: usize, pub nat_enabled: bool, pub live_ruleset: Option, pub kernel_status: String, } /// Controller for the dedicated `nx9_wg` nftables table and chains in the Linux kernel. #[derive(Debug, Clone, Default)] pub struct NativeLinuxNftablesEngine; impl NativeLinuxNftablesEngine { /// Create a new native nftables engine instance. pub fn new() -> Self { Self } /// Check if the dedicated `table inet nx9_wg` exists in the kernel. pub async fn table_exists(&self) -> Result { let mut ctx = NftContext::new()?; match ctx.run_cmd("list table inet nx9_wg") { Ok(_) => Ok(true), Err((_, err)) => { if err.contains("No such file or directory") || err.contains("does not exist") { Ok(false) } else if err.contains("Permission denied") || err.contains("Operation not permitted") { Err(NetworkError::PermissionDenied(err)) } else { Err(NetworkError::Nftables(err)) } } } } /// Query the active `table inet nx9_wg` ruleset directly from the kernel. pub async fn get_live_ruleset(&self) -> Result { let mut ctx = NftContext::new()?; match ctx.run_cmd("list table inet nx9_wg") { Ok(output) => Ok(output), Err((_, err)) => { if err.contains("No such file or directory") || err.contains("does not exist") { Ok(String::new()) } else if err.contains("Permission denied") || err.contains("Operation not permitted") { Err(NetworkError::PermissionDenied(err)) } else { Err(NetworkError::Nftables(err)) } } } } /// Apply an atomic ruleset update to `table inet nx9_wg`. /// /// # Safety and Ownership Invariant /// Verifies that the ruleset ONLY modifies `table inet nx9_wg`. /// Never flushes or deletes tables outside `nx9_wg`. pub async fn apply_ruleset(&self, ruleset: &str) -> Result<()> { // Enforce ownership: reject any ruleset targeting outside table inet nx9_wg for line in ruleset.lines() { let trimmed = line.trim(); if (trimmed.starts_with("table ") || trimmed.starts_with("flush table ") || trimmed.starts_with("delete table ")) && !trimmed.contains("table inet nx9_wg") { return Err(NetworkError::FirewallOwnershipViolation(format!( "Refusing to execute command outside 'table inet nx9_wg': {trimmed}" ))); } if trimmed == "flush ruleset" { return Err(NetworkError::FirewallOwnershipViolation( "Refusing to flush global nftables ruleset".to_string(), )); } } // Construct atomic table replacement transaction let atomic_tx = format!("table inet nx9_wg\ndelete table inet nx9_wg\n{ruleset}"); let mut ctx = NftContext::new()?; match ctx.run_cmd(&atomic_tx) { Ok(_) => { tracing::info!("Atomic nftables ruleset applied for 'table inet nx9_wg'"); Ok(()) } Err((rc, err)) => { if err.contains("Permission denied") || err.contains("Operation not permitted") { Err(NetworkError::PermissionDenied(format!( "Insufficient privileges to modify kernel nftables (requires CAP_NET_ADMIN): {err}" ))) } else { Err(NetworkError::Nftables(format!( "Failed to apply atomic nftables transaction (exit code {rc}): {err}" ))) } } } } /// Delete the dedicated `table inet nx9_wg` from the kernel. pub async fn delete_table(&self) -> Result<()> { let mut ctx = NftContext::new()?; match ctx.run_cmd("delete table inet nx9_wg") { Ok(_) => { tracing::info!("Deleted 'table inet nx9_wg' from kernel"); Ok(()) } Err((_, err)) => { if err.contains("No such file or directory") || err.contains("does not exist") { Ok(()) } else if err.contains("Permission denied") || err.contains("Operation not permitted") { Err(NetworkError::PermissionDenied(err)) } else { Err(NetworkError::Nftables(err)) } } } } /// Produce read-only diagnostic telemetry for firewall and NAT state. pub async fn diagnose( &self, _desired_rules: &[FirewallRule], desired_nat: bool, ) -> Result { let mut ctx = NftContext::new()?; match ctx.run_cmd("list table inet nx9_wg") { Ok(live) => { let chain_input = live.contains("chain input"); let chain_forward = live.contains("chain forward"); let chain_postrouting = live.contains("chain postrouting"); let mut chains = Vec::new(); if chain_input { chains.push("input".to_string()); } if chain_forward { chains.push("forward".to_string()); } if chain_postrouting { chains.push("postrouting".to_string()); } let rule_count = live .lines() .filter(|l| { let t = l.trim(); !t.is_empty() && !t.starts_with('#') && !t.starts_with("table ") && !t.starts_with("chain ") && !t.starts_with('}') && !t.starts_with("type ") }) .count(); let nat_enabled = live.contains("masquerade"); Ok(FirewallDiagnostics { table_exists: true, table_name: "nx9_wg".to_string(), family: "inet".to_string(), chain_count: chains.len(), chains, rule_count, nat_enabled, live_ruleset: Some(live), kernel_status: "active".to_string(), }) } Err((_, err)) => { let exists = !err.contains("No such file or directory") && !err.contains("does not exist"); Ok(FirewallDiagnostics { table_exists: exists, table_name: "nx9_wg".to_string(), family: "inet".to_string(), chain_count: 0, chains: Vec::new(), rule_count: 0, nat_enabled: desired_nat, live_ruleset: None, kernel_status: if exists { err } else { "not_found".to_string() }, }) } } } } // ============================================================================ // NetworkEngine Trait Implementation // ============================================================================ #[async_trait::async_trait] impl NetworkEngine for NativeLinuxNetworkEngine { /// Deterministically reconcile kernel routing table entries with desired routes. /// /// Preserves unmanaged system routes and default gateways while synchronizing /// nx9-wg desired routes. async fn sync_routes(&self, routes: &[Route]) -> Result<()> { let live_routes = list_routes().await.unwrap_or_default(); let enabled_routes: Vec<&Route> = routes.iter().filter(|r| r.enabled).collect(); let disabled_routes: Vec<&Route> = routes.iter().filter(|r| !r.enabled).collect(); // 1. Add or converge missing/changed enabled routes for desired in &enabled_routes { let matches_live = live_routes.iter().any(|live| { live.destination == desired.destination && (desired.gateway.is_none() || live.gateway == desired.gateway) }); if !matches_live && let Err(e) = add_route(desired).await { tracing::warn!(error = %e, route = %desired.destination, "Kernel route addition skipped (unprivileged or missing CAP_NET_ADMIN)"); } } // 2. Remove explicitly disabled routes that are present in the kernel for disabled in &disabled_routes { let matches_live = live_routes.iter().any(|live| { live.destination == disabled.destination && (disabled.gateway.is_none() || live.gateway == disabled.gateway) }); if matches_live { let _ = delete_route(disabled).await; } } tracing::debug!( active = enabled_routes.len(), disabled = disabled_routes.len(), "Native Linux kernel routes synchronized via RTNETLINK" ); Ok(()) } /// Synchronize the dedicated `table inet nx9_wg` nftables ruleset. async fn sync_firewall( &self, rules: &[FirewallRule], enable_nat: bool, wg_subnets: &[IpNet], ) -> Result<()> { let ruleset = NftablesRulesetBuilder::build(rules, enable_nat, wg_subnets); { let mut active = self.active_ruleset.write().await; *active = ruleset.clone(); } let nft = NativeLinuxNftablesEngine::new(); match nft.apply_ruleset(&ruleset).await { Ok(()) => { tracing::info!( "Native Linux nftables 'table inet nx9_wg' synchronized successfully via Netlink" ); Ok(()) } Err(e) => { tracing::warn!(error = %e, "Kernel nftables application skipped (unprivileged or non-root context)"); Ok(()) } } } /// Inspect kernel IP packet forwarding status via /proc/sys/net. async fn get_forwarding_status(&self) -> Result { IpForwardingStatus::detect() } /// Get current active generated or live nftables ruleset. async fn get_active_nftables_ruleset(&self) -> Result { let nft = NativeLinuxNftablesEngine::new(); match nft.get_live_ruleset().await { Ok(live) if !live.trim().is_empty() => Ok(live), _ => { let active = self.active_ruleset.read().await; if active.is_empty() { Ok(NftablesRulesetBuilder::build(&[], true, &[])) } else { Ok(active.clone()) } } } } async fn has_route_drift(&self, routes: &[Route]) -> Result { let live_routes = list_routes().await.unwrap_or_default(); let enabled_routes: Vec<&Route> = routes.iter().filter(|r| r.enabled).collect(); let disabled_routes: Vec<&Route> = routes.iter().filter(|r| !r.enabled).collect(); // 1. Any enabled route missing from live routes? for desired in &enabled_routes { let found = live_routes.iter().any(|live| { live.destination == desired.destination && (desired.gateway.is_none() || live.gateway == desired.gateway) }); if !found { return Ok(true); } } // 2. Any disabled route still present in live routes? for disabled in &disabled_routes { let found = live_routes.iter().any(|live| { live.destination == disabled.destination && (disabled.gateway.is_none() || live.gateway == disabled.gateway) }); if found { return Ok(true); } } Ok(false) } }