diff --git a/Makefile b/Makefile
index 972ff3eba..ff3afb751 100644
--- a/Makefile
+++ b/Makefile
@@ -4,3 +4,6 @@ docs:
@cargo doc -p clash_doc --no-deps
@echo "" > target/doc/index.html
@cp -r target/doc ./docs
+
+test-no-docker:
+ CLASH_RS_CI=true cargo test --all --all-features
diff --git a/clash/tests/data/config/rules.yaml b/clash/tests/data/config/rules.yaml
index b71679e26..1ba339ca4 100644
--- a/clash/tests/data/config/rules.yaml
+++ b/clash/tests/data/config/rules.yaml
@@ -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
@@ -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
diff --git a/clash_lib/Cargo.toml b/clash_lib/Cargo.toml
index bb6cbf66f..4c4b11a08 100644
--- a/clash_lib/Cargo.toml
+++ b/clash_lib/Cargo.toml
@@ -143,4 +143,5 @@ security-framework = "2.11.1"
windows = { version = "0.58", features = [
"Win32_Networking_WinSock",
"Win32_Foundation",
+ "Win32_NetworkManagement_Rras",
]}
\ No newline at end of file
diff --git a/clash_lib/src/app/logging.rs b/clash_lib/src/app/logging.rs
index 6a40564b3..a255748fc 100644
--- a/clash_lib/src/app/logging.rs
+++ b/clash_lib/src/app/logging.rs
@@ -26,6 +26,7 @@ impl From 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,
}
}
@@ -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(" "),
};
diff --git a/clash_lib/src/common/defer.rs b/clash_lib/src/common/defer.rs
new file mode 100644
index 000000000..7aac16ea2
--- /dev/null
+++ b/clash_lib/src/common/defer.rs
@@ -0,0 +1,25 @@
+// https://stackoverflow.com/a/29963675/1109167
+pub struct ScopeCall {
+ pub c: Option,
+}
+impl Drop for ScopeCall {
+ 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)* }) })
+ };
+ )
+}
diff --git a/clash_lib/src/common/mod.rs b/clash_lib/src/common/mod.rs
index 7aca64696..661a0ce9f 100644
--- a/clash_lib/src/common/mod.rs
+++ b/clash_lib/src/common/mod.rs
@@ -1,5 +1,6 @@
pub mod auth;
pub mod crypto;
+pub mod defer;
pub mod errors;
pub mod geodata;
pub mod http;
diff --git a/clash_lib/src/config/def.rs b/clash_lib/src/config/def.rs
index 31ed1dbbf..b4a0d59a2 100644
--- a/clash_lib/src/config/def.rs
+++ b/clash_lib/src/config/def.rs
@@ -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>,
+ #[serde(default)]
+ pub route_all: bool,
+}
+
#[derive(Serialize, Deserialize, Default, Copy, Clone)]
#[serde(rename_all = "lowercase")]
pub enum RunMode {
@@ -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,
@@ -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"),
@@ -313,7 +332,7 @@ pub struct Config {
/// enable: true
/// device-id: "dev://utun1989"
/// ```
- pub tun: Option>,
+ pub tun: Option,
}
impl TryFrom for Config {
@@ -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
diff --git a/clash_lib/src/config/internal/config.rs b/clash_lib/src/config/internal/config.rs
index b8044fc43..1f930c796 100644
--- a/clash_lib/src/config/internal/config.rs
+++ b/clash_lib/src/config/internal/config.rs
@@ -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;
@@ -97,15 +98,26 @@ impl TryFrom 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::, _>>()
+ })
+ .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 {
@@ -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,
- pub gateway: Option,
+ pub route_all: bool,
+ pub routes: Vec,
+ pub gateway: IpNet,
}
#[derive(Clone, Default)]
diff --git a/clash_lib/src/proxy/tun/inbound.rs b/clash_lib/src/proxy/tun/inbound.rs
index a2b27b6be..6ad5cb7fe 100644
--- a/clash_lib/src/proxy/tun/inbound.rs
+++ b/clash_lib/src/proxy/tun/inbound.rs
@@ -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;
@@ -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,
};
@@ -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();
@@ -177,7 +181,8 @@ 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)))?;
@@ -185,6 +190,8 @@ pub fn get_runner(
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)?;
diff --git a/clash_lib/src/proxy/tun/mod.rs b/clash_lib/src/proxy/tun/mod.rs
index cde10543e..19bc5b90a 100644
--- a/clash_lib/src/proxy/tun/mod.rs
+++ b/clash_lib/src/proxy/tun/mod.rs
@@ -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;
diff --git a/clash_lib/src/proxy/tun/routes/linux.rs b/clash_lib/src/proxy/tun/routes/linux.rs
new file mode 100644
index 000000000..0ee6e018b
--- /dev/null
+++ b/clash_lib/src/proxy/tun/routes/linux.rs
@@ -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(())
+}
diff --git a/clash_lib/src/proxy/tun/routes/macos.rs b/clash_lib/src/proxy/tun/routes/macos.rs
new file mode 100644
index 000000000..45cdacff4
--- /dev/null
+++ b/clash_lib/src/proxy/tun/routes/macos.rs
@@ -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(())
+}
diff --git a/clash_lib/src/proxy/tun/routes/mod.rs b/clash_lib/src/proxy/tun/routes/mod.rs
new file mode 100644
index 000000000..4b7d1e857
--- /dev/null
+++ b/clash_lib/src/proxy/tun/routes/mod.rs
@@ -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(())
+}
diff --git a/clash_lib/src/proxy/tun/routes/other.rs b/clash_lib/src/proxy/tun/routes/other.rs
new file mode 100644
index 000000000..01bda4583
--- /dev/null
+++ b/clash_lib/src/proxy/tun/routes/other.rs
@@ -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(())
+}
diff --git a/clash_lib/src/proxy/tun/routes/windows.rs b/clash_lib/src/proxy/tun/routes/windows.rs
new file mode 100644
index 000000000..f91194e3a
--- /dev/null
+++ b/clash_lib/src/proxy/tun/routes/windows.rs
@@ -0,0 +1,190 @@
+use ipnet::IpNet;
+use std::{io, ptr::null_mut};
+use tracing::{error, info};
+use windows::Win32::{
+ Foundation::{GetLastError, ERROR_SUCCESS},
+ NetworkManagement::Rras::{
+ RtmAddNextHop, RtmAddRouteToDest, RtmDeregisterEntity, RtmRegisterEntity,
+ RtmReleaseNextHops, RTM_ENTITY_ID, RTM_ENTITY_ID_0, RTM_ENTITY_ID_0_0,
+ RTM_ENTITY_INFO, RTM_NET_ADDRESS, RTM_NEXTHOP_INFO, RTM_REGN_PROFILE,
+ RTM_ROUTE_CHANGE_NEW, RTM_ROUTE_INFO, RTM_VIEW_MASK_MCAST,
+ RTM_VIEW_MASK_UCAST,
+ },
+ Networking::WinSock::{AF_INET, AF_INET6, PROTO_IP_RIP},
+};
+
+use crate::{common::errors::new_io_error, defer, proxy::utils::OutboundInterface};
+
+const PROTO_TYPE_UCAST: u32 = 0;
+const PROTO_VENDOR_ID: u32 = 0xFFFF;
+#[inline]
+fn protocol_id(typ: u32, vendor_id: u32, protocol_id: u32) -> u32 {
+ ((typ & 0x03) << 30) | ((vendor_id & 0x3FFF) << 16) | (protocol_id & 0xFFFF)
+}
+
+pub fn add_route(via: &OutboundInterface, dest: &IpNet) -> io::Result<()> {
+ let cmd = format!(
+ "route add {} mask {} {} if {}",
+ dest.addr(),
+ dest.netmask(),
+ via.addr_v4.expect("tun interface has no ipv4 address"),
+ via.index,
+ );
+
+ info!("executing: {}", cmd);
+
+ let output = std::process::Command::new("cmd")
+ .args(["/C", &cmd])
+ .output()
+ .map_err(|e| new_io_error(e.to_string().as_str()))?;
+
+ if output.status.success() {
+ info!("{} is now routed through {}", dest, via.name);
+ Ok(())
+ } else {
+ let err = String::from_utf8_lossy(&output.stderr);
+ error!("failed to add route: {}", err);
+ Err(new_io_error(err.to_string().as_str()))
+ }
+}
+
+/// Add a route to the routing table.
+/// https://learn.microsoft.com/en-us/windows/win32/rras/add-and-update-routes-using-rtmaddroutetodest
+/// FIXME: figure out why this doesn't work https://stackoverflow.com/questions/43632619/how-to-properly-use-rtmv2-and-rtmaddroutetodest
+#[allow(dead_code)]
+pub fn add_route_that_does_not_work(
+ via: &OutboundInterface,
+ dest: &IpNet,
+) -> io::Result<()> {
+ let address_family = match dest {
+ IpNet::V4(_) => AF_INET,
+ IpNet::V6(_) => AF_INET6,
+ };
+
+ let mut rtm_reg_handle: isize = 0;
+ let mut rtm_entity_info = RTM_ENTITY_INFO::default();
+ let mut rtm_regn_profile = RTM_REGN_PROFILE::default();
+
+ rtm_entity_info.RtmInstanceId = 0;
+ rtm_entity_info.AddressFamily = address_family.0;
+ rtm_entity_info.EntityId = RTM_ENTITY_ID {
+ Anonymous: RTM_ENTITY_ID_0 {
+ Anonymous: RTM_ENTITY_ID_0_0 {
+ EntityProtocolId: PROTO_IP_RIP.0.try_into().unwrap(),
+ EntityInstanceId: protocol_id(
+ PROTO_TYPE_UCAST,
+ PROTO_VENDOR_ID,
+ PROTO_IP_RIP.0.try_into().unwrap(),
+ ),
+ },
+ },
+ };
+ let rv = unsafe {
+ RtmRegisterEntity(
+ &mut rtm_entity_info,
+ null_mut(),
+ None,
+ false,
+ &mut rtm_regn_profile,
+ &mut rtm_reg_handle,
+ )
+ };
+
+ if rv != ERROR_SUCCESS.0 {
+ let err = unsafe { GetLastError().to_hresult().message() };
+ error!("failed to register entity: {}", err);
+ return Err(new_io_error(err));
+ }
+
+ defer! {
+ let rv = unsafe {RtmDeregisterEntity(rtm_reg_handle)};
+ if rv != ERROR_SUCCESS.0 {
+ let err = unsafe { GetLastError().to_hresult().message() };
+ error!("failed to deregister entity: {}", err);
+ }
+ }
+
+ let mut next_hop_info = RTM_NEXTHOP_INFO {
+ InterfaceIndex: via.index,
+ NextHopAddress: RTM_NET_ADDRESS {
+ AddressFamily: AF_INET.0,
+ NumBits: 32,
+ AddrBits: via
+ .addr_v4
+ .expect("tun interface has no ipv4 address")
+ .to_ipv6_compatible()
+ .octets(),
+ },
+ ..Default::default()
+ };
+
+ let mut next_hop_handle: isize = 0;
+ let mut change_flags = 0u32;
+
+ let status = unsafe {
+ RtmAddNextHop(
+ rtm_reg_handle,
+ &mut next_hop_info,
+ &mut next_hop_handle,
+ &mut change_flags,
+ )
+ };
+
+ if status != ERROR_SUCCESS.0 {
+ let err = unsafe { GetLastError().to_hresult().message() };
+ error!("failed to add next hop: {}", err);
+ return Err(new_io_error(err));
+ }
+
+ defer! {
+ let mut next_hops = [next_hop_handle];
+ let rv = unsafe {
+ RtmReleaseNextHops(rtm_reg_handle, 1, next_hops.as_mut_ptr())
+ };
+
+ if rv != ERROR_SUCCESS.0 {
+ let err = unsafe { GetLastError().to_hresult().message() };
+ error!("failed to release next hop: {}", err);
+ }
+ }
+
+ let mut route_info = RTM_ROUTE_INFO::default();
+ let mut net_address = RTM_NET_ADDRESS {
+ AddressFamily: address_family.0,
+ NumBits: dest.prefix_len() as u16,
+ AddrBits: match dest {
+ IpNet::V4(ip) => ip.addr().to_ipv6_compatible().octets(),
+ IpNet::V6(ip) => ip.addr().octets(),
+ },
+ };
+ route_info.Neighbour = next_hop_handle;
+ route_info.PrefInfo.Metric = 1;
+ route_info.BelongsToViews = RTM_VIEW_MASK_UCAST | RTM_VIEW_MASK_MCAST;
+ route_info.NextHopsList.NumNextHops = 1;
+ route_info.NextHopsList.NextHops[0] = next_hop_handle;
+
+ let mut change_flags = RTM_ROUTE_CHANGE_NEW;
+ let rv = unsafe {
+ RtmAddRouteToDest(
+ rtm_reg_handle,
+ null_mut() as _,
+ &mut net_address,
+ &mut route_info,
+ f32::INFINITY as _,
+ 0,
+ 0,
+ 0,
+ &mut change_flags,
+ )
+ };
+
+ if rv == ERROR_SUCCESS.0 {
+ info!("{} is now routed through {}", dest, via.name);
+ } else {
+ let err = unsafe { GetLastError().to_hresult().message() };
+ error!("failed to add route: {}", err);
+ return Err(new_io_error(err));
+ }
+
+ Ok(())
+}