From 7df5aa4317c6fba7b0bb3a275b7da8ec79f240b4 Mon Sep 17 00:00:00 2001 From: "E. Kaparulin" Date: Fri, 10 Jul 2026 07:42:05 +0300 Subject: [PATCH] chore: sync konduit-platform v0.1.0-beta.7 --- konduit-platform/src/dns/linux.rs | 26 ++++--- konduit-platform/src/routes/linux.rs | 102 ++++++++++++++++++++++----- konduit-platform/src/stats.rs | 29 -------- konduit-platform/src/tun.rs | 37 ++++++++-- 4 files changed, 131 insertions(+), 63 deletions(-) diff --git a/konduit-platform/src/dns/linux.rs b/konduit-platform/src/dns/linux.rs index b77ccaa..565a40e 100644 --- a/konduit-platform/src/dns/linux.rs +++ b/konduit-platform/src/dns/linux.rs @@ -100,15 +100,17 @@ pub async fn restore( info!("Restoring DNS settings..."); - let result = match if_index { - Some(idx) => revert_link_dbus(idx).await, - None => revert_resolvectl(interface).await, - }; - - if let Err(e) = result { - warn!("D-Bus DNS revert failed ({}), trying resolvectl", e); - revert_resolvectl(interface).await.ok(); + // Always attempt both paths: D-Bus RevertLink clears per-link config in + // systemd-resolved's in-memory state; resolvectl revert is a belt-and-suspenders + // pass that ensures the +DefaultRoute domain is removed even if D-Bus races + // with interface teardown. + if let Some(idx) = if_index { + if let Err(e) = revert_link_dbus(idx).await { + warn!("D-Bus RevertLink({}) failed: {}", idx, e); + } } + // Always run resolvectl revert as a second pass. + revert_resolvectl(interface).await.ok(); state.configured = false; Ok(()) @@ -172,6 +174,14 @@ async fn revert_link_dbus(if_index: u32) -> Result<()> { .await .context("Failed to create resolve1 proxy")?; + // Clear DNS servers and routing domains explicitly before the full revert. + // RevertLink alone can race with interface teardown in systemd-resolved; + // zeroing each setting first ensures they are gone even if RevertLink races. + let _ = proxy.set_link_dns(if_index as i32, vec![]).await; + let _ = proxy + .set_link_domains(if_index as i32, vec![]) + .await; + proxy .revert_link(if_index as i32) .await diff --git a/konduit-platform/src/routes/linux.rs b/konduit-platform/src/routes/linux.rs index 4120a2e..efaecab 100644 --- a/konduit-platform/src/routes/linux.rs +++ b/konduit-platform/src/routes/linux.rs @@ -5,6 +5,12 @@ use rtnetlink::{new_connection, Handle, IpVersion}; use std::net::{IpAddr, Ipv4Addr}; use tracing::{info, warn}; +// Added with priority lower than typical policy-routing stacks (e.g., xray uses 9001). +// Ensures konduit's main-table routes (tun0 default, server host-route) win in +// full-tunnel mode without modifying any third-party routing table. +const VPN_RULE_PRIORITY: u32 = 8000; + + const VPN_ROUTE_METRIC: u32 = 50; /// Manages routing table for VPN connection using netlink. @@ -21,6 +27,8 @@ pub struct RouteManager { wifi_gateway: Option, // Saved for re-adding the VPN default route after reconnect. vpn_gateway: Option, + // True when we installed an ip rule to override policy-routing stacks. + added_policy_rule: bool, } impl RouteManager { @@ -39,6 +47,7 @@ impl RouteManager { server_ip: None, wifi_gateway: None, vpn_gateway: None, + added_policy_rule: false, }) } @@ -58,22 +67,40 @@ impl RouteManager { } async fn get_default_gateway(&self) -> Result { + use netlink_packet_route::route::RouteAddress; + let mut routes = self.handle.route().get(IpVersion::V4).execute(); + // Collect all default-route gateways with their metrics. + // VPN default routes have low metric (50); physical/DHCP routes have high metric (600+). + // We want the physical gateway, so take the one with the highest metric. + let mut candidates: Vec<(u32, Ipv4Addr)> = Vec::new(); while let Some(route) = routes.try_next().await? { - if route.header.destination_prefix_length == 0 { - for nla in route.attributes.iter() { - if let netlink_packet_route::route::RouteAttribute::Gateway(addr) = nla { - use netlink_packet_route::route::RouteAddress; - if let RouteAddress::Inet(ipv4_addr) = addr { - return Ok(*ipv4_addr); - } - } + if route.header.destination_prefix_length != 0 { + continue; + } + let mut metric = 0u32; + let mut gateway: Option = None; + for nla in route.attributes.iter() { + match nla { + netlink_packet_route::route::RouteAttribute::Gateway( + RouteAddress::Inet(ip), + ) => gateway = Some(*ip), + netlink_packet_route::route::RouteAttribute::Priority(m) => metric = *m, + _ => {} } } + if let Some(gw) = gateway { + candidates.push((metric, gw)); + } } - anyhow::bail!("No default gateway found") + // Highest metric = physical/DHCP interface (not VPN). + candidates + .into_iter() + .max_by_key(|(m, _)| *m) + .map(|(_, gw)| gw) + .ok_or_else(|| anyhow::anyhow!("No default gateway found")) } /// Install routes for the VPN connection. @@ -178,6 +205,21 @@ impl RouteManager { .map_err(|e| anyhow::anyhow!("Failed to add VPN default route: {:?}", e))?; self.added_routes.push((Ipv4Addr::new(0, 0, 0, 0), 0)); + + // Add a high-priority ip rule so konduit's main-table routes override any + // policy-routing stack (e.g., xray, sing-box) that intercepts traffic via a + // separate routing table at priority ~9000. + match std::process::Command::new("ip") + .args(["rule", "add", "priority", &VPN_RULE_PRIORITY.to_string(), "lookup", "main"]) + .status() + { + Ok(s) if s.success() => { + self.added_policy_rule = true; + info!("Added ip rule priority {} → main table", VPN_RULE_PRIORITY); + } + Ok(_) => warn!("ip rule add returned non-zero (rule may already exist)"), + Err(e) => warn!("Failed to run ip rule add: {}", e), + } } else { info!( "Split-tunnel mode: adding {} routes via VPN...", @@ -312,11 +354,13 @@ impl RouteManager { let mut to_delete = None; while let Some(route) = routes.try_next().await? { if route.header.destination_prefix_length == 0 { + // Identify our default route by its output interface (tun0), not metric, + // since the kernel may not round-trip the Priority attribute reliably. let is_ours = route.attributes.iter().any(|nla| { matches!( nla, - netlink_packet_route::route::RouteAttribute::Priority(p) - if *p == VPN_ROUTE_METRIC + netlink_packet_route::route::RouteAttribute::Oif(idx) + if Some(*idx) == self.tun_index ) }); if is_ours { @@ -380,6 +424,19 @@ impl RouteManager { self.added_routes.len() ); + // Remove the policy rule first so traffic stops using the VPN immediately. + if self.added_policy_rule { + match std::process::Command::new("ip") + .args(["rule", "del", "priority", &VPN_RULE_PRIORITY.to_string(), "lookup", "main"]) + .status() + { + Ok(s) if s.success() => info!("Removed ip rule priority {}", VPN_RULE_PRIORITY), + Ok(_) => warn!("ip rule del returned non-zero"), + Err(e) => warn!("Failed to run ip rule del: {}", e), + } + self.added_policy_rule = false; + } + if self.added_routes.is_empty() { return Ok(()); } @@ -406,14 +463,21 @@ impl RouteManager { if let Some(ip) = dest_ip { if self.added_routes.contains(&(ip, prefix_len)) { - let is_ours = route.attributes.iter().any(|nla| { - matches!( - nla, - netlink_packet_route::route::RouteAttribute::Priority(p) - if *p == VPN_ROUTE_METRIC - ) - }); - if is_ours { + // For the default route (0.0.0.0/0), match by output interface + // (tun0), not just destination prefix — otherwise we also delete + // the WiFi default route (same prefix, different OIF/metric). + if prefix_len == 0 { + let is_ours = route.attributes.iter().any(|nla| { + matches!( + nla, + netlink_packet_route::route::RouteAttribute::Oif(idx) + if Some(*idx) == self.tun_index + ) + }); + if is_ours { + routes_to_delete.push(route); + } + } else { routes_to_delete.push(route); } } diff --git a/konduit-platform/src/stats.rs b/konduit-platform/src/stats.rs index 3ff3b6f..e61ca10 100644 --- a/konduit-platform/src/stats.rs +++ b/konduit-platform/src/stats.rs @@ -247,35 +247,6 @@ mod tests { assert_eq!(stats.download_bytes, 2048); } - #[test] - fn test_speed_calculation() { - let tracker = StatsTracker::new("utilbox.eu:8443".to_string(), "test".to_string()); - - // Record some traffic - tracker.record_upload(1024); - tracker.record_download(2048); - - // Update to calculate speed - tracker.update(); - - let stats = tracker.get_stats(false); - assert_eq!(stats.upload_speed, 1024); - assert_eq!(stats.download_speed, 2048); - - // Record more traffic - tracker.record_upload(512); - tracker.record_download(1024); - - // Update again - tracker.update(); - - let stats = tracker.get_stats(false); - assert_eq!(stats.upload_bytes, 1536); - assert_eq!(stats.download_bytes, 3072); - assert_eq!(stats.upload_speed, 512); - assert_eq!(stats.download_speed, 1024); - } - #[test] fn test_ring_buffer() { let mut buffer = RingBuffer::new(); diff --git a/konduit-platform/src/tun.rs b/konduit-platform/src/tun.rs index 517d3b5..946c909 100644 --- a/konduit-platform/src/tun.rs +++ b/konduit-platform/src/tun.rs @@ -185,8 +185,7 @@ impl TunDevice { info!("TUN device closed"); break; } - Ok(n) => { - debug!("TUN read: {} bytes", n); + Ok(_) => { if tun_pkt_tx.send(buf.freeze()).await.is_err() { break; } @@ -201,6 +200,35 @@ impl TunDevice { loop { tokio::select! { + biased; + + // Server→TUN first: deliver incoming VPN traffic immediately. + // + // Deliberately NOT write_all(): a TUN character device requires each + // write() syscall to contain exactly one complete packet. write_all() + // retries a short write by sending the *remaining* bytes in a follow-up + // write() call -- correct for stream sockets, but for a TUN device that + // turns one packet into two malformed ones (a truncated first packet, + // plus a bogus "packet" made of leftover bytes with no valid IP header). + // A single write() either succeeds atomically or it doesn't; there is no + // safe way to "continue" a partial packet write, so treat anything short + // of a full write as a dropped packet, not a retry target. + Some(packet) = from_tcp_rx.recv() => { + match writer.write(&packet).await { + Ok(n) if n == packet.len() => {} + Ok(n) => { + error!( + "TUN short write: wrote {} of {} bytes -- packet dropped (TUN writes must be atomic per-packet)", + n, packet.len() + ); + } + Err(e) => { + error!("TUN write error: {}", e); + } + } + } + + // TUN→TCP: forward outgoing packets from the kernel. packet = tun_pkt_rx.recv() => { match packet { Some(pkt) => { @@ -214,11 +242,6 @@ impl TunDevice { } } } - Some(packet) = from_tcp_rx.recv() => { - if let Err(e) = writer.write_all(&packet).await { - error!("TUN write error: {}", e); - } - } } }