Skip to content

Commit

Permalink
feat(tun): windows auto-route (#561)
Browse files Browse the repository at this point in the history
  • Loading branch information
ibigbug authored Aug 31, 2024
1 parent d163787 commit d6171fe
Show file tree
Hide file tree
Showing 15 changed files with 387 additions and 29 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ docs:
@cargo doc -p clash_doc --no-deps
@echo "<meta http-equiv=\"refresh\" content=\"0; url=clash_doc\">" > target/doc/index.html
@cp -r target/doc ./docs

test-no-docker:
CLASH_RS_CI=true cargo test --all --all-features
8 changes: 6 additions & 2 deletions clash/tests/data/config/rules.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ socks-port: 8889
mixed-port: 8899

tun:
enable: false
enable: true
device-id: "dev://utun1989"
route-all: false
routes:
- 0.0.0.0/1
- 128.0.0.0/1

ipv6: true

Expand Down Expand Up @@ -42,7 +46,7 @@ dns:
nameserver:
# - 114.114.114.114 # default value
# - 1.1.1.1#auto # default value
- tls://1.1.1.1:853#en0 # DNS over TLS
- tls://1.1.1.1:853#auto # DNS over TLS
# - dhcp://en0 # dns from dhcp

allow-lan: true
Expand Down
1 change: 1 addition & 0 deletions clash_lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -143,4 +143,5 @@ security-framework = "2.11.1"
windows = { version = "0.58", features = [
"Win32_Networking_WinSock",
"Win32_Foundation",
"Win32_NetworkManagement_Rras",
]}
3 changes: 2 additions & 1 deletion clash_lib/src/app/logging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ impl From<LogLevel> for filter::LevelFilter {
LogLevel::Warning => filter::LevelFilter::WARN,
LogLevel::Info => filter::LevelFilter::INFO,
LogLevel::Debug => filter::LevelFilter::DEBUG,
LogLevel::Trace => filter::LevelFilter::TRACE,
LogLevel::Silent => filter::LevelFilter::OFF,
}
}
Expand Down Expand Up @@ -65,7 +66,7 @@ where
tracing::Level::WARN => LogLevel::Warning,
tracing::Level::INFO => LogLevel::Info,
tracing::Level::DEBUG => LogLevel::Debug,
tracing::Level::TRACE => LogLevel::Debug,
tracing::Level::TRACE => LogLevel::Trace,
},
msg: strs.join(" "),
};
Expand Down
25 changes: 25 additions & 0 deletions clash_lib/src/common/defer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// https://stackoverflow.com/a/29963675/1109167
pub struct ScopeCall<F: FnOnce()> {
pub c: Option<F>,
}
impl<F: FnOnce()> Drop for ScopeCall<F> {
fn drop(&mut self) {
self.c.take().unwrap()()
}
}

#[macro_export]
macro_rules! expr {
($e:expr) => {
$e
};
} // tt hack

#[macro_export]
macro_rules! defer {
($($data: tt)*) => (
let _scope_call = $crate::common::defer::ScopeCall {
c: Some(|| -> () { $crate::expr!({ $($data)* }) })
};
)
}
1 change: 1 addition & 0 deletions clash_lib/src/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod auth;
pub mod crypto;
pub mod defer;
pub mod errors;
pub mod geodata;
pub mod http;
Expand Down
25 changes: 21 additions & 4 deletions clash_lib/src/config/def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,23 @@ use std::{collections::HashMap, fmt::Display, path::PathBuf, str::FromStr};
use serde::{Deserialize, Serialize};
use serde_yaml::Value;

fn default_tun_address() -> String {
"198.18.0.1/32".to_string()
}

#[derive(Serialize, Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
pub struct TunConfig {
pub enable: bool,
pub device_id: String,
/// tun interface address
#[serde(default = "default_tun_address")]
pub gateway: String,
pub routes: Option<Vec<String>>,
#[serde(default)]
pub route_all: bool,
}

#[derive(Serialize, Deserialize, Default, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RunMode {
Expand All @@ -29,6 +46,7 @@ impl Display for RunMode {
#[derive(PartialEq, Serialize, Deserialize, Default, Copy, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum LogLevel {
Trace,
Debug,
#[default]
Info,
Expand All @@ -41,6 +59,7 @@ pub enum LogLevel {
impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogLevel::Trace => write!(f, "trace"),
LogLevel::Debug => write!(f, "debug"),
LogLevel::Info => write!(f, "info"),
LogLevel::Warning => write!(f, "warn"),
Expand Down Expand Up @@ -313,7 +332,7 @@ pub struct Config {
/// enable: true
/// device-id: "dev://utun1989"
/// ```
pub tun: Option<HashMap<String, Value>>,
pub tun: Option<TunConfig>,
}

impl TryFrom<PathBuf> for Config {
Expand Down Expand Up @@ -553,9 +572,7 @@ allow-lan: false
tun:
enable: true
stack: system
device-url: dev://clash0
dns-hijack:
- 10.0.0.5
device-id: dev://clash0
# This is only applicable when `allow-lan` is `true`
# '*': bind all IP addresses
Expand Down
42 changes: 24 additions & 18 deletions clash_lib/src/config/internal/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::collections::HashMap;

use std::{fmt::Display, net::IpAddr, str::FromStr};

use ipnet::IpNet;
use serde::{de::value::MapDeserializer, Deserialize, Serialize};
use serde_yaml::Value;

Expand Down Expand Up @@ -97,15 +98,26 @@ impl TryFrom<def::Config> for Config {
dns: (&c).try_into()?,
experimental: c.experimental,
tun: match c.tun {
Some(mapping) => {
TunConfig::deserialize(MapDeserializer::new(mapping.into_iter()))
.map_err(|e| {
Error::InvalidConfig(format!(
"invalid tun config: {}",
e
))
Some(t) => TunConfig {
enable: t.enable,
device_id: t.device_id,
route_all: t.route_all,
routes: t
.routes
.map(|r| {
r.into_iter()
.map(|x| x.parse())
.collect::<Result<Vec<_>, _>>()
})
.transpose()
.map_err(|x| {
Error::InvalidConfig(format!("parse tun routes: {}", x))
})?
}
.unwrap_or_default(),
gateway: t.gateway.parse().map_err(|x| {
Error::InvalidConfig(format!("parse tun gateway: {}", x))
})?,
},
None => TunConfig::default(),
},
profile: Profile {
Expand Down Expand Up @@ -279,19 +291,13 @@ pub struct Profile {
// store_fake_ip: bool,
}

#[derive(Deserialize, Default)]
#[serde(rename_all = "kebab-case")]
#[derive(Default)]
pub struct TunConfig {
pub enable: bool,
/// tun device id, could be
/// dev://utun886 # Linux
/// fd://3 # file descriptor
#[serde(alias = "device-url")]
pub device_id: String,
/// tun device address
/// default: 198.18.0.0/16
pub network: Option<String>,
pub gateway: Option<IpAddr>,
pub route_all: bool,
pub routes: Vec<IpNet>,
pub gateway: IpNet,
}

#[derive(Clone, Default)]
Expand Down
15 changes: 11 additions & 4 deletions clash_lib/src/proxy/tun/inbound.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use super::{datagram::TunDatagram, netstack};
use std::{net::SocketAddr, sync::Arc};

use futures::{SinkExt, StreamExt};

use tracing::{debug, error, info, trace, warn};
use tun::{Device, TunPacket};
use url::Url;
Expand All @@ -10,7 +11,10 @@ use crate::{
app::{dispatcher::Dispatcher, dns::ThreadSafeDNSResolver},
common::errors::{map_io_error, new_io_error},
config::internal::config::TunConfig,
proxy::{datagram::UdpPacket, utils::get_outbound_interface},
proxy::{
datagram::UdpPacket, tun::routes::maybe_add_routes,
utils::get_outbound_interface,
},
session::{Network, Session, SocksAddr, Type},
Error, Runner,
};
Expand Down Expand Up @@ -148,9 +152,9 @@ pub fn get_runner(
return Ok(None);
}

let device_id = cfg.device_id;
let device_id = &cfg.device_id;

let u = Url::parse(&device_id)
let u = Url::parse(device_id)
.map_err(|x| Error::InvalidConfig(format!("tun device {}", x)))?;

let mut tun_cfg = tun::Configuration::default();
Expand All @@ -177,14 +181,17 @@ pub fn get_runner(
}
}

tun_cfg.up();
let gw = cfg.gateway;
tun_cfg.address(gw.addr()).netmask(gw.netmask()).up();

let tun = tun::create_as_async(&tun_cfg)
.map_err(|x| new_io_error(format!("failed to create tun device: {}", x)))?;

let tun_name = tun.get_ref().name().map_err(map_io_error)?;
info!("tun started at {}", tun_name);

maybe_add_routes(&cfg, &tun_name)?;

let (stack, mut tcp_listener, udp_socket) =
netstack::NetStack::with_buffer_size(512, 256).map_err(map_io_error)?;

Expand Down
1 change: 1 addition & 0 deletions clash_lib/src/proxy/tun/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ pub mod inbound;
pub use netstack_lwip as netstack;
mod datagram;
pub use inbound::get_runner as get_tun_runner;
mod routes;
9 changes: 9 additions & 0 deletions clash_lib/src/proxy/tun/routes/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use ipnet::IpNet;
use tracing::warn;

use crate::proxy::utils::OutboundInterface;

pub fn add_route(_: &OutboundInterface, _: &IpNet) -> std::io::Result<()> {
warn!("add_route is not implemented on Linux");
Ok(())
}
9 changes: 9 additions & 0 deletions clash_lib/src/proxy/tun/routes/macos.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use ipnet::IpNet;
use tracing::warn;

use crate::proxy::utils::OutboundInterface;

pub fn add_route(_: &OutboundInterface, _: &IpNet) -> std::io::Result<()> {
warn!("add_route is not implemented on macOS");
Ok(())
}
75 changes: 75 additions & 0 deletions clash_lib/src/proxy/tun/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#[cfg(windows)]
mod windows;
#[cfg(windows)]
use windows::add_route;

#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "macos")]
use macos::add_route;

#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
use linux::add_route;

#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
mod other;
#[cfg(not(any(windows, target_os = "macos", target_os = "linux")))]
use other::add_route;

use std::net::Ipv4Addr;

use tracing::warn;

use crate::{
common::errors::map_io_error, config::internal::config::TunConfig,
proxy::utils::OutboundInterface,
};

use ipnet::IpNet;
use network_interface::NetworkInterfaceConfig;

pub fn maybe_add_routes(cfg: &TunConfig, tun_name: &str) -> std::io::Result<()> {
if cfg.route_all || !cfg.routes.is_empty() {
let tun_iface = network_interface::NetworkInterface::show()
.map_err(map_io_error)?
.into_iter()
.find(|iface| iface.name == tun_name)
.map(|x| OutboundInterface {
name: x.name,
addr_v4: x.addr.iter().find_map(|addr| match addr {
network_interface::Addr::V4(addr) => Some(addr.ip),
_ => None,
}),
addr_v6: x.addr.iter().find_map(|addr| match addr {
network_interface::Addr::V6(addr) => Some(addr.ip),
_ => None,
}),
index: x.index,
})
.expect("tun interface not found");

if cfg.route_all {
warn!(
"route_all is enabled, all traffic will be routed through the tun \
interface"
);
let default_routes = vec![
IpNet::new(std::net::IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 1)
.unwrap(),
IpNet::new(std::net::IpAddr::V4(Ipv4Addr::new(128, 0, 0, 0)), 1)
.unwrap(),
];
for r in default_routes {
add_route(&tun_iface, &r).map_err(map_io_error)?;
}
} else {
for r in &cfg.routes {
add_route(&tun_iface, r).map_err(map_io_error)?;
}
}
}

Ok(())
}
9 changes: 9 additions & 0 deletions clash_lib/src/proxy/tun/routes/other.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use ipnet::IpNet;
use tracing::warn;

use crate::proxy::utils::OutboundInterface;

pub fn add_route(_: &OutboundInterface, _: &IpNet) -> std::io::Result<()> {
warn!("add_route is not implemented on {}", std::env::consts::OS);
Ok(())
}
Loading

0 comments on commit d6171fe

Please sign in to comment.