chore: sync konduit-platform
This commit is contained in:
444
konduit-platform/src/routes/linux.rs
Normal file
444
konduit-platform/src/routes/linux.rs
Normal file
@@ -0,0 +1,444 @@
|
||||
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};
|
||||
|
||||
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>,
|
||||
}
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
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> {
|
||||
let mut routes = self.handle.route().get(IpVersion::V4).execute();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("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));
|
||||
} 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(())
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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 {
|
||||
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()
|
||||
);
|
||||
|
||||
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)) {
|
||||
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 {
|
||||
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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
konduit-platform/src/routes/macos.rs
Normal file
198
konduit-platform/src/routes/macos.rs
Normal file
@@ -0,0 +1,198 @@
|
||||
use anyhow::Result;
|
||||
use std::net::IpAddr;
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub struct MacOsNetManager {
|
||||
pub tun_name: String,
|
||||
server_host: String,
|
||||
original_gateway: String,
|
||||
primary_service: String,
|
||||
}
|
||||
|
||||
impl MacOsNetManager {
|
||||
/// Discover the current default gateway and primary network service, then return a manager.
|
||||
/// Uses blocking `std::process::Command` — call from a sync context or `spawn_blocking`.
|
||||
pub fn new(tun_name: String, server_endpoint: &str) -> Result<Self> {
|
||||
let server_host = server_endpoint
|
||||
.split(':')
|
||||
.next()
|
||||
.unwrap_or(server_endpoint)
|
||||
.to_string();
|
||||
|
||||
let route_out = std::process::Command::new("route")
|
||||
.args(["-n", "get", "default"])
|
||||
.output()?;
|
||||
let route_text = String::from_utf8_lossy(&route_out.stdout).into_owned();
|
||||
let (original_gateway, iface) = Self::parse_default_route(&route_text)?;
|
||||
|
||||
let svc_out = std::process::Command::new("networksetup")
|
||||
.args(["-listnetworkserviceorder"])
|
||||
.output()?;
|
||||
let svc_text = String::from_utf8_lossy(&svc_out.stdout).into_owned();
|
||||
let primary_service = Self::parse_primary_service(&svc_text, &iface)
|
||||
.unwrap_or_else(|_| "Wi-Fi".to_string());
|
||||
|
||||
info!(
|
||||
"MacOsNetManager: gateway={}, service={}, tun={}",
|
||||
original_gateway, primary_service, tun_name
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
tun_name,
|
||||
server_host,
|
||||
original_gateway,
|
||||
primary_service,
|
||||
})
|
||||
}
|
||||
|
||||
// Always adds split-default (0/1 + 128/1) for full-tunnel mode.
|
||||
// Split-tunnel (per-route from ServerConfig) is not yet supported on macOS.
|
||||
pub async fn setup_vpn_routes(&self) -> Result<()> {
|
||||
run("route", &["add", "-host", &self.server_host, &self.original_gateway]).await;
|
||||
run("route", &["add", "-net", "0.0.0.0/1", "-interface", &self.tun_name]).await;
|
||||
run("route", &["add", "-net", "128.0.0.0/1", "-interface", &self.tun_name]).await;
|
||||
info!("macOS VPN routes added via {}", self.tun_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure DNS for the primary network service.
|
||||
pub async fn setup_dns(&self, dns_ips: &[IpAddr], _assigned_ip: IpAddr) -> Result<()> {
|
||||
if dns_ips.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let ip_strings: Vec<String> = dns_ips.iter().map(|ip| ip.to_string()).collect();
|
||||
let mut args: Vec<&str> = vec!["-setdnsservers", self.primary_service.as_str()];
|
||||
args.extend(ip_strings.iter().map(|s| s.as_str()));
|
||||
run("networksetup", &args).await;
|
||||
info!("macOS DNS configured: {:?}", dns_ips);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove split-default routes so traffic falls through to WiFi during reconnect.
|
||||
pub async fn suspend_default_route(&self) -> Result<()> {
|
||||
run("route", &["delete", "-net", "0.0.0.0/1"]).await;
|
||||
run("route", &["delete", "-net", "128.0.0.0/1"]).await;
|
||||
info!("macOS default route suspended");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Re-add split-default routes after successful reconnect.
|
||||
pub async fn resume_default_route(&self) -> Result<()> {
|
||||
run("route", &["add", "-net", "0.0.0.0/1", "-interface", &self.tun_name]).await;
|
||||
run("route", &["add", "-net", "128.0.0.0/1", "-interface", &self.tun_name]).await;
|
||||
info!("macOS default route resumed via {}", self.tun_name);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NOTE: original_gateway is captured once at construction time.
|
||||
// If the WiFi gateway changes (network roaming), this route may go via
|
||||
// the wrong gateway until the next reconnect that creates a new MacOsNetManager.
|
||||
pub async fn ensure_server_route(&self) -> Result<()> {
|
||||
run("route", &["delete", "-host", &self.server_host]).await;
|
||||
run("route", &["add", "-host", &self.server_host, &self.original_gateway]).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove all VPN routes.
|
||||
pub async fn restore_routes(&self) -> Result<()> {
|
||||
run("route", &["delete", "-host", &self.server_host]).await;
|
||||
run("route", &["delete", "-net", "0.0.0.0/1"]).await;
|
||||
run("route", &["delete", "-net", "128.0.0.0/1"]).await;
|
||||
info!("macOS VPN routes removed");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Revert DNS to automatic (empty = use DHCP-provided DNS).
|
||||
pub async fn restore_dns(&self) -> Result<()> {
|
||||
run("networksetup", &["-setdnsservers", &self.primary_service, "Empty"]).await;
|
||||
info!("macOS DNS restored to automatic");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_default_route(output: &str) -> Result<(String, String)> {
|
||||
let gateway = output
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with("gateway:"))
|
||||
.and_then(|l| l.splitn(2, ':').nth(1))
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not find default gateway in route output"))?;
|
||||
|
||||
let iface = output
|
||||
.lines()
|
||||
.find(|l| l.trim_start().starts_with("interface:"))
|
||||
.and_then(|l| l.splitn(2, ':').nth(1))
|
||||
.map(|s| s.trim().to_string())
|
||||
.ok_or_else(|| anyhow::anyhow!("Could not find default interface in route output"))?;
|
||||
|
||||
Ok((gateway, iface))
|
||||
}
|
||||
|
||||
fn parse_primary_service(output: &str, iface: &str) -> Result<String> {
|
||||
let lines: Vec<&str> = output.lines().collect();
|
||||
for i in 0..lines.len().saturating_sub(1) {
|
||||
if lines[i + 1].contains(&format!("Device: {}", iface)) {
|
||||
let name = lines[i].trim();
|
||||
let name = name.splitn(2, ") ").nth(1).unwrap_or(name.trim());
|
||||
return Ok(name.to_string());
|
||||
}
|
||||
}
|
||||
anyhow::bail!("Could not find network service for interface {}", iface)
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(cmd: &str, args: &[&str]) {
|
||||
match tokio::process::Command::new(cmd).args(args).output().await {
|
||||
Ok(out) if out.status.success() => {}
|
||||
Ok(out) => warn!(
|
||||
"{} {} failed: {}",
|
||||
cmd,
|
||||
args.join(" "),
|
||||
String::from_utf8_lossy(&out.stderr).trim()
|
||||
),
|
||||
Err(e) => warn!("Failed to run {}: {}", cmd, e),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ROUTE_OUTPUT: &str = " route to: default
|
||||
destination: default
|
||||
mask: default
|
||||
gateway: 192.168.1.1
|
||||
interface: en0
|
||||
flags: <UP,GATEWAY,DONE,STATIC,PRCLONING,GLOBAL>
|
||||
recvpipe sendpipe ssthresh rtt,msec rtt,var hopcount mtu expire
|
||||
0 0 0 35 128 0 1500 0 ";
|
||||
|
||||
const SERVICES_OUTPUT: &str = "An asterisk (*) denotes that a network service is disabled.
|
||||
(1) Wi-Fi
|
||||
(Hardware Port: Wi-Fi, Device: en0)
|
||||
|
||||
(2) Bluetooth PAN
|
||||
(Hardware Port: Bluetooth PAN, Device: en1)
|
||||
|
||||
(3) Thunderbolt Bridge
|
||||
(Hardware Port: Thunderbolt Bridge, Device: bridge0)
|
||||
";
|
||||
|
||||
#[test]
|
||||
fn parses_gateway_and_interface() {
|
||||
let (gw, iface) = MacOsNetManager::parse_default_route(ROUTE_OUTPUT).unwrap();
|
||||
assert_eq!(gw, "192.168.1.1");
|
||||
assert_eq!(iface, "en0");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_primary_service() {
|
||||
let svc = MacOsNetManager::parse_primary_service(SERVICES_OUTPUT, "en0").unwrap();
|
||||
assert_eq!(svc, "Wi-Fi");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_service_unknown_iface_returns_err() {
|
||||
let result = MacOsNetManager::parse_primary_service(SERVICES_OUTPUT, "en99");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
9
konduit-platform/src/routes/mod.rs
Normal file
9
konduit-platform/src/routes/mod.rs
Normal file
@@ -0,0 +1,9 @@
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use linux::RouteManager;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub use macos::MacOsNetManager;
|
||||
Reference in New Issue
Block a user