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));
|
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
|
self.install_policy_rule().await;
|
||||||
// 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 {
|
} else {
|
||||||
info!(
|
info!(
|
||||||
"Split-tunnel mode: adding {} routes via VPN...",
|
"Split-tunnel mode: adding {} routes via VPN...",
|
||||||
@@ -281,6 +268,52 @@ impl RouteManager {
|
|||||||
Ok(())
|
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
|
/// Setup DNS configuration
|
||||||
pub async fn setup_dns(&mut self, dns_servers: &[IpAddr], assigned_ip: IpAddr) -> Result<()> {
|
pub async fn setup_dns(&mut self, dns_servers: &[IpAddr], assigned_ip: IpAddr) -> Result<()> {
|
||||||
if dns_servers.is_empty() {
|
if dns_servers.is_empty() {
|
||||||
@@ -425,14 +458,28 @@ impl RouteManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Remove the policy rule first so traffic stops using the VPN immediately.
|
// 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 {
|
if self.added_policy_rule {
|
||||||
match std::process::Command::new("ip")
|
let mut rules = self.handle.rule().get(IpVersion::V4).execute();
|
||||||
.args(["rule", "del", "priority", &VPN_RULE_PRIORITY.to_string(), "lookup", "main"])
|
let mut to_delete = None;
|
||||||
.status()
|
while let Some(rule) = rules.try_next().await? {
|
||||||
{
|
let matches_priority = rule
|
||||||
Ok(s) if s.success() => info!("Removed ip rule priority {}", VPN_RULE_PRIORITY),
|
.attributes
|
||||||
Ok(_) => warn!("ip rule del returned non-zero"),
|
.iter()
|
||||||
Err(e) => warn!("Failed to run ip rule del: {}", e),
|
.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;
|
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