chore: sync konduit-platform v0.1.0-beta.3
This commit is contained in:
@@ -7,3 +7,8 @@ pub use linux::RouteManager;
|
||||
mod macos;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::MacOsNetManager;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod windows;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub use windows::RouteManager;
|
||||
|
||||
300
konduit-platform/src/routes/windows.rs
Normal file
300
konduit-platform/src/routes/windows.rs
Normal file
@@ -0,0 +1,300 @@
|
||||
use anyhow::Result;
|
||||
use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::os::windows::process::CommandExt;
|
||||
use tracing::{info, warn};
|
||||
|
||||
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
|
||||
|
||||
use crate::dns::DnsManager;
|
||||
|
||||
pub struct RouteManager {
|
||||
tun_interface: String,
|
||||
dns_manager: DnsManager,
|
||||
tun_routes: Vec<(Ipv4Addr, u8)>,
|
||||
server_ip: Option<Ipv4Addr>,
|
||||
wifi_gateway: Option<Ipv4Addr>,
|
||||
}
|
||||
|
||||
impl RouteManager {
|
||||
pub async fn new(tun_interface: String) -> Result<Self> {
|
||||
Ok(Self {
|
||||
dns_manager: DnsManager::new(
|
||||
tun_interface.clone(),
|
||||
None,
|
||||
IpAddr::V4(Ipv4Addr::UNSPECIFIED),
|
||||
),
|
||||
tun_interface,
|
||||
tun_routes: Vec::new(),
|
||||
server_ip: None,
|
||||
wifi_gateway: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_default_gateway() -> Result<Ipv4Addr> {
|
||||
let adapters = ipconfig::get_adapters()
|
||||
.map_err(|e| anyhow::anyhow!("Failed to enumerate adapters: {}", e))?;
|
||||
for adapter in &adapters {
|
||||
for gw in adapter.gateways() {
|
||||
if let IpAddr::V4(gw4) = gw {
|
||||
if !gw4.is_loopback() && !gw4.is_unspecified() {
|
||||
return Ok(*gw4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::bail!("No default gateway found")
|
||||
}
|
||||
|
||||
fn prefix_to_mask(prefix: u8) -> Ipv4Addr {
|
||||
let bits = if prefix == 0 {
|
||||
0u32
|
||||
} else {
|
||||
!0u32 << (32 - prefix)
|
||||
};
|
||||
let [a, b, c, d] = bits.to_be_bytes();
|
||||
Ipv4Addr::new(a, b, c, d)
|
||||
}
|
||||
|
||||
// Add/delete server host route through WiFi gateway via `route` command.
|
||||
// Specifying gateway on delete ensures we only remove our entry, not any other path.
|
||||
fn wifi_route_add(dest: Ipv4Addr, prefix: u8, gateway: Ipv4Addr) {
|
||||
let mask = Self::prefix_to_mask(prefix);
|
||||
let out = std::process::Command::new("route")
|
||||
.args([
|
||||
"add",
|
||||
&dest.to_string(),
|
||||
"MASK",
|
||||
&mask.to_string(),
|
||||
&gateway.to_string(),
|
||||
"METRIC",
|
||||
"1",
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
match out {
|
||||
Ok(o) => {
|
||||
let stdout = String::from_utf8_lossy(&o.stdout);
|
||||
if !o.status.success() {
|
||||
// "already exists" is acceptable — a previous run may have left it
|
||||
warn!("route add {}/{} via {}: {}", dest, prefix, gateway, stdout.trim());
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("route add {}/{} via {}: {}", dest, prefix, gateway, e),
|
||||
}
|
||||
}
|
||||
|
||||
fn wifi_route_delete(dest: Ipv4Addr, prefix: u8, gateway: Ipv4Addr) {
|
||||
let mask = Self::prefix_to_mask(prefix);
|
||||
let _ = std::process::Command::new("route")
|
||||
.args([
|
||||
"delete",
|
||||
&dest.to_string(),
|
||||
"MASK",
|
||||
&mask.to_string(),
|
||||
&gateway.to_string(),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.status();
|
||||
}
|
||||
|
||||
// Add/delete routes through the TUN interface using netsh.
|
||||
//
|
||||
// `netsh interface ipv4 add route` binds routes to a named interface explicitly,
|
||||
// avoiding the gateway-resolution ambiguity of the `route` command and ensuring
|
||||
// cleanup never touches WiFi routing state.
|
||||
fn tun_route_add(tun_name: &str, dest: Ipv4Addr, prefix: u8) {
|
||||
let prefix_str = format!("{}/{}", dest, prefix);
|
||||
let out = std::process::Command::new("netsh")
|
||||
.args([
|
||||
"interface",
|
||||
"ipv4",
|
||||
"add",
|
||||
"route",
|
||||
&prefix_str,
|
||||
tun_name,
|
||||
"nexthop=0.0.0.0",
|
||||
"metric=1",
|
||||
"store=active",
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
match out {
|
||||
Ok(o) => {
|
||||
let stdout = String::from_utf8_lossy(&o.stdout);
|
||||
if o.status.success() {
|
||||
info!("Added TUN route {}/{}", dest, prefix);
|
||||
} else {
|
||||
warn!("netsh add route {}/{} on {}: {}", dest, prefix, tun_name, stdout.trim());
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("netsh add route {}/{} on {}: {}", dest, prefix, tun_name, e),
|
||||
}
|
||||
}
|
||||
|
||||
fn tun_route_delete(tun_name: &str, dest: Ipv4Addr, prefix: u8) {
|
||||
let prefix_str = format!("{}/{}", dest, prefix);
|
||||
let out = std::process::Command::new("netsh")
|
||||
.args([
|
||||
"interface",
|
||||
"ipv4",
|
||||
"delete",
|
||||
"route",
|
||||
&prefix_str,
|
||||
tun_name,
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
match out {
|
||||
Ok(o) => {
|
||||
let stdout = String::from_utf8_lossy(&o.stdout);
|
||||
if !o.status.success() {
|
||||
warn!(
|
||||
"netsh delete route {}/{} on {}: {}",
|
||||
dest,
|
||||
prefix,
|
||||
tun_name,
|
||||
stdout.trim()
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("netsh delete route {}/{} on {}: {}", dest, prefix, tun_name, e),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn setup_vpn_routes(
|
||||
&mut self,
|
||||
server_addr: &str,
|
||||
_vpn_gateway: IpAddr,
|
||||
routes: &[String],
|
||||
) -> Result<()> {
|
||||
info!("Setting up Windows VPN routes...");
|
||||
|
||||
let current_gateway = Self::get_default_gateway()?;
|
||||
info!("Current gateway: {}", current_gateway);
|
||||
|
||||
let server_ip: Ipv4Addr = {
|
||||
let host = server_addr.split(':').next().unwrap_or(server_addr);
|
||||
if let Ok(IpAddr::V4(v4)) = host.parse::<IpAddr>() {
|
||||
v4
|
||||
} else {
|
||||
let addrs = tokio::net::lookup_host(format!("{}:0", host)).await?;
|
||||
match addrs.map(|a| a.ip()).next() {
|
||||
Some(IpAddr::V4(v4)) => v4,
|
||||
_ => anyhow::bail!("Could not resolve server address to IPv4"),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Always record server + gateway so cleanup can delete the host route
|
||||
// even if the add below fails because the route already exists.
|
||||
self.server_ip = Some(server_ip);
|
||||
self.wifi_gateway = Some(current_gateway);
|
||||
Self::wifi_route_add(server_ip, 32, current_gateway);
|
||||
|
||||
let is_full_tunnel = routes.iter().any(|r| r == "0.0.0.0/0");
|
||||
|
||||
if is_full_tunnel {
|
||||
// Two /1 routes on the TUN interface cover the entire address space and
|
||||
// always beat the existing WiFi /0 default route regardless of metric.
|
||||
// netsh with explicit interface name avoids gateway-resolution ambiguity.
|
||||
info!("Full-tunnel mode: adding /1 cover routes on {}...", self.tun_interface);
|
||||
let lower = Ipv4Addr::new(0, 0, 0, 0);
|
||||
let upper = Ipv4Addr::new(128, 0, 0, 0);
|
||||
Self::tun_route_add(&self.tun_interface, lower, 1);
|
||||
self.tun_routes.push((lower, 1));
|
||||
Self::tun_route_add(&self.tun_interface, upper, 1);
|
||||
self.tun_routes.push((upper, 1));
|
||||
} else {
|
||||
info!("Split-tunnel mode: adding {} routes via TUN...", 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 Ok(dest) = dest_str.parse::<Ipv4Addr>() else {
|
||||
warn!("Invalid route destination: {}", dest_str);
|
||||
continue;
|
||||
};
|
||||
let Ok(prefix_len) = prefix_str.parse::<u8>() else {
|
||||
warn!("Invalid prefix length: {}", prefix_str);
|
||||
continue;
|
||||
};
|
||||
Self::tun_route_add(&self.tun_interface, dest, prefix_len);
|
||||
self.tun_routes.push((dest, prefix_len));
|
||||
info!("Added split-tunnel route: {}", route_str);
|
||||
}
|
||||
}
|
||||
|
||||
info!("Windows VPN routes configured");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setup_dns(&mut self, dns_servers: &[IpAddr], assigned_ip: IpAddr) -> Result<()> {
|
||||
if dns_servers.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
self.dns_manager = DnsManager::new(self.tun_interface.clone(), None, assigned_ip);
|
||||
self.dns_manager.set_dns(dns_servers).await
|
||||
}
|
||||
|
||||
pub async fn restore_dns(&self) -> Result<()> {
|
||||
self.dns_manager.restore().await
|
||||
}
|
||||
|
||||
pub async fn ensure_server_route(&mut self) -> Result<()> {
|
||||
if let (Some(server_ip), Some(gateway)) = (self.server_ip, self.wifi_gateway) {
|
||||
Self::wifi_route_add(server_ip, 32, gateway);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn suspend_default_route(&mut self) -> Result<()> {
|
||||
let lower = Ipv4Addr::new(0, 0, 0, 0);
|
||||
if !self.tun_routes.contains(&(lower, 1)) {
|
||||
return Ok(());
|
||||
}
|
||||
for (dest, prefix) in [(lower, 1u8), (Ipv4Addr::new(128, 0, 0, 0), 1u8)] {
|
||||
Self::tun_route_delete(&self.tun_interface, dest, prefix);
|
||||
}
|
||||
info!("VPN cover routes suspended — traffic falls to WiFi during reconnect");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn resume_default_route(&mut self) -> Result<()> {
|
||||
let lower = Ipv4Addr::new(0, 0, 0, 0);
|
||||
if !self.tun_routes.contains(&(lower, 1)) {
|
||||
return Ok(());
|
||||
}
|
||||
let upper = Ipv4Addr::new(128, 0, 0, 0);
|
||||
for (dest, prefix) in [(lower, 1u8), (upper, 1u8)] {
|
||||
Self::tun_route_add(&self.tun_interface, dest, prefix);
|
||||
}
|
||||
info!("VPN cover routes restored after reconnect");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn restore_routes(&mut self) -> Result<()> {
|
||||
info!("Removing {} tracked VPN routes...", self.tun_routes.len());
|
||||
for (dest, prefix) in self.tun_routes.drain(..) {
|
||||
Self::tun_route_delete(&self.tun_interface, dest, prefix);
|
||||
}
|
||||
// Always clean up the server host route — a previous unclean exit may have
|
||||
// left it behind even if our add was skipped.
|
||||
if let (Some(server_ip), Some(gateway)) = (self.server_ip.take(), self.wifi_gateway.take()) {
|
||||
Self::wifi_route_delete(server_ip, 32, gateway);
|
||||
}
|
||||
info!("Windows VPN routes removed");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for RouteManager {
|
||||
fn drop(&mut self) {
|
||||
if !self.tun_routes.is_empty() {
|
||||
warn!(
|
||||
"RouteManager dropped with {} unrestored TUN routes — call restore_routes() before dropping",
|
||||
self.tun_routes.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user