chore: sync konduit-platform v0.1.0-beta.8
This commit is contained in:
@@ -206,20 +206,7 @@ impl RouteManager {
|
||||
|
||||
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),
|
||||
}
|
||||
self.install_policy_rule().await;
|
||||
} else {
|
||||
info!(
|
||||
"Split-tunnel mode: adding {} routes via VPN...",
|
||||
@@ -281,6 +268,52 @@ impl RouteManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Uses the same netlink handle as every other route change here, rather
|
||||
/// than shelling out to `ip rule add` -- a child process spawned via
|
||||
/// std::process::Command does NOT inherit capabilities granted to this
|
||||
/// process via `setcap`/file capabilities (they'd need to be raised into
|
||||
/// the ambient set first, which nothing here does), so the subprocess
|
||||
/// silently failed with EPERM ("RTNETLINK answers: Operation not
|
||||
/// permitted") every time, regardless of environment. That meant this
|
||||
/// rule never actually got installed, so a competing policy-routing
|
||||
/// stack (e.g. Xray) could keep overriding the VPN's own routes.
|
||||
async fn install_policy_rule(&mut self) {
|
||||
match self
|
||||
.handle
|
||||
.rule()
|
||||
.add()
|
||||
.v4()
|
||||
.priority(VPN_RULE_PRIORITY)
|
||||
// RuleAddRequest::new() defaults the rule's action to Unspec, not
|
||||
// ToTable. A "from all" rule (matches every packet) with an
|
||||
// Unspec action is not a valid "jump to main table" rule and the
|
||||
// kernel does not fall through past it — it broke route lookups
|
||||
// for ALL traffic, not just VPN traffic, until reboot.
|
||||
.action(netlink_packet_route::rule::RuleAction::ToTable)
|
||||
.execute()
|
||||
.await
|
||||
{
|
||||
Ok(()) => {
|
||||
self.added_policy_rule = true;
|
||||
info!("Added ip rule priority {} → main table", VPN_RULE_PRIORITY);
|
||||
}
|
||||
Err(e) => {
|
||||
let err_msg = format!("{:?}", e);
|
||||
if err_msg.contains("File exists") || err_msg.contains("EEXIST") || err_msg.contains("code: Some(-17)")
|
||||
{
|
||||
self.added_policy_rule = true;
|
||||
warn!("ip rule priority {} already exists, continuing...", VPN_RULE_PRIORITY);
|
||||
} else {
|
||||
warn!("Failed to add ip rule priority {}: {:?}", VPN_RULE_PRIORITY, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Setup DNS configuration
|
||||
pub async fn setup_dns(&mut self, dns_servers: &[IpAddr], assigned_ip: IpAddr) -> Result<()> {
|
||||
if dns_servers.is_empty() {
|
||||
@@ -425,14 +458,28 @@ impl RouteManager {
|
||||
);
|
||||
|
||||
// Remove the policy rule first so traffic stops using the VPN immediately.
|
||||
// Same netlink-handle approach as the add side above -- `handle.rule().del()`
|
||||
// needs the exact existing RuleMessage, so look it up by priority first.
|
||||
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),
|
||||
let mut rules = self.handle.rule().get(IpVersion::V4).execute();
|
||||
let mut to_delete = None;
|
||||
while let Some(rule) = rules.try_next().await? {
|
||||
let matches_priority = rule
|
||||
.attributes
|
||||
.iter()
|
||||
.any(|attr| matches!(attr, netlink_packet_route::rule::RuleAttribute::Priority(p) if *p == VPN_RULE_PRIORITY));
|
||||
if matches_priority {
|
||||
to_delete = Some(rule);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
match to_delete {
|
||||
Some(rule) => match self.handle.rule().del(rule).execute().await {
|
||||
Ok(()) => info!("Removed ip rule priority {}", VPN_RULE_PRIORITY),
|
||||
Err(e) => warn!("Failed to delete ip rule priority {}: {:?}", VPN_RULE_PRIORITY, e),
|
||||
},
|
||||
None => warn!("ip rule priority {} not found, nothing to remove", VPN_RULE_PRIORITY),
|
||||
}
|
||||
self.added_policy_rule = false;
|
||||
}
|
||||
@@ -506,3 +553,69 @@ impl Drop for RouteManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use netlink_packet_route::rule::RuleAction;
|
||||
|
||||
const REEXEC_ENV_VAR: &str = "KORIDOR_ROUTE_TEST_IN_NETNS";
|
||||
|
||||
// `unshare(CLONE_NEWUSER)` fails with EINVAL if the calling process is
|
||||
// multithreaded (which `cargo test`'s harness always is). So instead of
|
||||
// calling the syscall in-process, re-exec this same test binary under
|
||||
// the external `unshare --user --net --map-root-user` command, which
|
||||
// starts a fresh, single-threaded process already inside an isolated
|
||||
// user+net namespace -- no real root needed, and the host's actual
|
||||
// routing rules are never touched.
|
||||
fn run_in_isolated_netns(test_name: &str) {
|
||||
let exe = std::env::current_exe().expect("current_exe");
|
||||
let status = std::process::Command::new("unshare")
|
||||
.args(["--user", "--net", "--map-root-user", "--"])
|
||||
.arg(&exe)
|
||||
.args(["--exact", test_name, "--nocapture", "--test-threads=1"])
|
||||
.env(REEXEC_ENV_VAR, "1")
|
||||
.status()
|
||||
.expect("failed to spawn unshare (is util-linux's `unshare` installed?)");
|
||||
assert!(status.success(), "test failed inside isolated netns (see output above)");
|
||||
}
|
||||
|
||||
// Regression test for the bug where connecting broke ALL host networking
|
||||
// (not just VPN traffic) until a reboot: the policy rule installed to
|
||||
// make konduit's routes win over other policy-routing stacks was built
|
||||
// via `RuleAddRequest::new()`, which defaults `header.action` to
|
||||
// `RuleAction::Unspec` — a "from all" rule (matches every packet) with
|
||||
// no action is not a valid "jump to main table" rule, and the kernel
|
||||
// does not fall through to the next rule for it, breaking route lookups
|
||||
// system-wide. The fix must set the action explicitly to `ToTable`.
|
||||
#[tokio::test(flavor = "current_thread")]
|
||||
async fn policy_rule_uses_to_table_action() {
|
||||
if std::env::var(REEXEC_ENV_VAR).is_err() {
|
||||
run_in_isolated_netns("routes::linux::tests::policy_rule_uses_to_table_action");
|
||||
return;
|
||||
}
|
||||
|
||||
let mut mgr = RouteManager::new("lo".to_string())
|
||||
.await
|
||||
.expect("failed to create RouteManager in isolated netns");
|
||||
|
||||
mgr.install_policy_rule().await;
|
||||
|
||||
let mut rules = mgr.handle.rule().get(IpVersion::V4).execute();
|
||||
let mut found = false;
|
||||
while let Some(rule) = rules.try_next().await.unwrap() {
|
||||
let is_ours = rule.attributes.iter().any(|attr| {
|
||||
matches!(attr, netlink_packet_route::rule::RuleAttribute::Priority(p) if *p == VPN_RULE_PRIORITY)
|
||||
});
|
||||
if is_ours {
|
||||
assert_eq!(
|
||||
rule.header.action,
|
||||
RuleAction::ToTable,
|
||||
"policy rule must use the ToTable action, or the kernel treats it as a black hole for every packet it matches (i.e. everything)"
|
||||
);
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
assert!(found, "installed policy rule not found via rule().get()");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user