Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tun): auto route on macos #592

Merged
merged 4 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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