diff --git a/Cargo.lock b/Cargo.lock index 6c14492bc..31092a4df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -573,7 +573,7 @@ dependencies = [ "cexpr", "clang-sys", "clap 2.34.0", - "env_logger", + "env_logger 0.9.3", "lazy_static", "lazycell", "log", @@ -1091,6 +1091,7 @@ dependencies = [ "cfb-mode", "chacha20poly1305", "chrono", + "cmd_lib", "console-subscriber", "crc32fast", "criterion", @@ -1142,7 +1143,7 @@ dependencies = [ "sha2", "shadowsocks", "smoltcp", - "socket2", + "socket2 0.5.6", "tempfile", "thiserror", "tokio", @@ -1163,6 +1164,7 @@ dependencies = [ "tuic", "tuic-quinn", "tun", + "unix-udp-sock", "url", "uuid", "webpki-roots", @@ -1177,6 +1179,32 @@ dependencies = [ "cc", ] +[[package]] +name = "cmd_lib" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f4cbdcab51ca635c5b19c85ece4072ea42e0d2360242826a6fc96fb11f0d40" +dependencies = [ + "cmd_lib_macros", + "env_logger 0.10.2", + "faccess", + "lazy_static", + "log", + "os_pipe", +] + +[[package]] +name = "cmd_lib_macros" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae881960f7e2a409f91ef0b1c09558cf293031a1d6e8b45f908311f2a43f5fdf" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "coarsetime" version = "0.1.34" @@ -1946,6 +1974,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1982,6 +2023,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags 1.3.2", + "libc", + "winapi", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -2726,7 +2778,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -2809,7 +2861,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.2.0", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower", "tower-service", @@ -2997,7 +3049,7 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.6", "widestring", "windows-sys 0.48.0", "winreg", @@ -3746,6 +3798,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "os_pipe" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57119c3b893986491ec9aa85056780d3a0f3cf4da7cc09dd3650dbd6c6738fb9" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "overload" version = "0.1.1" @@ -5022,7 +5084,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "shadowsocks-crypto", - "socket2", + "socket2 0.5.6", "spin 0.9.8", "thiserror", "tokio", @@ -5150,6 +5212,16 @@ dependencies = [ "managed", ] +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.6" @@ -5513,7 +5585,7 @@ dependencies = [ "parking_lot 0.12.1", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.6", "tokio-macros", "tracing", "windows-sys 0.48.0", @@ -5586,7 +5658,7 @@ dependencies = [ "log", "once_cell", "pin-project", - "socket2", + "socket2 0.5.6", "tokio", "windows-sys 0.48.0", ] @@ -6845,6 +6917,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "unix-udp-sock" +version = "0.7.0" +source = "git+https://github.com/Watfaq/unix-udp-sock.git?rev=cd3e4eca43e6f3be82a2703c3d711b7e18fbfd18#cd3e4eca43e6f3be82a2703c3d711b7e18fbfd18" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "libc", + "pin-project-lite", + "socket2 0.4.10", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" diff --git a/clash/tests/data/config/tproxy.yaml b/clash/tests/data/config/tproxy.yaml new file mode 100644 index 000000000..31839b333 --- /dev/null +++ b/clash/tests/data/config/tproxy.yaml @@ -0,0 +1,4 @@ +tproxy-port: 7893 +external-controller: 127.0.0.1:9090 +mode: global +log-level: debug \ No newline at end of file diff --git a/clash_lib/Cargo.toml b/clash_lib/Cargo.toml index f41f9a76b..f8cfc26b7 100644 --- a/clash_lib/Cargo.toml +++ b/clash_lib/Cargo.toml @@ -37,6 +37,8 @@ base64 = "0.22" uuid = { version = "1.8.0", features = ["v4", "fast-rng", "macro-diagnostics", "serde"] } boring = "4.5.0" boring-sys = "4.5.0" +unix-udp-sock = { git = "https://github.com/Watfaq/unix-udp-sock.git", rev = "cd3e4eca43e6f3be82a2703c3d711b7e18fbfd18"} +cmd_lib = "1.9.3" ip_network_table-deps-treebitmap = "0.5.0" once_cell = "1.18.0" diff --git a/clash_lib/src/app/dispatcher/dispatcher_impl.rs b/clash_lib/src/app/dispatcher/dispatcher_impl.rs index 78427a63c..b339a4972 100644 --- a/clash_lib/src/app/dispatcher/dispatcher_impl.rs +++ b/clash_lib/src/app/dispatcher/dispatcher_impl.rs @@ -181,6 +181,7 @@ impl Dispatcher { /// Dispatch a UDP packet to outbound handler /// returns the close sender + /// will ignore the source and destination in `Session` #[instrument] pub fn dispatch_datagram( &self, diff --git a/clash_lib/src/app/inbound/manager.rs b/clash_lib/src/app/inbound/manager.rs index c68670edf..7428644f4 100644 --- a/clash_lib/src/app/inbound/manager.rs +++ b/clash_lib/src/app/inbound/manager.rs @@ -98,6 +98,9 @@ impl InboundManager { ListenerType::Mixed => { ports.mixed_port = Some(x.port); } + ListenerType::TProxy => { + ports.tproxy_port = Some(x.port); + } }); ports @@ -147,6 +150,20 @@ impl InboundManager { ); } + if let Some(tproxy_port) = ports.tproxy_port { + network_listeners.insert( + ListenerType::TProxy, + NetworkInboundListener { + name: "TProxy".to_string(), + bind_addr: self.bind_address.clone(), + port: tproxy_port, + listener_type: ListenerType::TProxy, + dispatcher: self.dispatcher.clone(), + authenticator: self.authenticator.clone(), + }, + ); + } + self.network_listeners = network_listeners; } } diff --git a/clash_lib/src/app/inbound/network_listener.rs b/clash_lib/src/app/inbound/network_listener.rs index 9d3db7ea5..af81d8998 100644 --- a/clash_lib/src/app/inbound/network_listener.rs +++ b/clash_lib/src/app/inbound/network_listener.rs @@ -1,7 +1,7 @@ use crate::common::auth::ThreadSafeAuthenticator; use crate::config::internal::config::BindAddress; -use crate::proxy::{http, mixed, socks, AnyInboundListener}; +use crate::proxy::{http, mixed, socks, tproxy, AnyInboundListener}; use crate::proxy::utils::Interface; use crate::{Dispatcher, Error, Runner}; @@ -17,6 +17,7 @@ pub enum ListenerType { Http, Socks5, Mixed, + TProxy, } pub struct NetworkInboundListener { @@ -111,6 +112,17 @@ impl NetworkInboundListener { self.dispatcher.clone(), self.authenticator.clone(), ), + + ListenerType::TProxy => { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + tproxy::TProxyListener::new((ip, self.port).into(), self.dispatcher.clone()) + } + #[cfg(not(target_os = "linux"))] + { + warn!("tproxy only support linux and android, ignore this config"); + } + } }; if listener.handle_tcp() { diff --git a/clash_lib/src/common/cmd.rs b/clash_lib/src/common/cmd.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/clash_lib/src/common/cmd.rs @@ -0,0 +1 @@ + diff --git a/clash_lib/src/common/mod.rs b/clash_lib/src/common/mod.rs index 3604b7d55..3a11acd1f 100644 --- a/clash_lib/src/common/mod.rs +++ b/clash_lib/src/common/mod.rs @@ -1,4 +1,5 @@ pub mod auth; +pub mod cmd; pub mod crypto; pub mod errors; pub mod http; @@ -7,4 +8,5 @@ pub mod mmdb; pub mod timed_future; pub mod tls; pub mod trie; +pub mod tunnel_datagram; pub mod utils; diff --git a/clash_lib/src/proxy/tun/datagram.rs b/clash_lib/src/common/tunnel_datagram.rs similarity index 85% rename from clash_lib/src/proxy/tun/datagram.rs rename to clash_lib/src/common/tunnel_datagram.rs index d8f2ec966..86d6ee68d 100644 --- a/clash_lib/src/proxy/tun/datagram.rs +++ b/clash_lib/src/common/tunnel_datagram.rs @@ -1,4 +1,4 @@ -use std::{net::SocketAddr, task::Poll}; +use std::task::Poll; use futures::{ready, Sink, Stream}; @@ -8,38 +8,33 @@ use crate::{ }; #[derive(Debug)] -pub struct TunDatagram { +pub struct TunnelDatagram { + // send to tproxy/tun, used for `Stream` rx: tokio::sync::mpsc::Receiver, + // receive from tproxy/tun, used for `Sink` tx: tokio::sync::mpsc::Sender, pkt: Option, flushed: bool, - #[allow(unused)] - local_addr: SocketAddr, } -impl TunDatagram { +impl TunnelDatagram { pub fn new( - // send to tun tx: tokio::sync::mpsc::Sender, - // receive from tun rx: tokio::sync::mpsc::Receiver, - // the address of the tun udp socket - local_addr: SocketAddr, ) -> Self { Self { rx, tx, pkt: None, flushed: true, - local_addr, } } } -impl InboundDatagram for TunDatagram {} +impl InboundDatagram for TunnelDatagram {} -impl Stream for TunDatagram { +impl Stream for TunnelDatagram { type Item = UdpPacket; fn poll_next( @@ -50,7 +45,7 @@ impl Stream for TunDatagram { } } -impl Sink for TunDatagram { +impl Sink for TunnelDatagram { type Error = std::io::Error; fn poll_ready( diff --git a/clash_lib/src/config/def.rs b/clash_lib/src/config/def.rs index 22fb41206..1891f29f5 100644 --- a/clash_lib/src/config/def.rs +++ b/clash_lib/src/config/def.rs @@ -374,6 +374,7 @@ pub struct Config { /// device-id: "dev://utun1989" /// ``` pub tun: Option>, + pub iptables: Iptables, } impl TryFrom for Config { @@ -431,6 +432,7 @@ impl Default for Config { .to_owned(), ), tun: Default::default(), + iptables: Default::default(), } } } @@ -559,6 +561,25 @@ impl Default for Profile { } } +#[derive(Serialize, Deserialize)] +#[serde(default)] +#[serde(rename_all = "kebab-case")] +pub struct Iptables { + enable: bool, + inbound_interface: String, + bypass: Vec, +} + +impl Default for Iptables { + fn default() -> Self { + Self { + enable: false, + inbound_interface: "lo".to_owned(), + bypass: vec![], + } + } +} + #[cfg(test)] mod tests { use serde_yaml::Value; diff --git a/clash_lib/src/config/internal/proxy.rs b/clash_lib/src/config/internal/proxy.rs index 918e48cff..351626835 100644 --- a/clash_lib/src/config/internal/proxy.rs +++ b/clash_lib/src/config/internal/proxy.rs @@ -1,5 +1,6 @@ use crate::common::utils::default_bool_true; use crate::config::utils; +use crate::proxy::CommonOption; use crate::Error; use serde::de::value::MapDeserializer; use serde::Deserialize; @@ -116,6 +117,8 @@ impl Display for OutboundProxyProtocol { #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] pub struct OutboundShadowsocks { + #[serde(flatten)] + pub common_opts: CommonOption, pub name: String, pub server: String, pub port: u16, @@ -130,6 +133,8 @@ pub struct OutboundShadowsocks { #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] pub struct OutboundSocks5 { + #[serde(flatten)] + pub common_opts: CommonOption, pub name: String, pub server: String, pub port: u16, @@ -142,6 +147,8 @@ pub struct OutboundSocks5 { #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] pub struct WsOpt { + #[serde(flatten)] + pub common_opts: CommonOption, pub path: Option, pub headers: Option>, pub max_early_data: Option, @@ -163,6 +170,8 @@ pub struct GrpcOpt { #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case")] pub struct OutboundTrojan { + #[serde(flatten)] + pub common_opts: CommonOption, pub name: String, pub server: String, pub port: u16, @@ -179,6 +188,8 @@ pub struct OutboundTrojan { #[derive(serde::Serialize, serde::Deserialize, Debug, Default)] #[serde(rename_all = "kebab-case")] pub struct OutboundVmess { + #[serde(flatten)] + pub common_opts: CommonOption, pub name: String, pub server: String, pub port: u16, @@ -200,6 +211,8 @@ pub struct OutboundVmess { #[derive(serde::Serialize, serde::Deserialize, Debug, Default, Clone)] #[serde(rename_all = "kebab-case")] pub struct OutboundWireguard { + #[serde(flatten)] + pub common_opts: CommonOption, pub name: String, pub server: String, pub port: u16, diff --git a/clash_lib/src/proxy/converters/shadowsocks.rs b/clash_lib/src/proxy/converters/shadowsocks.rs index b916650c9..c0f194a4a 100644 --- a/clash_lib/src/proxy/converters/shadowsocks.rs +++ b/clash_lib/src/proxy/converters/shadowsocks.rs @@ -2,7 +2,7 @@ use crate::{ config::internal::proxy::OutboundShadowsocks, proxy::{ shadowsocks::{Handler, HandlerOptions, OBFSOption}, - AnyOutboundHandler, CommonOption, + AnyOutboundHandler, }, Error, }; @@ -21,7 +21,7 @@ impl TryFrom<&OutboundShadowsocks> for AnyOutboundHandler { fn try_from(s: &OutboundShadowsocks) -> Result { let h = Handler::new(HandlerOptions { name: s.name.to_owned(), - common_opts: CommonOption::default(), + common_opts: s.common_opts.clone(), server: s.server.to_owned(), port: s.port, password: s.password.to_owned(), diff --git a/clash_lib/src/proxy/converters/trojan.rs b/clash_lib/src/proxy/converters/trojan.rs index 212ebdf19..642f76550 100644 --- a/clash_lib/src/proxy/converters/trojan.rs +++ b/clash_lib/src/proxy/converters/trojan.rs @@ -5,7 +5,7 @@ use crate::{ proxy::{ options::{GrpcOption, WsOption}, trojan::{Handler, Opts, Transport}, - AnyOutboundHandler, CommonOption, + AnyOutboundHandler, }, Error, }; @@ -29,7 +29,7 @@ impl TryFrom<&OutboundTrojan> for AnyOutboundHandler { let h = Handler::new(Opts { name: s.name.to_owned(), - common_opts: CommonOption::default(), + common_opts: s.common_opts.clone(), server: s.server.to_owned(), port: s.port, password: s.password.clone(), diff --git a/clash_lib/src/proxy/converters/vmess.rs b/clash_lib/src/proxy/converters/vmess.rs index e04606e97..89a3731e1 100644 --- a/clash_lib/src/proxy/converters/vmess.rs +++ b/clash_lib/src/proxy/converters/vmess.rs @@ -6,7 +6,7 @@ use crate::{ options::{GrpcOption, Http2Option, WsOption}, transport::TLSOptions, vmess::{Handler, HandlerOptions, VmessTransport}, - AnyOutboundHandler, CommonOption, + AnyOutboundHandler, }, Error, }; @@ -30,7 +30,7 @@ impl TryFrom<&OutboundVmess> for AnyOutboundHandler { let h = Handler::new(HandlerOptions { name: s.name.to_owned(), - common_opts: CommonOption::default(), + common_opts: s.common_opts.clone(), server: s.server.to_owned(), port: s.port, uuid: s.uuid.clone(), diff --git a/clash_lib/src/proxy/converters/wireguard.rs b/clash_lib/src/proxy/converters/wireguard.rs index d1e5188c8..b482ae4f0 100644 --- a/clash_lib/src/proxy/converters/wireguard.rs +++ b/clash_lib/src/proxy/converters/wireguard.rs @@ -23,7 +23,7 @@ impl TryFrom<&OutboundWireguard> for AnyOutboundHandler { fn try_from(s: &OutboundWireguard) -> Result { let h = Handler::new(HandlerOpts { name: s.name.to_owned(), - common_opts: Default::default(), + common_opts: s.common_opts.clone(), server: s.server.to_owned(), port: s.port, ip: s diff --git a/clash_lib/src/proxy/direct/mod.rs b/clash_lib/src/proxy/direct/mod.rs index 8abf99ff1..9f0069a0d 100644 --- a/clash_lib/src/proxy/direct/mod.rs +++ b/clash_lib/src/proxy/direct/mod.rs @@ -52,9 +52,9 @@ impl OutboundHandler for Handler { resolver, sess.destination.host().as_str(), sess.destination.port(), - None, + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .await?; @@ -81,7 +81,7 @@ impl OutboundHandler for Handler { None, sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .await .map(|x| OutboundDatagramImpl::new(x, resolver))?; diff --git a/clash_lib/src/proxy/mod.rs b/clash_lib/src/proxy/mod.rs index 96c76b826..0ebe7c9b5 100644 --- a/clash_lib/src/proxy/mod.rs +++ b/clash_lib/src/proxy/mod.rs @@ -21,6 +21,8 @@ pub mod reject; pub mod http; pub mod mixed; +#[cfg(target_os = "linux")] +pub mod tproxy; pub(crate) mod datagram; mod options; @@ -85,13 +87,37 @@ impl OutboundDatagram for T where pub type AnyOutboundDatagram = Box>; -#[derive(Default, Debug, Clone)] +#[allow(dead_code)] +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CommonOption { - #[allow(dead_code)] + #[serde(rename = "interface-name")] so_mark: Option, + #[serde(rename = "routing-mark")] iface: Option, } +impl CommonOption { + pub fn merge<'a>(&'a self, sess: &'a Session) -> (Option, Option<&'a Interface>) { + let so_mark = if let Some(so_mark) = self.so_mark { + Some(so_mark) + } else if let Some(so_mark) = sess.packet_mark { + Some(so_mark) + } else { + None + }; + + let iface = if let Some(iface) = self.iface.as_ref() { + Some(iface) + } else if let Some(iface) = sess.iface.as_ref() { + Some(iface) + } else { + None + }; + + (so_mark, iface) + } +} + #[async_trait] pub trait InboundListener: Send + Sync + Unpin { /// support tcp or not diff --git a/clash_lib/src/proxy/relay/mod.rs b/clash_lib/src/proxy/relay/mod.rs index 39bb69539..1f540391c 100644 --- a/clash_lib/src/proxy/relay/mod.rs +++ b/clash_lib/src/proxy/relay/mod.rs @@ -91,9 +91,9 @@ impl OutboundHandler for Handler { resolver.clone(), remote_addr.host().as_str(), remote_addr.port(), - None, + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .await?; diff --git a/clash_lib/src/proxy/shadowsocks/mod.rs b/clash_lib/src/proxy/shadowsocks/mod.rs index ade423be0..44073c70b 100644 --- a/clash_lib/src/proxy/shadowsocks/mod.rs +++ b/clash_lib/src/proxy/shadowsocks/mod.rs @@ -179,13 +179,14 @@ impl OutboundHandler for Handler { sess: &Session, resolver: ThreadSafeDNSResolver, ) -> io::Result { + let (packet_mark, iface) = self.opts.common_opts.merge(sess); let stream = new_tcp_stream( resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - None, + packet_mark, ) .map_err(|x| { io::Error::new( @@ -269,11 +270,12 @@ impl OutboundHandler for Handler { _ => return Err(io::Error::new(io::ErrorKind::Other, "unsupported cipher")), }, ); + let (packet_mark, iface) = self.opts.common_opts.merge(sess); let socket = new_udp_socket( None, - self.opts.common_opts.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - None, + packet_mark, ) .await?; let socket = ProxySocket::from_socket(UdpSocketType::Client, ctx, &cfg, socket); diff --git a/clash_lib/src/proxy/socks/inbound/stream.rs b/clash_lib/src/proxy/socks/inbound/stream.rs index d518c8ca9..29b4006b5 100644 --- a/clash_lib/src/proxy/socks/inbound/stream.rs +++ b/clash_lib/src/proxy/socks/inbound/stream.rs @@ -140,9 +140,9 @@ pub async fn handle_tcp<'a>( let udp_addr = SocketAddr::new(s.local_addr()?.ip(), 0); let udp_inbound = new_udp_socket( Some(&udp_addr), - None, + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .await?; @@ -166,8 +166,6 @@ pub async fn handle_tcp<'a>( let sess = Session { network: Network::Udp, typ: Type::Socks5, - packet_mark: None, - iface: None, ..Default::default() }; diff --git a/clash_lib/src/proxy/tproxy/inbound.rs b/clash_lib/src/proxy/tproxy/inbound.rs new file mode 100644 index 000000000..196f5fc5d --- /dev/null +++ b/clash_lib/src/proxy/tproxy/inbound.rs @@ -0,0 +1,175 @@ +use std::{net::SocketAddr, sync::Arc}; + +use tracing::{trace, warn}; + +use crate::{ + app::dispatcher::Dispatcher, + proxy::{ + datagram::UdpPacket, utils::new_transparent_udp_socket, AnyInboundListener, InboundListener, + }, + session::{get_packet_mark, Network, Session, SocksAddr, Type}, +}; + +use async_trait::async_trait; + +use super::{ + iptables::TProxyGuard, + transparent_socket::{new_tcp_listener, new_udp_listener}, +}; +use crate::common::tunnel_datagram::TunnelDatagram; + +pub struct Listener { + addr: SocketAddr, + dispatcher: Arc, + _guard: TProxyGuard, +} + +impl Listener { + pub fn new(addr: SocketAddr, dispatcher: Arc) -> AnyInboundListener { + let _guard = TProxyGuard::new( + Default::default(), + get_packet_mark(), + addr.port(), + None, + None, + ); + + Arc::new(Self { + addr, + dispatcher, + _guard, + }) as _ + } +} + +impl Drop for Listener { + fn drop(&mut self) { + warn!("tproxy inbound listener on {} stopped", self.addr); + } +} + +#[async_trait] +impl InboundListener for Listener { + fn handle_tcp(&self) -> bool { + true + } + + fn handle_udp(&self) -> bool { + true + } + + async fn listen_tcp(&self) -> std::io::Result<()> { + let listener = new_tcp_listener(self.addr)?; + loop { + let (socket, _) = listener.accept().await?; + let dispatcher = self.dispatcher.clone(); + tokio::spawn(async move { + let source = socket.peer_addr().unwrap(); + let destination = SocksAddr::Ip(socket.local_addr().unwrap()); + let session = crate::session::Session { + source, + destination, + typ: crate::session::Type::TProxy, + ..Default::default() + }; + dispatcher.dispatch_stream(session, socket).await + }); + } + } + + async fn listen_udp(&self) -> std::io::Result<()> { + let listener = new_udp_listener(self.addr).await?; + + handle_inbound_datagram(Arc::new(listener), self.dispatcher.clone()).await + } +} + +async fn handle_inbound_datagram( + socket: Arc, + dispatcher: Arc, +) -> std::io::Result<()> { + // dispatcher <-> tun communications + let (l_tx, mut l_rx) = tokio::sync::mpsc::channel::(32); + + // forward packets from tun to dispatcher + let (d_tx, d_rx) = tokio::sync::mpsc::channel::(32); + + // for dispatcher - the dispatcher would receive packets from this channel, which is from the stack + // and send back packets to this channel, which is to the tun + let udp_stream = TunnelDatagram::new(l_tx, d_rx); + + let sess = Session { + network: Network::Udp, + typ: Type::TProxy, + ..Default::default() + }; + + let closer = dispatcher.dispatch_datagram(sess, Box::new(udp_stream)); + + // dispatcher -> tproxy + let fut1 = tokio::spawn(async move { + while let Some(pkt) = l_rx.recv().await { + trace!("tproxy <- dispatcher: {:?}", pkt); + // populate the correct src_addr, though is it necessary? + let src_addr: SocketAddr = match pkt.src_addr { + SocksAddr::Ip(ip) => ip, + SocksAddr::Domain(host, port) => { + warn!("unexpected domain address: {}:{:?}", host, port); + continue; + } + }; + let response_socket = + match new_transparent_udp_socket(Some(&src_addr), None, Some(0xff)).await { + Ok(response_socket) => response_socket, + Err(e) => { + warn!("failed to create udp socket, err:{:?}", e); + continue; + } + }; + // remote -> local + match response_socket + .send_to(&pkt.data[..], &pkt.dst_addr.must_into_socket_addr()) + .await + { + Ok(_) => {} + Err(e) => { + warn!("failed to send udp packet to proxy: {}", e); + } + } + } + }); + + // tproxy -> dispatcher + let fut2 = tokio::spawn(async move { + let mut buf = vec![0_u8; 1024 * 64]; + while let Ok(meta) = socket.recv_msg(&mut buf).await { + match meta.orig_dst { + Some(orig_dst) => { + trace!("recv msg:{:?} orig_dst:{:?}", meta, orig_dst); + let pkt = UdpPacket { + data: buf[..meta.len].to_vec(), + src_addr: meta.addr.into(), + dst_addr: orig_dst.into(), + }; + trace!("tproxy -> dispatcher: {:?}", pkt); + match d_tx.send(pkt).await { + Ok(_) => {} + Err(e) => { + warn!("failed to send udp packet to proxy: {}", e); + continue; + } + } + } + None => { + warn!("failed to get orig_dst"); + continue; + } + } + } + + closer.send(0).ok(); + }); + + let _ = futures::future::join(fut1, fut2).await; + Ok(()) +} diff --git a/clash_lib/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs new file mode 100644 index 000000000..265e0dd14 --- /dev/null +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -0,0 +1,221 @@ +use cmd_lib::run_cmd; +use std::process::Command; +use tracing::{debug, error, warn}; + +// TODO: support nftables +#[derive(Debug, Clone, Copy)] +pub enum TProxyStrategy { + Iptables, + None, +} + +impl Default for TProxyStrategy { + // auto detect command in system + fn default() -> Self { + if command_exists("iptables") { + TProxyStrategy::Iptables + } else { + warn!("iptables command not found, tproxy is disabled"); + TProxyStrategy::None + } + } +} + +impl From<&str> for TProxyStrategy { + fn from(s: &str) -> Self { + match s { + "iptables" => TProxyStrategy::Iptables, + _ => TProxyStrategy::None, + } + } +} + +impl TProxyStrategy { + pub fn setup( + &self, + skip_mark: u32, + tproxy_port: u16, + output_chain_name: Option<&str>, + prerouting_chain_name: Option<&str>, + ) { + match self { + TProxyStrategy::Iptables => { + clean_iptables_tproxy(output_chain_name, prerouting_chain_name); + setup_iptables_tproxy( + skip_mark, + tproxy_port, + output_chain_name, + prerouting_chain_name, + ); + } + TProxyStrategy::None => { + error!("No tproxy command found"); + } + } + } +} + +#[derive(Debug)] +pub struct TProxyGuard { + strategy: TProxyStrategy, + skip_mark: u32, + tproxy_port: u16, + output_chain_name: Option, + prerouting_chain_name: Option, +} + +impl TProxyGuard { + pub fn new( + strategy: TProxyStrategy, + skip_mark: u32, + tproxy_port: u16, + output_chain_name: Option, + prerouting_chain_name: Option, + ) -> Self { + strategy.setup( + skip_mark, + tproxy_port, + output_chain_name.as_deref(), + prerouting_chain_name.as_deref(), + ); + Self { + strategy, + skip_mark, + tproxy_port, + output_chain_name, + prerouting_chain_name, + } + } +} + +impl Drop for TProxyGuard { + fn drop(&mut self) { + match self.strategy { + TProxyStrategy::Iptables => { + clean_iptables_tproxy( + self.output_chain_name.as_deref(), + self.prerouting_chain_name.as_deref(), + ); + } + TProxyStrategy::None => {} + } + } +} + +fn command_exists(command: &str) -> bool { + Command::new(command).arg("-v").output().is_ok() +} + +const DEFAULT_OUTPUT_DIVERT_CHAIN: &str = "CLASH_TPROXY_OUTPUT_DIVERT"; +const DEFAULT_OUTPUT_CHAIN: &str = "CLASH_TPROXY_OUTPUT"; +const DEFAULT_PREROUTING_CHAIN: &str = "CLASH_TPROXY_PREROUTING"; +const POLICY_ROUTING_TABLE_NUM: u32 = 400; +const DEFAULT_TPROXY_MARK: u32 = 0x1; +const DEFAULT_TPROXY_MARK_MUSK: &str = "0x1/0x1"; + +// TODO: add a cli to mamange these reserved addresses more easily? +const RESERVED_ADDRS: &[&str] = &[ + "0/8", + "10/8", + "100.64/10", + "127/8", + "169.254/16", + "172.16/12", + "192/24", + "192.0.2.0/24", + "192.88.99/24", + "192.168/16", + "198.18/15", + "198.51.100/24", + "203.0.113/24", + "224/4", + "240/4", + "255.255.255.255/32", +]; + +// TODO: handle corner cases in iptables' rules +pub fn setup_iptables_tproxy( + skip_mark: u32, + tproxy_port: u16, + output_chain_name: Option<&str>, + prerouting_chain_name: Option<&str>, +) { + let divert_chain_name = DEFAULT_OUTPUT_DIVERT_CHAIN; + let output_chain_name = output_chain_name.unwrap_or(DEFAULT_OUTPUT_CHAIN); + let prerouting_chain_name = prerouting_chain_name.unwrap_or(DEFAULT_PREROUTING_CHAIN); + + debug!( + "setup tproxy via iptables, policy_routing_table_num: {}, tproxy_mark: {}, output_chain_name: {}, prerouting_chain_name: {}", + POLICY_ROUTING_TABLE_NUM, DEFAULT_TPROXY_MARK, output_chain_name, prerouting_chain_name + ); + + run_cmd!(ip rule add fwmark $DEFAULT_TPROXY_MARK lookup $POLICY_ROUTING_TABLE_NUM); + run_cmd!(ip route add local "0.0.0.0/0" dev lo table $POLICY_ROUTING_TABLE_NUM); + + // re-route the packet flow to the local listener by cooperating with `ip rule`'s fwmark + // to avoid the infinite loop + run_cmd!(iptables "-t" mangle "-N" $output_chain_name); + run_cmd!(iptables "-t" mangle "-F" $output_chain_name); + for addr in RESERVED_ADDRS { + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-d" $addr "-j" RETURN); + } + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-j" RETURN "-m" mark "--mark" $skip_mark); + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-m" addrtype "--dst-type" LOCAL "-j" RETURN); + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-m" addrtype "--dst-type" BROADCAST "-j" RETURN); + // dig example.com => 93.184.216.34 + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-p" tcp "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-p" udp "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); + run_cmd!(iptables "-t" mangle "-A" OUTPUT -p tcp "-j" $output_chain_name); + run_cmd!(iptables "-t" mangle "-A" OUTPUT -p udp "-j" $output_chain_name); + + // for optimization of tcp + run_cmd!(iptables "-t" mangle "-N" $divert_chain_name); + run_cmd!(iptables "-t" mangle "-F" $divert_chain_name); + run_cmd!(iptables "-t" mangle "-A" $divert_chain_name "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); + run_cmd!(iptables "-t" mangle "-A" $divert_chain_name "-j" ACCEPT); + + // to catch the output socket to the listening socket on tproxy-port + run_cmd!(iptables "-t" mangle "-N" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-F" $prerouting_chain_name); + for addr in RESERVED_ADDRS { + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-d" $addr "-j" RETURN); + } + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" addrtype "--dst-type" LOCAL "-j" LOG "--log-prefix" "\"LOCAL IN\""); + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" addrtype "--dst-type" LOCAL "-j" RETURN); + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-m" mark "--mark" $skip_mark "-j" RETURN); + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" tcp "-m" socket "-j" divert_chain_name); + + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" tcp "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" udp "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); + run_cmd!(iptables "-t" mangle "-A" PREROUTING "-p" tcp "-j" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-A" PREROUTING "-p" udp "-j" $prerouting_chain_name); +} + +pub fn clean_iptables_tproxy(output_chain_name: Option<&str>, prerouting_chain_name: Option<&str>) { + let output_chain_name = output_chain_name.unwrap_or(DEFAULT_OUTPUT_CHAIN); + let prerouting_chain_name = prerouting_chain_name.unwrap_or(DEFAULT_PREROUTING_CHAIN); + debug!( + "clean tproxy via iptables, policy_routing_table_num: {}, tproxy_mark: {}, output_chain_name: {}, prerouting_chain_name: {}", + POLICY_ROUTING_TABLE_NUM, DEFAULT_TPROXY_MARK, output_chain_name, prerouting_chain_name + ); + run_cmd!(ip rule del fwmark $DEFAULT_TPROXY_MARK lookup $POLICY_ROUTING_TABLE_NUM); + run_cmd!(ip route flush table $POLICY_ROUTING_TABLE_NUM); + + // must delete the ref in the OUTPUT chain before deleting the chain + run_cmd!(iptables "-t" mangle "-D" OUTPUT "-p" "tcp" "-j" $output_chain_name); + run_cmd!(iptables "-t" mangle "-D" OUTPUT "-p" "udp" "-j" $output_chain_name); + // must flush the user-defined chain before delete it + run_cmd!(iptables "-t" mangle "-F" $output_chain_name); + run_cmd!(iptables "-t" mangle "-X" $output_chain_name); + + run_cmd!(iptables "-t" mangle "-D" PREROUTING "-p" "tcp" "-j" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-D" PREROUTING "-p" "udp" "-j" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-F" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-X" $prerouting_chain_name); +} + +#[test] +#[ignore = "only for cleanup after manual testing"] +fn test_clean() { + clean_iptables_tproxy(None, None); +} diff --git a/clash_lib/src/proxy/tproxy/mod.rs b/clash_lib/src/proxy/tproxy/mod.rs new file mode 100644 index 000000000..39db67cd6 --- /dev/null +++ b/clash_lib/src/proxy/tproxy/mod.rs @@ -0,0 +1,8 @@ +#[allow(warnings)] +pub mod iptables; +#[allow(warnings)] +pub mod transparent_socket; + +pub mod inbound; + +pub use inbound::Listener as TProxyListener; diff --git a/clash_lib/src/proxy/tproxy/transparent_socket.rs b/clash_lib/src/proxy/tproxy/transparent_socket.rs new file mode 100644 index 000000000..f917336e5 --- /dev/null +++ b/clash_lib/src/proxy/tproxy/transparent_socket.rs @@ -0,0 +1,95 @@ +use std::net::SocketAddr; +use std::os::unix::io::AsRawFd; +use std::{io, mem}; + +use tokio::net::{TcpListener, TcpSocket, UdpSocket}; + +macro_rules! syscall { + ($fn: ident ( $($arg: expr),* $(,)* ) ) => {{ + let res = unsafe { libc::$fn($($arg, )*) }; + if res == -1 { + Err(std::io::Error::last_os_error()) + } else { + Ok(res) + } + }}; +} + +/// Set IP_TRANSPARENT for use of tproxy. +/// User may need to get root privilege to use it. +fn set_ip_transparent(socket_fd: i32) -> io::Result { + let enable: libc::c_int = 1; + syscall!(setsockopt( + socket_fd, + libc::SOL_IP, + libc::IP_TRANSPARENT, + &enable as *const _ as *const _, + mem::size_of_val(&enable) as libc::socklen_t, + )) +} + +fn set_recv_tos(socket_fd: i32) -> io::Result { + let enable: libc::c_int = 1; + syscall!(setsockopt( + socket_fd, + libc::IPPROTO_IP, + libc::IP_RECVTOS, + &enable as *const _ as *const _, + mem::size_of_val(&enable) as libc::socklen_t, + )) +} + +fn set_mark(socket_fd: i32, mark: u32) -> io::Result { + syscall!(setsockopt( + socket_fd, + libc::SOL_SOCKET, + libc::SO_MARK, + &mark as *const _ as *const _, + mem::size_of_val(&mark) as libc::socklen_t, + )) +} + +fn set_recv_origin_dst(socket_fd: i32) -> io::Result { + let enable: libc::c_int = 1; + syscall!(setsockopt( + socket_fd, + libc::IPPROTO_IP, + libc::IP_RECVORIGDSTADDR, + &enable as *const _ as *const _, + mem::size_of_val(&enable) as libc::socklen_t, + )) +} + +fn set_reuse(socket_fd: i32) -> io::Result { + let enable: libc::c_int = 1; + syscall!(setsockopt( + socket_fd, + libc::SOL_SOCKET, + libc::SO_REUSEADDR, + &enable as *const _ as *const _, + mem::size_of_val(&enable) as libc::socklen_t, + )) +} + +// only support v4 now +pub fn new_tcp_listener(addr: SocketAddr) -> io::Result { + let socket = TcpSocket::new_v4()?; + set_ip_transparent(socket.as_raw_fd())?; + socket.set_reuseaddr(true)?; + socket.bind(addr)?; + Ok(socket.listen(1024)?) +} + +// TODO: return a udp transparent listener +pub async fn new_udp_listener(addr: SocketAddr) -> io::Result { + let socket = unix_udp_sock::UdpSocket::bind(addr).await?; + let fd = socket.as_raw_fd(); + set_ip_transparent(fd)?; + set_reuse(fd)?; + set_recv_origin_dst(fd)?; + // TODO: support tos + set_recv_tos(fd)?; + set_mark(fd, 0xff)?; + + Ok(socket) +} diff --git a/clash_lib/src/proxy/trojan/mod.rs b/clash_lib/src/proxy/trojan/mod.rs index 233b8b92e..dbd11e842 100644 --- a/clash_lib/src/proxy/trojan/mod.rs +++ b/clash_lib/src/proxy/trojan/mod.rs @@ -151,13 +151,15 @@ impl OutboundHandler for Handler { sess: &Session, resolver: ThreadSafeDNSResolver, ) -> io::Result { + let (packet_mark, iface) = self.opts.common_opts.merge(sess); + let stream = new_tcp_stream( resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - None, + packet_mark, ) .map_err(|x| { io::Error::new( @@ -191,13 +193,14 @@ impl OutboundHandler for Handler { sess: &Session, resolver: ThreadSafeDNSResolver, ) -> io::Result { + let (packet_mark, iface) = self.opts.common_opts.merge(sess); let stream = new_tcp_stream( resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - None, + packet_mark, ) .map_err(|x| { io::Error::new( diff --git a/clash_lib/src/proxy/tun/inbound.rs b/clash_lib/src/proxy/tun/inbound.rs index 7e1a33393..194ea8161 100644 --- a/clash_lib/src/proxy/tun/inbound.rs +++ b/clash_lib/src/proxy/tun/inbound.rs @@ -1,4 +1,4 @@ -use super::{datagram::TunDatagram, netstack}; +use super::netstack; use std::{net::SocketAddr, sync::Arc}; use futures::{SinkExt, StreamExt}; @@ -8,7 +8,7 @@ use url::Url; use crate::{ app::{dispatcher::Dispatcher, dns::ThreadSafeDNSResolver}, - common::errors::map_io_error, + common::{errors::map_io_error, tunnel_datagram::TunnelDatagram}, config::internal::config::TunConfig, proxy::datagram::UdpPacket, session::{Network, Session, SocksAddr, Type}, @@ -37,9 +37,6 @@ async fn handle_inbound_datagram( dispatcher: Arc, resolver: ThreadSafeDNSResolver, ) { - let local_addr = socket.local_addr(); - // tun i/o - let (ls, mut lr) = socket.split(); let ls = Arc::new(ls); @@ -51,7 +48,7 @@ async fn handle_inbound_datagram( // for dispatcher - the dispatcher would receive packets from this channel, which is from the stack // and send back packets to this channel, which is to the tun - let udp_stream = TunDatagram::new(l_tx, d_rx, local_addr); + let udp_stream = TunnelDatagram::new(l_tx, d_rx); let sess = Session { network: Network::Udp, @@ -66,7 +63,7 @@ async fn handle_inbound_datagram( while let Some(pkt) = l_rx.recv().await { trace!("tun <- dispatcher: {:?}", pkt); // populate the correct src_addr, though is it necessary? - let src_addr = match pkt.src_addr { + let src_addr: SocketAddr = match pkt.src_addr { SocksAddr::Ip(ip) => ip, SocksAddr::Domain(host, port) => { match resolver.resolve(&host, resolver.fake_ip_enabled()).await { diff --git a/clash_lib/src/proxy/tun/mod.rs b/clash_lib/src/proxy/tun/mod.rs index cde10543e..a84455efa 100644 --- a/clash_lib/src/proxy/tun/mod.rs +++ b/clash_lib/src/proxy/tun/mod.rs @@ -1,4 +1,3 @@ pub mod inbound; -pub use netstack_lwip as netstack; -mod datagram; pub use inbound::get_runner as get_tun_runner; +pub use netstack_lwip as netstack; diff --git a/clash_lib/src/proxy/utils/socket_helpers.rs b/clash_lib/src/proxy/utils/socket_helpers.rs index fe9ef4b82..362818228 100644 --- a/clash_lib/src/proxy/utils/socket_helpers.rs +++ b/clash_lib/src/proxy/utils/socket_helpers.rs @@ -124,6 +124,23 @@ pub async fn new_udp_socket( src: Option<&SocketAddr>, iface: Option<&Interface>, #[cfg(any(target_os = "linux", target_os = "android"))] packet_mark: Option, +) -> io::Result { + new_udp_socket_inner(false, src, iface, packet_mark).await +} + +pub async fn new_transparent_udp_socket( + src: Option<&SocketAddr>, + iface: Option<&Interface>, + #[cfg(any(target_os = "linux", target_os = "android"))] packet_mark: Option, +) -> io::Result { + new_udp_socket_inner(true, src, iface, packet_mark).await +} + +pub async fn new_udp_socket_inner( + transparent: bool, + src: Option<&SocketAddr>, + iface: Option<&Interface>, + #[cfg(any(target_os = "linux", target_os = "android"))] packet_mark: Option, ) -> io::Result { let socket = match src { Some(src) => { @@ -136,6 +153,18 @@ pub async fn new_udp_socket( None => socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::DGRAM, None)?, }; + // see: https://docs.kernel.org/networking/tproxy.html#making-non-local-sockets-work + // **All you have to do is enable the (SOL_IP, IP_TRANSPARENT) socket option before calling bind** + // for tcp and udp, situations are slightly different: + // 1. for tcp, all you need to do is to create a listener with IP_TRANSPARENT enabled, since the packet flow + // is based on the stream, the ingress and egress of the stream will be handled by system stack properly + // 2, for udp, the socket that binds the tproxy-port shall have IP_TRANSPARENT enabled, + // and the response socket should also have IP_TRANSPARENT enabled, in order to sink a packet that + // have any (src, sport), which is the (dst, sport) of the inbound packet + if transparent { + socket.set_ip_transparent(true)?; + } + if let Some(src) = src { socket.bind(&(*src).into())?; } diff --git a/clash_lib/src/proxy/vmess/mod.rs b/clash_lib/src/proxy/vmess/mod.rs index c600a0efc..07724159c 100644 --- a/clash_lib/src/proxy/vmess/mod.rs +++ b/clash_lib/src/proxy/vmess/mod.rs @@ -169,14 +169,14 @@ impl OutboundHandler for Handler { sess: &Session, resolver: ThreadSafeDNSResolver, ) -> io::Result { - debug!("Connecting to {} via VMess", sess); + let (packet_mark, iface) = self.opts.common_opts.merge(sess); let stream = new_tcp_stream( resolver, self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - None, + packet_mark, ) .map_err(|x| { io::Error::new( @@ -210,13 +210,14 @@ impl OutboundHandler for Handler { sess: &Session, resolver: ThreadSafeDNSResolver, ) -> io::Result { + let (packet_mark, iface) = self.opts.common_opts.merge(sess); let stream = new_tcp_stream( resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - None, + packet_mark, ) .map_err(|x| { io::Error::new( diff --git a/clash_lib/src/session.rs b/clash_lib/src/session.rs index be5a17399..27e3d2cde 100644 --- a/clash_lib/src/session.rs +++ b/clash_lib/src/session.rs @@ -12,6 +12,13 @@ use tokio::io::{AsyncRead, AsyncReadExt}; use erased_serde::Serialize as ESerialize; +// mark of the packet from clash-rs +static DEFAULT_PACKET_MARK: u32 = 0xff; + +pub(crate) fn get_packet_mark() -> u32 { + DEFAULT_PACKET_MARK +} + #[derive(Debug, PartialEq, Eq, Serialize)] pub enum SocksAddr { Ip(SocketAddr), @@ -353,6 +360,7 @@ pub enum Type { Http, HttpConnect, Socks5, + TProxy, Tun, } @@ -409,7 +417,7 @@ impl Default for Session { typ: Type::Http, source: SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0), destination: SocksAddr::any_ipv4(), - packet_mark: None, + packet_mark: Some(get_packet_mark()), iface: None, } }