622 lines
24 KiB
Rust
622 lines
24 KiB
Rust
use anyhow::{Context, Result};
|
|
use futures::stream::TryStreamExt;
|
|
use netlink_packet_route::route::RouteProtocol;
|
|
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.
|
|
/// Tracks every route it installs and removes exactly those on teardown.
|
|
/// Pre-existing routes are never touched.
|
|
pub struct RouteManager {
|
|
handle: Handle,
|
|
added_routes: Vec<(Ipv4Addr, u8)>,
|
|
tun_interface: String,
|
|
tun_index: Option<u32>,
|
|
dns_manager: crate::dns::DnsManager,
|
|
// Saved during setup for re-adding the server host route after resume.
|
|
server_ip: Option<Ipv4Addr>,
|
|
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 {
|
|
pub async fn new(tun_interface: String) -> Result<Self> {
|
|
let (connection, handle, _) =
|
|
new_connection().context("Failed to create netlink connection")?;
|
|
|
|
tokio::spawn(connection);
|
|
|
|
Ok(Self {
|
|
handle,
|
|
added_routes: Vec::new(),
|
|
tun_interface: tun_interface.clone(),
|
|
tun_index: None,
|
|
dns_manager: crate::dns::DnsManager::new(tun_interface, None, IpAddr::V4(Ipv4Addr::UNSPECIFIED)),
|
|
server_ip: None,
|
|
wifi_gateway: None,
|
|
vpn_gateway: None,
|
|
added_policy_rule: false,
|
|
})
|
|
}
|
|
|
|
async fn get_interface_index(&self, name: &str) -> Result<u32> {
|
|
let mut links = self
|
|
.handle
|
|
.link()
|
|
.get()
|
|
.match_name(name.to_string())
|
|
.execute();
|
|
|
|
if let Some(link) = links.try_next().await? {
|
|
Ok(link.header.index)
|
|
} else {
|
|
anyhow::bail!("Interface {} not found", name)
|
|
}
|
|
}
|
|
|
|
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 {
|
|
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));
|
|
}
|
|
}
|
|
|
|
// 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.
|
|
///
|
|
/// Uses metric 50 (lower than typical DHCP default of 100) so VPN routes
|
|
/// win without deleting the original default route.
|
|
pub async fn setup_vpn_routes(
|
|
&mut self,
|
|
server_addr: &str,
|
|
vpn_gateway: IpAddr,
|
|
routes: &[String],
|
|
) -> Result<()> {
|
|
info!("Setting up VPN routes...");
|
|
|
|
self.tun_index = Some(
|
|
self.get_interface_index(&self.tun_interface)
|
|
.await
|
|
.context("Failed to get TUN interface index")?,
|
|
);
|
|
info!(
|
|
"TUN interface {} has index {}",
|
|
self.tun_interface,
|
|
self.tun_index.unwrap()
|
|
);
|
|
|
|
let current_gateway = self
|
|
.get_default_gateway()
|
|
.await
|
|
.context("Failed to get current gateway")?;
|
|
info!("Current gateway: {}", current_gateway);
|
|
|
|
// Resolve server address to IP
|
|
let server_ip = if let Ok(ip) = server_addr.parse::<IpAddr>() {
|
|
ip
|
|
} else {
|
|
let host = server_addr.split(':').next().unwrap_or(server_addr);
|
|
let addrs = tokio::net::lookup_host(format!("{}:0", host))
|
|
.await
|
|
.context("Failed to resolve server hostname")?;
|
|
addrs
|
|
.map(|addr| addr.ip())
|
|
.next()
|
|
.context("No IP address found for server")?
|
|
};
|
|
info!("Server IP: {}", server_ip);
|
|
|
|
// Host route for VPN server through current gateway so the tunnel itself
|
|
// doesn't loop back through itself.
|
|
if let IpAddr::V4(server_v4) = server_ip {
|
|
self.server_ip = Some(server_v4);
|
|
self.wifi_gateway = Some(current_gateway);
|
|
|
|
let result = self
|
|
.handle
|
|
.route()
|
|
.add()
|
|
.v4()
|
|
.destination_prefix(server_v4, 32)
|
|
.gateway(current_gateway)
|
|
.priority(VPN_ROUTE_METRIC)
|
|
.execute()
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => self.added_routes.push((server_v4, 32)),
|
|
Err(e) => {
|
|
let err_msg = format!("{:?}", e);
|
|
if err_msg.contains("File exists")
|
|
|| err_msg.contains("EEXIST")
|
|
|| err_msg.contains("code: Some(-17)")
|
|
{
|
|
warn!("Server route already exists, continuing...");
|
|
} else {
|
|
return Err(anyhow::anyhow!("Failed to add server route: {:#}", e));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
warn!("IPv6 server addresses not yet supported for routing");
|
|
}
|
|
|
|
let IpAddr::V4(gateway_v4) = vpn_gateway else {
|
|
return Err(anyhow::anyhow!("IPv6 VPN gateways not supported"));
|
|
};
|
|
self.vpn_gateway = Some(gateway_v4);
|
|
|
|
let is_full_tunnel = routes.iter().any(|r| r == "0.0.0.0/0");
|
|
|
|
if is_full_tunnel {
|
|
info!("Full-tunnel mode: adding default route via VPN...");
|
|
self.handle
|
|
.route()
|
|
.add()
|
|
.v4()
|
|
.destination_prefix(Ipv4Addr::new(0, 0, 0, 0), 0)
|
|
.gateway(gateway_v4)
|
|
.output_interface(self.tun_index.unwrap())
|
|
.priority(VPN_ROUTE_METRIC)
|
|
.protocol(RouteProtocol::Boot)
|
|
.execute()
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to add VPN default route: {:?}", e))?;
|
|
|
|
self.added_routes.push((Ipv4Addr::new(0, 0, 0, 0), 0));
|
|
|
|
self.install_policy_rule().await;
|
|
} else {
|
|
info!(
|
|
"Split-tunnel mode: adding {} routes via VPN...",
|
|
routes.len()
|
|
);
|
|
for route_str in routes {
|
|
let Some((dest_str, prefix_str)) = route_str.split_once('/') else {
|
|
warn!("Invalid route format: {}", route_str);
|
|
continue;
|
|
};
|
|
let dest: Ipv4Addr = match dest_str.parse() {
|
|
Ok(ip) => ip,
|
|
Err(_) => {
|
|
warn!("Invalid route destination: {}", dest_str);
|
|
continue;
|
|
}
|
|
};
|
|
let prefix_len: u8 = match prefix_str.parse() {
|
|
Ok(p) => p,
|
|
Err(_) => {
|
|
warn!("Invalid prefix length: {}", prefix_str);
|
|
continue;
|
|
}
|
|
};
|
|
|
|
let result = self
|
|
.handle
|
|
.route()
|
|
.add()
|
|
.v4()
|
|
.destination_prefix(dest, prefix_len)
|
|
.gateway(gateway_v4)
|
|
.output_interface(self.tun_index.unwrap())
|
|
.priority(VPN_ROUTE_METRIC)
|
|
.execute()
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => {
|
|
self.added_routes.push((dest, prefix_len));
|
|
info!("Added route: {}/{}", dest, prefix_len);
|
|
}
|
|
Err(e) => {
|
|
let err_msg = format!("{:?}", e);
|
|
if err_msg.contains("File exists")
|
|
|| err_msg.contains("EEXIST")
|
|
|| err_msg.contains("code: Some(-17)")
|
|
{
|
|
warn!("Route {} already exists, continuing...", route_str);
|
|
} else {
|
|
warn!("Failed to add route {}: {:?}", route_str, e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
info!("✅ VPN routes configured successfully");
|
|
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() {
|
|
return Ok(());
|
|
}
|
|
|
|
self.dns_manager =
|
|
crate::dns::DnsManager::new(self.tun_interface.clone(), self.tun_index, assigned_ip);
|
|
|
|
self.dns_manager.set_dns(dns_servers).await
|
|
}
|
|
|
|
/// Restore DNS configuration
|
|
pub async fn restore_dns(&self) -> Result<()> {
|
|
self.dns_manager.restore().await
|
|
}
|
|
|
|
/// Re-add the server host route if it has been removed by the kernel.
|
|
///
|
|
/// On Linux, suspending the machine brings wlan0 down, which causes the kernel
|
|
/// to purge all routes installed on that interface — including our host route for
|
|
/// the VPN server IP via the WiFi gateway. Without that route, reconnect SYNs
|
|
/// follow the VPN default route into tun0, get dropped, and time out. This
|
|
/// method must be called before each reconnect attempt.
|
|
pub async fn ensure_server_route(&mut self) -> Result<()> {
|
|
let (server_ip, gateway) = match (self.server_ip, self.wifi_gateway) {
|
|
(Some(s), Some(g)) => (s, g),
|
|
_ => return Ok(()),
|
|
};
|
|
|
|
let result = self
|
|
.handle
|
|
.route()
|
|
.add()
|
|
.v4()
|
|
.destination_prefix(server_ip, 32)
|
|
.gateway(gateway)
|
|
.priority(VPN_ROUTE_METRIC)
|
|
.execute()
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => {
|
|
info!(
|
|
"Re-added server host route {}/32 via {} (was removed on suspend)",
|
|
server_ip, gateway
|
|
);
|
|
}
|
|
Err(e) => {
|
|
let msg = format!("{:?}", e);
|
|
if msg.contains("File exists") || msg.contains("EEXIST") || msg.contains("code: Some(-17)") {
|
|
// Route is already present — nothing to do.
|
|
} else {
|
|
return Err(anyhow::anyhow!("Failed to re-add server route: {:#}", e));
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove the VPN default route (0.0.0.0/0) during a reconnect window so
|
|
/// traffic falls through to the WiFi default route (metric 600).
|
|
/// Call `resume_default_route()` after the new session is established.
|
|
pub async fn suspend_default_route(&mut self) -> Result<()> {
|
|
if !self.added_routes.contains(&(Ipv4Addr::new(0, 0, 0, 0), 0)) {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut routes = self.handle.route().get(IpVersion::V4).execute();
|
|
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::Oif(idx)
|
|
if Some(*idx) == self.tun_index
|
|
)
|
|
});
|
|
if is_ours {
|
|
to_delete = Some(route);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(route) = to_delete {
|
|
self.handle.route().del(route).execute().await.map_err(|e| {
|
|
anyhow::anyhow!("Failed to suspend VPN default route: {:?}", e)
|
|
})?;
|
|
info!("VPN default route suspended — traffic falls to WiFi during reconnect");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Re-add the VPN default route after a successful reconnect.
|
|
pub async fn resume_default_route(&mut self) -> Result<()> {
|
|
let gateway = match self.vpn_gateway {
|
|
Some(gw) => gw,
|
|
None => return Ok(()),
|
|
};
|
|
if !self.added_routes.contains(&(Ipv4Addr::new(0, 0, 0, 0), 0)) {
|
|
return Ok(());
|
|
}
|
|
|
|
let result = self
|
|
.handle
|
|
.route()
|
|
.add()
|
|
.v4()
|
|
.destination_prefix(Ipv4Addr::new(0, 0, 0, 0), 0)
|
|
.gateway(gateway)
|
|
.output_interface(self.tun_index.unwrap_or(0))
|
|
.priority(VPN_ROUTE_METRIC)
|
|
.protocol(RouteProtocol::Boot)
|
|
.execute()
|
|
.await;
|
|
|
|
match result {
|
|
Ok(_) => info!("VPN default route restored after reconnect"),
|
|
Err(e) => {
|
|
let msg = format!("{:?}", e);
|
|
if msg.contains("File exists") || msg.contains("EEXIST") || msg.contains("code: Some(-17)") {
|
|
// already present
|
|
} else {
|
|
return Err(anyhow::anyhow!("Failed to resume VPN default route: {:#}", e));
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove every route that was added by `setup_vpn_routes`.
|
|
/// Pre-existing routes are untouched.
|
|
pub async fn restore_routes(&mut self) -> Result<()> {
|
|
info!(
|
|
"Removing {} tracked VPN routes...",
|
|
self.added_routes.len()
|
|
);
|
|
|
|
// 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 {
|
|
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;
|
|
}
|
|
|
|
if self.added_routes.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut routes_to_delete = Vec::new();
|
|
let mut routes = self.handle.route().get(IpVersion::V4).execute();
|
|
|
|
while let Some(route) = routes.try_next().await? {
|
|
let prefix_len = route.header.destination_prefix_length;
|
|
|
|
let dest_ip: Option<Ipv4Addr> = if prefix_len == 0 {
|
|
Some(Ipv4Addr::new(0, 0, 0, 0))
|
|
} else {
|
|
route.attributes.iter().find_map(|nla| {
|
|
if let netlink_packet_route::route::RouteAttribute::Destination(addr) = nla {
|
|
use netlink_packet_route::route::RouteAddress;
|
|
if let RouteAddress::Inet(ipv4) = addr {
|
|
return Some(*ipv4);
|
|
}
|
|
}
|
|
None
|
|
})
|
|
};
|
|
|
|
if let Some(ip) = dest_ip {
|
|
if self.added_routes.contains(&(ip, prefix_len)) {
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for route in routes_to_delete {
|
|
if let Err(e) = self.handle.route().del(route).execute().await {
|
|
warn!("Failed to delete tracked route: {:?}", e);
|
|
}
|
|
}
|
|
|
|
self.added_routes.clear();
|
|
info!("✅ VPN routes removed");
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl Drop for RouteManager {
|
|
fn drop(&mut self) {
|
|
if !self.added_routes.is_empty() {
|
|
warn!(
|
|
"RouteManager dropped with {} unrestored routes — call restore_routes() before dropping",
|
|
self.added_routes.len()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[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()");
|
|
}
|
|
}
|