From 10499b69bfb2d6d2de2c55b21921d04c90cf2845 Mon Sep 17 00:00:00 2001 From: Yuwei Ba Date: Tue, 17 Sep 2024 20:31:36 +1000 Subject: [PATCH] feat(tun): auto route on macos (#592) --- clash/tests/data/config/rules.yaml | 10 +- clash_lib/src/proxy/tun/inbound.rs | 12 +++ clash_lib/src/proxy/tun/routes/macos.rs | 116 +++++++++++++++++++++++- clash_lib/src/proxy/tun/routes/mod.rs | 7 ++ 4 files changed, 136 insertions(+), 9 deletions(-) diff --git a/clash/tests/data/config/rules.yaml b/clash/tests/data/config/rules.yaml index c1c58c93..0819cf0d 100644 --- a/clash/tests/data/config/rules.yaml +++ b/clash/tests/data/config/rules.yaml @@ -4,13 +4,13 @@ socks-port: 8889 mixed-port: 8899 tun: - enable: false + enable: true device-id: "dev://utun1989" - route-all: false + route-all: true gateway: "198.19.0.1/32" - routes: - - 0.0.0.0/1 - - 128.0.0.0/1 + # routes: + # - 0.0.0.0/1 + # - 128.0.0.0/1 ipv6: true diff --git a/clash_lib/src/proxy/tun/inbound.rs b/clash_lib/src/proxy/tun/inbound.rs index f1dd8edf..8af5167d 100644 --- a/clash_lib/src/proxy/tun/inbound.rs +++ b/clash_lib/src/proxy/tun/inbound.rs @@ -19,6 +19,11 @@ use crate::{ Error, Runner, }; +#[cfg(target_os = "macos")] +use crate::defer; +#[cfg(target_os = "macos")] +use crate::proxy::tun::routes; + async fn handle_inbound_stream( stream: netstack::TcpStream, local_addr: SocketAddr, @@ -200,6 +205,13 @@ pub fn get_runner( netstack::NetStack::with_buffer_size(512, 256).map_err(map_io_error)?; Ok(Some(Box::pin(async move { + #[cfg(target_os = "macos")] + defer! { + warn!("cleaning up routes"); + + let _ = routes::maybe_routes_clean_up(); + } + let framed = tun.into_framed(); let (mut tun_sink, mut tun_stream) = framed.split(); diff --git a/clash_lib/src/proxy/tun/routes/macos.rs b/clash_lib/src/proxy/tun/routes/macos.rs index 45cdacff..e78c86c1 100644 --- a/clash_lib/src/proxy/tun/routes/macos.rs +++ b/clash_lib/src/proxy/tun/routes/macos.rs @@ -1,9 +1,117 @@ +use std::net::Ipv4Addr; + use ipnet::IpNet; use tracing::warn; -use crate::proxy::utils::OutboundInterface; +use crate::{ + common::errors::new_io_error, + proxy::utils::{get_outbound_interface, OutboundInterface}, +}; + +/// let's assume that the `route` command is available on macOS +pub fn add_route(via: &OutboundInterface, dest: &IpNet) -> std::io::Result<()> { + let cmd = std::process::Command::new("route") + .arg("add") + .arg("-net") + .arg(dest.to_string()) + .arg("-interface") + .arg(&via.name) + .output()?; + + warn!("executing: route add -net {} -interface {}", dest, via.name); + if !cmd.status.success() { + Err(new_io_error("add route failed")) + } else { + Ok(()) + } +} + +fn get_default_gateway() -> std::io::Result> { + let cmd = std::process::Command::new("route") + .arg("-n") + .arg("get") + .arg("default") + .output()?; + + if !cmd.status.success() { + return Ok(None); + } + + let output = String::from_utf8_lossy(&cmd.stdout); + + let mut gateway = None; + for line in output.lines() { + if line.trim().contains("gateway:") { + gateway = line + .split_whitespace() + .last() + .and_then(|x| x.parse::().ok()); + break; + } + } + + Ok(gateway) +} + +/// it seems to be fine to add the default route multiple times +pub fn maybe_add_default_route() -> std::io::Result<()> { + let gateway = get_default_gateway()?; + if let Some(gateway) = gateway { + let default_interface = + get_outbound_interface().ok_or(new_io_error("get default interface"))?; + + let cmd = std::process::Command::new("route") + .arg("add") + .arg("-ifscope") + .arg(&default_interface.name) + .arg("0/0") + .arg(gateway.to_string()) + .output()?; + + warn!( + "executing: route add -ifscope {} 0/0 {}", + default_interface.name, gateway + ); + + if !cmd.status.success() { + Err(new_io_error("add default route failed")) + } else { + Ok(()) + } + } else { + Err(new_io_error( + "cant set default route, default gateway not found", + )) + } +} + +/// failing to delete the default route won't cause route failure +pub fn maybe_routes_clean_up() -> std::io::Result<()> { + let gateway = get_default_gateway()?; + if let Some(gateway) = gateway { + let default_interface = + get_outbound_interface().ok_or(new_io_error("get default interface"))?; + let cmd = std::process::Command::new("route") + .arg("delete") + .arg("-ifscope") + .arg(&default_interface.name) + .arg("0/0") + .arg(gateway.to_string()) + .output()?; + + warn!( + "executing: route delete -ifscope {} 0/0 {}", + default_interface.name, gateway + ); -pub fn add_route(_: &OutboundInterface, _: &IpNet) -> std::io::Result<()> { - warn!("add_route is not implemented on macOS"); - Ok(()) + if !cmd.status.success() { + Err(new_io_error("delete default route failed")) + } else { + Ok(()) + } + } else { + Err(new_io_error( + "cant delete default route, default gateway not found", + )) + } } diff --git a/clash_lib/src/proxy/tun/routes/mod.rs b/clash_lib/src/proxy/tun/routes/mod.rs index 4b7d1e85..e18dee30 100644 --- a/clash_lib/src/proxy/tun/routes/mod.rs +++ b/clash_lib/src/proxy/tun/routes/mod.rs @@ -7,6 +7,8 @@ use windows::add_route; mod macos; #[cfg(target_os = "macos")] use macos::add_route; +#[cfg(target_os = "macos")] +pub use macos::maybe_routes_clean_up; #[cfg(target_os = "linux")] mod linux; @@ -64,6 +66,11 @@ pub fn maybe_add_routes(cfg: &TunConfig, tun_name: &str) -> std::io::Result<()> for r in default_routes { add_route(&tun_iface, &r).map_err(map_io_error)?; } + + #[cfg(target_os = "macos")] + { + macos::maybe_add_default_route()?; + } } else { for r in &cfg.routes { add_route(&tun_iface, r).map_err(map_io_error)?;