From c2322815cfe11e18421a847421160a4d615b20ea Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Sat, 23 Mar 2024 11:16:27 +0800 Subject: [PATCH 01/11] feat:tproxy draft --- Cargo.lock | 104 ++++++++- clash_lib/Cargo.toml | 2 + .../src/app/dispatcher/dispatcher_impl.rs | 1 + clash_lib/src/app/inbound/manager.rs | 20 +- clash_lib/src/app/inbound/network_listener.rs | 14 +- clash_lib/src/common/cmd.rs | 0 clash_lib/src/common/mod.rs | 2 + .../datagram.rs => common/tunnel_datagram.rs} | 21 +- clash_lib/src/config/def.rs | 21 ++ clash_lib/src/proxy/direct/mod.rs | 6 +- clash_lib/src/proxy/mod.rs | 5 +- clash_lib/src/proxy/relay/mod.rs | 4 +- clash_lib/src/proxy/shadowsocks/mod.rs | 8 +- clash_lib/src/proxy/socks/inbound/stream.rs | 6 +- clash_lib/src/proxy/tproxy/inbound.rs | 175 +++++++++++++++ clash_lib/src/proxy/tproxy/iptables.rs | 205 ++++++++++++++++++ clash_lib/src/proxy/tproxy/mod.rs | 8 + .../src/proxy/tproxy/transparent_socket.rs | 95 ++++++++ clash_lib/src/proxy/trojan/mod.rs | 8 +- clash_lib/src/proxy/tun/inbound.rs | 11 +- clash_lib/src/proxy/tun/mod.rs | 1 - clash_lib/src/proxy/utils/socket_helpers.rs | 30 +++ clash_lib/src/proxy/vmess/mod.rs | 9 +- clash_lib/src/session.rs | 11 +- 24 files changed, 713 insertions(+), 54 deletions(-) create mode 100644 clash_lib/src/common/cmd.rs rename clash_lib/src/{proxy/tun/datagram.rs => common/tunnel_datagram.rs} (85%) create mode 100644 clash_lib/src/proxy/tproxy/inbound.rs create mode 100644 clash_lib/src/proxy/tproxy/iptables.rs create mode 100644 clash_lib/src/proxy/tproxy/mod.rs create mode 100644 clash_lib/src/proxy/tproxy/transparent_socket.rs diff --git a/Cargo.lock b/Cargo.lock index b1ed56768..e53477e06 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", @@ -1046,6 +1046,7 @@ dependencies = [ "cfb-mode", "chacha20poly1305", "chrono", + "cmd_lib", "console-subscriber", "crc32fast", "criterion", @@ -1094,7 +1095,7 @@ dependencies = [ "sha2", "shadowsocks", "smoltcp", - "socket2", + "socket2 0.5.6", "tempfile", "thiserror", "tokio", @@ -1112,6 +1113,7 @@ dependencies = [ "tracing-subscriber", "tracing-timing", "tun", + "unix-udp-sock", "url", "uuid", "webpki-roots", @@ -1126,6 +1128,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.33" @@ -1878,6 +1906,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" @@ -1914,6 +1955,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" @@ -2654,7 +2706,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -2721,7 +2773,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.1.0", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tracing", ] @@ -2892,7 +2944,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", @@ -3636,6 +3688,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" @@ -4820,7 +4882,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "shadowsocks-crypto", - "socket2", + "socket2 0.5.6", "spin 0.9.8", "thiserror", "tokio", @@ -4954,6 +5016,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" @@ -5309,7 +5381,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", @@ -5382,7 +5454,7 @@ dependencies = [ "log", "once_cell", "pin-project", - "socket2", + "socket2 0.5.6", "tokio", "windows-sys 0.48.0", ] @@ -6611,6 +6683,22 @@ dependencies = [ "subtle", ] +[[package]] +name = "unix-udp-sock" +version = "0.7.0" +source = "git+https://github.com/Watfaq/unix-udp-sock.git#4760df7e624351189b8b1d9289413f32772618e2" +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_lib/Cargo.toml b/clash_lib/Cargo.toml index ebd7ec2e5..435311ecd 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"} +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..16fb39bb7 100644 --- a/clash_lib/src/app/inbound/manager.rs +++ b/clash_lib/src/app/inbound/manager.rs @@ -88,7 +88,7 @@ impl InboundManager { }; self.network_listeners .values() - .for_each(|x| match x.listener_type { + .for_each(|x: &NetworkInboundListener| match x.listener_type { ListenerType::Http => { ports.port = Some(x.port); } @@ -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,21 @@ 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..e69de29bb 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/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 9b0fe0b2b..96b0858c5 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; @@ -84,9 +86,10 @@ impl OutboundDatagram for T where pub type AnyOutboundDatagram = Box>; +// TODO: delete this struct +#[allow(dead_code)] #[derive(Default, Debug, Clone)] pub struct CommonOption { - #[allow(dead_code)] so_mark: Option, iface: Option, } 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 f3e42e743..856f87ab8 100644 --- a/clash_lib/src/proxy/shadowsocks/mod.rs +++ b/clash_lib/src/proxy/shadowsocks/mod.rs @@ -183,9 +183,9 @@ impl OutboundHandler for Handler { resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .map_err(|x| { io::Error::new( @@ -271,9 +271,9 @@ impl OutboundHandler for Handler { ); let socket = new_udp_socket( None, - self.opts.common_opts.iface.as_ref(), + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.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..417612e17 --- /dev/null +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -0,0 +1,205 @@ +use cmd_lib::run_cmd; +use std::process::Command; +use tracing::{debug, error}; + +#[derive(Debug, Clone, Copy)] +pub enum TProxyStrategy { + Nftables, + Iptables, + None, +} + +impl From<&str> for TProxyStrategy { + fn from(s: &str) -> Self { + match s { + "nft" => TProxyStrategy::Nftables, + "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::Nftables => { + // TODO: support nft + } + TProxyStrategy::Iptables => { + setup_iptables_tproxy( + skip_mark, + tproxy_port, + output_chain_name, + prerouting_chain_name, + ); + } + TProxyStrategy::None => { + error!("No tproxy command found"); + } + } + } +} + +impl Default for TProxyStrategy { + // auto detect command in system + fn default() -> Self { + if command_exists("nft") { + TProxyStrategy::Nftables + } else if command_exists("iptables") { + TProxyStrategy::Iptables + } else { + TProxyStrategy::None + } + } +} +#[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 { + // clean before setup + clean_iptables_tproxy( + output_chain_name.as_deref(), + prerouting_chain_name.as_deref(), + ); + 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::Nftables => { + // TODO: support nft + } + 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_RS_TPROXY_OUTPUT_DIVERT"; +const DEFAULT_OUTPUT_CHAIN: &str = "CLASH_RS_TPROXY_OUTPUT"; +const DEFAULT_PREROUTING_CHAIN: &str = "CLASH_RS_TPROXY_PREROUTING"; +const POLICY_ROUTING_TABLE_NUM: u32 = 400; +const DEFAULT_TPROXY_MARK: u32 = 0x1; +const DEFAULT_TPROXY_MARK_MUSK: &str = "0x1/0x1"; + +// 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!( + "clean iptables tproxy, 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); + 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 "--dst" "93.184.216.34" "--dport" 80 "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-p" udp "--dst" "1.0.0.1" "--dport" 53 -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 port 7893 + run_cmd!(iptables "-t" mangle "-N" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-F" $prerouting_chain_name); + 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 "--dst" "93.184.216.34" "--dport" 80 "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" udp "--dst" "1.0.0.1" "--dport" 53 "-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 iptables tproxy, 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); + + run_cmd!(iptables "-t" mangle "-D" OUTPUT "-j" $output_chain_name); + 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 "-j" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-F" $prerouting_chain_name); + run_cmd!(iptables "-t" mangle "-X" $prerouting_chain_name); +} + +#[test] +fn test_setup() { + setup_iptables_tproxy(0xff, 7893, None, None); +} + +#[test] +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 b11915dab..3a2f73db3 100644 --- a/clash_lib/src/proxy/trojan/mod.rs +++ b/clash_lib/src/proxy/trojan/mod.rs @@ -155,9 +155,9 @@ impl OutboundHandler for Handler { resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .map_err(|x| { io::Error::new( @@ -195,9 +195,9 @@ impl OutboundHandler for Handler { resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.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..10f2737f7 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; diff --git a/clash_lib/src/proxy/utils/socket_helpers.rs b/clash_lib/src/proxy/utils/socket_helpers.rs index fe9ef4b82..f557555e7 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,19 @@ 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 327996a50..99dc809e9 100644 --- a/clash_lib/src/proxy/vmess/mod.rs +++ b/clash_lib/src/proxy/vmess/mod.rs @@ -36,6 +36,7 @@ pub enum VmessTransport { pub struct HandlerOptions { pub name: String, + // TODO: @VendettaReborn, delete this after confirmed pub common_opts: CommonOption, pub server: String, pub port: u16, @@ -172,9 +173,9 @@ impl OutboundHandler for Handler { resolver, self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .map_err(|x| { io::Error::new( @@ -212,9 +213,9 @@ impl OutboundHandler for Handler { resolver.clone(), self.opts.server.as_str(), self.opts.port, - self.opts.common_opts.iface.as_ref(), + sess.iface.as_ref(), #[cfg(any(target_os = "linux", target_os = "android"))] - None, + sess.packet_mark, ) .map_err(|x| { io::Error::new( diff --git a/clash_lib/src/session.rs b/clash_lib/src/session.rs index be5a17399..686b20b95 100644 --- a/clash_lib/src/session.rs +++ b/clash_lib/src/session.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; +use std::sync::atomic::AtomicU32; use std::{ io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, @@ -12,6 +13,13 @@ use tokio::io::{AsyncRead, AsyncReadExt}; use erased_serde::Serialize as ESerialize; +// mark of the packet from clash-rs +static DEFAULT_PACKET_MARK: AtomicU32 = AtomicU32::new(0xff); + +pub(crate) fn get_packet_mark() -> u32 { + DEFAULT_PACKET_MARK.load(std::sync::atomic::Ordering::Relaxed) +} + #[derive(Debug, PartialEq, Eq, Serialize)] pub enum SocksAddr { Ip(SocketAddr), @@ -353,6 +361,7 @@ pub enum Type { Http, HttpConnect, Socks5, + TProxy, Tun, } @@ -409,7 +418,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, } } From 983387412f097b4d73d867ea3dd60110a130eae7 Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Tue, 26 Mar 2024 23:12:54 +0800 Subject: [PATCH 02/11] fix wrong rules in iptables --- clash/tests/data/config/tproxy.yaml | 4 ++ clash_lib/src/proxy/tproxy/iptables.rs | 51 +++++++++++--------------- 2 files changed, 25 insertions(+), 30 deletions(-) create mode 100644 clash/tests/data/config/tproxy.yaml 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/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs index 417612e17..97c85295d 100644 --- a/clash_lib/src/proxy/tproxy/iptables.rs +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -2,9 +2,9 @@ use cmd_lib::run_cmd; use std::process::Command; use tracing::{debug, error}; +// TODO: support nftables #[derive(Debug, Clone, Copy)] pub enum TProxyStrategy { - Nftables, Iptables, None, } @@ -12,7 +12,6 @@ pub enum TProxyStrategy { impl From<&str> for TProxyStrategy { fn from(s: &str) -> Self { match s { - "nft" => TProxyStrategy::Nftables, "iptables" => TProxyStrategy::Iptables, _ => TProxyStrategy::None, } @@ -28,10 +27,11 @@ impl TProxyStrategy { prerouting_chain_name: Option<&str>, ) { match self { - TProxyStrategy::Nftables => { - // TODO: support nft - } TProxyStrategy::Iptables => { + clean_iptables_tproxy( + output_chain_name, + prerouting_chain_name, + ); setup_iptables_tproxy( skip_mark, tproxy_port, @@ -49,9 +49,7 @@ impl TProxyStrategy { impl Default for TProxyStrategy { // auto detect command in system fn default() -> Self { - if command_exists("nft") { - TProxyStrategy::Nftables - } else if command_exists("iptables") { + if command_exists("iptables") { TProxyStrategy::Iptables } else { TProxyStrategy::None @@ -75,11 +73,6 @@ impl TProxyGuard { output_chain_name: Option, prerouting_chain_name: Option, ) -> Self { - // clean before setup - clean_iptables_tproxy( - output_chain_name.as_deref(), - prerouting_chain_name.as_deref(), - ); strategy.setup( skip_mark, tproxy_port, @@ -99,9 +92,6 @@ impl TProxyGuard { impl Drop for TProxyGuard { fn drop(&mut self) { match self.strategy { - TProxyStrategy::Nftables => { - // TODO: support nft - } TProxyStrategy::Iptables => { clean_iptables_tproxy( self.output_chain_name.as_deref(), @@ -117,9 +107,9 @@ fn command_exists(command: &str) -> bool { Command::new(command).arg("-v").output().is_ok() } -const DEFAULT_OUTPUT_DIVERT_CHAIN: &str = "CLASH_RS_TPROXY_OUTPUT_DIVERT"; -const DEFAULT_OUTPUT_CHAIN: &str = "CLASH_RS_TPROXY_OUTPUT"; -const DEFAULT_PREROUTING_CHAIN: &str = "CLASH_RS_TPROXY_PREROUTING"; +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"; @@ -151,8 +141,8 @@ pub fn setup_iptables_tproxy( 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 "--dst" "93.184.216.34" "--dport" 80 "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); - run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-p" udp "--dst" "1.0.0.1" "--dport" 53 -j MARK "--set-mark" $DEFAULT_TPROXY_MARK); + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-p" tcp "--dst" "192.168.2.6" "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); + run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-p" udp "--dst" "192.168.2.6" "-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); @@ -162,15 +152,15 @@ pub fn setup_iptables_tproxy( 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 port 7893 + // 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); 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 "--dst" "93.184.216.34" "--dport" 80 "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); - run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" udp "--dst" "1.0.0.1" "--dport" 53 "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" tcp "--dst" "192.168.2.6" "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); + run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" udp "--dst" "192.168.2.6" "-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); } @@ -185,21 +175,22 @@ pub fn clean_iptables_tproxy(output_chain_name: Option<&str>, prerouting_chain_n run_cmd!(ip rule del fwmark $DEFAULT_TPROXY_MARK lookup $POLICY_ROUTING_TABLE_NUM); run_cmd!(ip route flush table $POLICY_ROUTING_TABLE_NUM); - run_cmd!(iptables "-t" mangle "-D" OUTPUT "-j" $output_chain_name); + // 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 "-j" $prerouting_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] -fn test_setup() { - setup_iptables_tproxy(0xff, 7893, None, None); -} #[test] +#[ignore = "only for cleanup after manual testing"] fn test_clean() { clean_iptables_tproxy(None, None); } From c207d67c78bafaf1e27faebbd5fd0b1910c49664 Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Tue, 26 Mar 2024 23:23:35 +0800 Subject: [PATCH 03/11] fix logs --- clash_lib/src/proxy/tproxy/iptables.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/clash_lib/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs index 97c85295d..96f96cabf 100644 --- a/clash_lib/src/proxy/tproxy/iptables.rs +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -126,7 +126,7 @@ pub fn setup_iptables_tproxy( let prerouting_chain_name = prerouting_chain_name.unwrap_or(DEFAULT_PREROUTING_CHAIN); debug!( - "clean iptables tproxy, policy_routing_table_num: {}, tproxy_mark: {}, output_chain_name: {}, prerouting_chain_name: {}", + "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 ); @@ -169,7 +169,7 @@ pub fn clean_iptables_tproxy(output_chain_name: Option<&str>, prerouting_chain_n 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 iptables tproxy, policy_routing_table_num: {}, tproxy_mark: {}, output_chain_name: {}, prerouting_chain_name: {}", + "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); From 3eb4f40a4a82bf72ca996204e7c14cdc6e334631 Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Wed, 27 Mar 2024 21:20:04 +0800 Subject: [PATCH 04/11] fix iptables rules --- clash_lib/src/proxy/tproxy/iptables.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/clash_lib/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs index 96f96cabf..7ce265b4d 100644 --- a/clash_lib/src/proxy/tproxy/iptables.rs +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -141,8 +141,8 @@ pub fn setup_iptables_tproxy( 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 "--dst" "192.168.2.6" "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); - run_cmd!(iptables "-t" mangle "-A" $output_chain_name "-p" udp "--dst" "192.168.2.6" "-j" MARK "--set-mark" $DEFAULT_TPROXY_MARK); + 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); @@ -159,8 +159,8 @@ pub fn setup_iptables_tproxy( 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 "--dst" "192.168.2.6" "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); - run_cmd!(iptables "-t" mangle "-A" $prerouting_chain_name "-p" udp "--dst" "192.168.2.6" "-j" TPROXY "--tproxy-mark" $DEFAULT_TPROXY_MARK_MUSK "--on-port" $tproxy_port); + 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); } From 00e5047a183ad20d8615babc125150e0bc6535fd Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Wed, 27 Mar 2024 22:30:15 +0800 Subject: [PATCH 05/11] fix iptables rules, add reserved addrs --- clash_lib/src/proxy/tproxy/iptables.rs | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/clash_lib/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs index 7ce265b4d..28de8563c 100644 --- a/clash_lib/src/proxy/tproxy/iptables.rs +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -28,10 +28,7 @@ impl TProxyStrategy { ) { match self { TProxyStrategy::Iptables => { - clean_iptables_tproxy( - output_chain_name, - prerouting_chain_name, - ); + clean_iptables_tproxy(output_chain_name, prerouting_chain_name); setup_iptables_tproxy( skip_mark, tproxy_port, @@ -114,6 +111,19 @@ 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", + "127/8", + "172.16/12", + "169.254/16", + "192.168/16", + "224/4", + "240/4", + "255.255.255.255/32", +]; + // TODO: handle corner cases in iptables' rules pub fn setup_iptables_tproxy( skip_mark: u32, @@ -137,6 +147,9 @@ pub fn setup_iptables_tproxy( // 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); @@ -155,6 +168,10 @@ pub fn setup_iptables_tproxy( // 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); @@ -188,7 +205,6 @@ pub fn clean_iptables_tproxy(output_chain_name: Option<&str>, prerouting_chain_n run_cmd!(iptables "-t" mangle "-X" $prerouting_chain_name); } - #[test] #[ignore = "only for cleanup after manual testing"] fn test_clean() { From 93838beef11d002cac3ac61428ec411e62ebde79 Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Wed, 27 Mar 2024 22:34:49 +0800 Subject: [PATCH 06/11] update unix-udp-sock dep --- Cargo.lock | 2 +- clash_lib/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e53477e06..aa1e7774d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6686,7 +6686,7 @@ dependencies = [ [[package]] name = "unix-udp-sock" version = "0.7.0" -source = "git+https://github.com/Watfaq/unix-udp-sock.git#4760df7e624351189b8b1d9289413f32772618e2" +source = "git+https://github.com/Watfaq/unix-udp-sock.git?rev=cd3e4eca43e6f3be82a2703c3d711b7e18fbfd18#cd3e4eca43e6f3be82a2703c3d711b7e18fbfd18" dependencies = [ "bytes", "futures-core", diff --git a/clash_lib/Cargo.toml b/clash_lib/Cargo.toml index 435311ecd..4d43d27d6 100644 --- a/clash_lib/Cargo.toml +++ b/clash_lib/Cargo.toml @@ -37,7 +37,7 @@ 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"} +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" From 9a2e8e9aab80d85b01dadfb396fe76f5baee431c Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Thu, 28 Mar 2024 10:40:04 +0800 Subject: [PATCH 07/11] update reserved addrs for tproxy --- clash_lib/src/proxy/tproxy/iptables.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/clash_lib/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs index 28de8563c..a3eff058f 100644 --- a/clash_lib/src/proxy/tproxy/iptables.rs +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -115,10 +115,17 @@ const DEFAULT_TPROXY_MARK_MUSK: &str = "0x1/0x1"; const RESERVED_ADDRS: &[&str] = &[ "0/8", "10/8", + "100.64/10", "127/8", - "172.16/12", "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", From e06531663c25fa0f893ae9094dc114ab30449c69 Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Thu, 28 Mar 2024 15:48:52 +0800 Subject: [PATCH 08/11] override the iface&so_mark in each proxy's config --- clash_lib/src/config/internal/proxy.rs | 13 +++++++++ clash_lib/src/proxy/converters/shadowsocks.rs | 4 +-- clash_lib/src/proxy/converters/trojan.rs | 4 +-- clash_lib/src/proxy/converters/vmess.rs | 4 +-- clash_lib/src/proxy/converters/wireguard.rs | 2 +- clash_lib/src/proxy/mod.rs | 27 +++++++++++++++++-- clash_lib/src/proxy/shadowsocks/mod.rs | 10 ++++--- clash_lib/src/proxy/trojan/mod.rs | 11 +++++--- clash_lib/src/proxy/vmess/mod.rs | 10 ++++--- 9 files changed, 64 insertions(+), 21 deletions(-) diff --git a/clash_lib/src/config/internal/proxy.rs b/clash_lib/src/config/internal/proxy.rs index 0603c7348..d7a44b956 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; @@ -111,6 +112,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, @@ -125,6 +128,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, @@ -137,6 +142,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, @@ -158,6 +165,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, @@ -174,6 +183,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, @@ -195,6 +206,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..bcbfc3f97 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 9b70ee7b8..9f39ef7dd 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/mod.rs b/clash_lib/src/proxy/mod.rs index 96b0858c5..2d4446413 100644 --- a/clash_lib/src/proxy/mod.rs +++ b/clash_lib/src/proxy/mod.rs @@ -86,14 +86,37 @@ impl OutboundDatagram for T where pub type AnyOutboundDatagram = Box>; -// TODO: delete this struct #[allow(dead_code)] -#[derive(Default, Debug, Clone)] +#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct CommonOption { + #[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/shadowsocks/mod.rs b/clash_lib/src/proxy/shadowsocks/mod.rs index 856f87ab8..d9b41d532 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, - sess.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - sess.packet_mark, + 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, - sess.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - sess.packet_mark, + packet_mark, ) .await?; let socket = ProxySocket::from_socket(UdpSocketType::Client, ctx, &cfg, socket); diff --git a/clash_lib/src/proxy/trojan/mod.rs b/clash_lib/src/proxy/trojan/mod.rs index 3a2f73db3..02be040a7 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, - sess.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - sess.packet_mark, + 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, - sess.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - sess.packet_mark, + packet_mark, ) .map_err(|x| { io::Error::new( diff --git a/clash_lib/src/proxy/vmess/mod.rs b/clash_lib/src/proxy/vmess/mod.rs index 99dc809e9..dab384642 100644 --- a/clash_lib/src/proxy/vmess/mod.rs +++ b/clash_lib/src/proxy/vmess/mod.rs @@ -169,13 +169,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, self.opts.server.as_str(), self.opts.port, - sess.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - sess.packet_mark, + packet_mark, ) .map_err(|x| { io::Error::new( @@ -209,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, - sess.iface.as_ref(), + iface, #[cfg(any(target_os = "linux", target_os = "android"))] - sess.packet_mark, + packet_mark, ) .map_err(|x| { io::Error::new( From 19553762fb45322aab77213991ec5a73e3a27506 Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Thu, 28 Mar 2024 15:53:52 +0800 Subject: [PATCH 09/11] cargo fmt --- clash_lib/src/app/inbound/manager.rs | 1 - clash_lib/src/common/cmd.rs | 1 + clash_lib/src/proxy/converters/shadowsocks.rs | 2 +- clash_lib/src/proxy/tun/mod.rs | 2 +- clash_lib/src/proxy/utils/socket_helpers.rs | 5 ++--- 5 files changed, 5 insertions(+), 6 deletions(-) diff --git a/clash_lib/src/app/inbound/manager.rs b/clash_lib/src/app/inbound/manager.rs index 16fb39bb7..fbc7958d0 100644 --- a/clash_lib/src/app/inbound/manager.rs +++ b/clash_lib/src/app/inbound/manager.rs @@ -150,7 +150,6 @@ impl InboundManager { ); } - if let Some(tproxy_port) = ports.tproxy_port { network_listeners.insert( ListenerType::TProxy, diff --git a/clash_lib/src/common/cmd.rs b/clash_lib/src/common/cmd.rs index e69de29bb..8b1378917 100644 --- a/clash_lib/src/common/cmd.rs +++ b/clash_lib/src/common/cmd.rs @@ -0,0 +1 @@ + diff --git a/clash_lib/src/proxy/converters/shadowsocks.rs b/clash_lib/src/proxy/converters/shadowsocks.rs index bcbfc3f97..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 + AnyOutboundHandler, }, Error, }; diff --git a/clash_lib/src/proxy/tun/mod.rs b/clash_lib/src/proxy/tun/mod.rs index 10f2737f7..a84455efa 100644 --- a/clash_lib/src/proxy/tun/mod.rs +++ b/clash_lib/src/proxy/tun/mod.rs @@ -1,3 +1,3 @@ pub mod inbound; -pub use netstack_lwip as netstack; 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 f557555e7..362818228 100644 --- a/clash_lib/src/proxy/utils/socket_helpers.rs +++ b/clash_lib/src/proxy/utils/socket_helpers.rs @@ -153,14 +153,13 @@ pub async fn new_udp_socket_inner( 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 + // 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 + // 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)?; From c0d682ec04b3aae717ffebd5ebd989441de39ee5 Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Fri, 29 Mar 2024 22:36:52 +0800 Subject: [PATCH 10/11] run cargo clippy --- clash_lib/src/proxy/tproxy/iptables.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/clash_lib/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs index a3eff058f..89426f8f3 100644 --- a/clash_lib/src/proxy/tproxy/iptables.rs +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -53,6 +53,7 @@ impl Default for TProxyStrategy { } } } + #[derive(Debug)] pub struct TProxyGuard { strategy: TProxyStrategy, From d58d87478e7ea82c20f8594487f03638ef71174d Mon Sep 17 00:00:00 2001 From: VendettaReborn Date: Fri, 29 Mar 2024 22:58:42 +0800 Subject: [PATCH 11/11] fix error commented in PR --- clash_lib/src/app/inbound/manager.rs | 2 +- clash_lib/src/proxy/tproxy/iptables.rs | 25 +++++++++++++------------ clash_lib/src/proxy/vmess/mod.rs | 1 - clash_lib/src/session.rs | 5 ++--- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/clash_lib/src/app/inbound/manager.rs b/clash_lib/src/app/inbound/manager.rs index fbc7958d0..7428644f4 100644 --- a/clash_lib/src/app/inbound/manager.rs +++ b/clash_lib/src/app/inbound/manager.rs @@ -88,7 +88,7 @@ impl InboundManager { }; self.network_listeners .values() - .for_each(|x: &NetworkInboundListener| match x.listener_type { + .for_each(|x| match x.listener_type { ListenerType::Http => { ports.port = Some(x.port); } diff --git a/clash_lib/src/proxy/tproxy/iptables.rs b/clash_lib/src/proxy/tproxy/iptables.rs index 89426f8f3..265e0dd14 100644 --- a/clash_lib/src/proxy/tproxy/iptables.rs +++ b/clash_lib/src/proxy/tproxy/iptables.rs @@ -1,6 +1,6 @@ use cmd_lib::run_cmd; use std::process::Command; -use tracing::{debug, error}; +use tracing::{debug, error, warn}; // TODO: support nftables #[derive(Debug, Clone, Copy)] @@ -9,6 +9,18 @@ pub enum TProxyStrategy { 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 { @@ -43,17 +55,6 @@ impl TProxyStrategy { } } -impl Default for TProxyStrategy { - // auto detect command in system - fn default() -> Self { - if command_exists("iptables") { - TProxyStrategy::Iptables - } else { - TProxyStrategy::None - } - } -} - #[derive(Debug)] pub struct TProxyGuard { strategy: TProxyStrategy, diff --git a/clash_lib/src/proxy/vmess/mod.rs b/clash_lib/src/proxy/vmess/mod.rs index dab384642..7a12a833e 100644 --- a/clash_lib/src/proxy/vmess/mod.rs +++ b/clash_lib/src/proxy/vmess/mod.rs @@ -36,7 +36,6 @@ pub enum VmessTransport { pub struct HandlerOptions { pub name: String, - // TODO: @VendettaReborn, delete this after confirmed pub common_opts: CommonOption, pub server: String, pub port: u16, diff --git a/clash_lib/src/session.rs b/clash_lib/src/session.rs index 686b20b95..27e3d2cde 100644 --- a/clash_lib/src/session.rs +++ b/clash_lib/src/session.rs @@ -1,6 +1,5 @@ use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; -use std::sync::atomic::AtomicU32; use std::{ io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, @@ -14,10 +13,10 @@ use tokio::io::{AsyncRead, AsyncReadExt}; use erased_serde::Serialize as ESerialize; // mark of the packet from clash-rs -static DEFAULT_PACKET_MARK: AtomicU32 = AtomicU32::new(0xff); +static DEFAULT_PACKET_MARK: u32 = 0xff; pub(crate) fn get_packet_mark() -> u32 { - DEFAULT_PACKET_MARK.load(std::sync::atomic::Ordering::Relaxed) + DEFAULT_PACKET_MARK } #[derive(Debug, PartialEq, Eq, Serialize)]