Skip to content

Commit

Permalink
feat(tun): auto route on macos (#592)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibigbug authored Sep 17, 2024
1 parent 7ae8944 commit 10499b6
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 9 deletions.
10 changes: 5 additions & 5 deletions clash/tests/data/config/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions clash_lib/src/proxy/tun/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
116 changes: 112 additions & 4 deletions clash_lib/src/proxy/tun/routes/macos.rs
Original file line number Diff line number Diff line change
@@ -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<Option<Ipv4Addr>> {
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::<Ipv4Addr>().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",
))
}
}
7 changes: 7 additions & 0 deletions clash_lib/src/proxy/tun/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)?;
Expand Down

0 comments on commit 10499b6

Please sign in to comment.