chore: sync konduit-platform v0.1.0-beta.7

This commit is contained in:
E. Kaparulin
2026-07-10 07:42:05 +03:00
parent 99a2993ace
commit 7df5aa4317
4 changed files with 131 additions and 63 deletions

View File

@@ -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<Ipv4Addr>,
// Saved for re-adding the VPN default route after reconnect.
vpn_gateway: Option<Ipv4Addr>,
// 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<Ipv4Addr> {
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<Ipv4Addr> = 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);
}
}