diff --git a/clash_lib/src/app/dns/dhcp.rs b/clash_lib/src/app/dns/dhcp.rs index 50d08fd5f..6817e19c9 100644 --- a/clash_lib/src/app/dns/dhcp.rs +++ b/clash_lib/src/app/dns/dhcp.rs @@ -177,7 +177,6 @@ async fn listen_dhcp_client(iface: &str) -> io::Result { new_udp_socket( Some(&listen_addr.parse().expect("must parse")), Some(&Interface::Name(iface.to_string())), - #[cfg(any(target_os = "linux", target_os = "android"))] None, ) .await diff --git a/clash_lib/src/common/http.rs b/clash_lib/src/common/http.rs index 3fab427a9..f0890d362 100644 --- a/clash_lib/src/common/http.rs +++ b/clash_lib/src/common/http.rs @@ -50,7 +50,6 @@ impl Service for LocalConnector { }, }), None, - #[cfg(any(target_os = "linux", target_os = "android"))] None, ) .await 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/lib.rs b/clash_lib/src/lib.rs index 793cbcaf3..367bb179c 100644 --- a/clash_lib/src/lib.rs +++ b/clash_lib/src/lib.rs @@ -33,11 +33,11 @@ mod common; mod config; mod proxy; mod session; - pub use config::def::Config as ClashConfigDef; pub use config::def::DNS as ClashDNSConfigDef; pub use config::DNSListen as ClashDNSListen; pub use config::RuntimeConfig as ClashRuntimeConfig; +pub use session::{get_iface, get_somark, set_iface, set_somark}; #[derive(Error, Debug)] pub enum Error { diff --git a/clash_lib/src/proxy/converters/shadowsocks.rs b/clash_lib/src/proxy/converters/shadowsocks.rs index 02adb2f9a..1e0afe6c7 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..2daa2a024 100644 --- a/clash_lib/src/proxy/direct/mod.rs +++ b/clash_lib/src/proxy/direct/mod.rs @@ -53,7 +53,6 @@ impl OutboundHandler for Handler { sess.destination.host().as_str(), sess.destination.port(), None, - #[cfg(any(target_os = "linux", target_os = "android"))] None, ) .await?; @@ -66,25 +65,20 @@ impl OutboundHandler for Handler { async fn proxy_stream( &self, s: AnyStream, - #[allow(unused_variables)] sess: &Session, - #[allow(unused_variables)] _resolver: ThreadSafeDNSResolver, + _sess: &Session, + _resolver: ThreadSafeDNSResolver, ) -> std::io::Result { Ok(s) } async fn connect_datagram( &self, - sess: &Session, + _sess: &Session, resolver: ThreadSafeDNSResolver, ) -> std::io::Result { - let d = new_udp_socket( - None, - sess.iface.as_ref(), - #[cfg(any(target_os = "linux", target_os = "android"))] - None, - ) - .await - .map(|x| OutboundDatagramImpl::new(x, resolver))?; + let d = new_udp_socket(None, None, None) + .await + .map(|x| OutboundDatagramImpl::new(x, resolver))?; let d = ChainedDatagramWrapper::new(d); d.append_to_chain(self.name()).await; diff --git a/clash_lib/src/proxy/http/inbound/connector.rs b/clash_lib/src/proxy/http/inbound/connector.rs index cf227f962..51d55a092 100644 --- a/clash_lib/src/proxy/http/inbound/connector.rs +++ b/clash_lib/src/proxy/http/inbound/connector.rs @@ -51,7 +51,6 @@ impl tower::Service for Connector { typ: Type::Http, source: src, destination: destination.ok_or(ProxyError::InvalidUrl(url.to_string()))?, - ..Default::default() }; tokio::spawn(async move { diff --git a/clash_lib/src/proxy/http/inbound/proxy.rs b/clash_lib/src/proxy/http/inbound/proxy.rs index 1438e2ed2..d90c87df9 100644 --- a/clash_lib/src/proxy/http/inbound/proxy.rs +++ b/clash_lib/src/proxy/http/inbound/proxy.rs @@ -65,8 +65,6 @@ async fn proxy( typ: Type::HttpConnect, source: src, destination: addr, - - ..Default::default() }; dispatcher.dispatch_stream(sess, upgraded).await diff --git a/clash_lib/src/proxy/mod.rs b/clash_lib/src/proxy/mod.rs index 96c76b826..7508c7cfd 100644 --- a/clash_lib/src/proxy/mod.rs +++ b/clash_lib/src/proxy/mod.rs @@ -85,10 +85,12 @@ 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 = "routing-mark")] so_mark: Option, + #[serde(rename = "interface-name")] iface: Option, } diff --git a/clash_lib/src/proxy/relay/mod.rs b/clash_lib/src/proxy/relay/mod.rs index 39bb69539..1eec2d829 100644 --- a/clash_lib/src/proxy/relay/mod.rs +++ b/clash_lib/src/proxy/relay/mod.rs @@ -92,7 +92,6 @@ impl OutboundHandler for Handler { remote_addr.host().as_str(), remote_addr.port(), None, - #[cfg(any(target_os = "linux", target_os = "android"))] None, ) .await?; diff --git a/clash_lib/src/proxy/shadowsocks/mod.rs b/clash_lib/src/proxy/shadowsocks/mod.rs index 86f14d083..6fa10c951 100644 --- a/clash_lib/src/proxy/shadowsocks/mod.rs +++ b/clash_lib/src/proxy/shadowsocks/mod.rs @@ -218,8 +218,7 @@ impl OutboundHandler for Handler { self.opts.server.as_str(), self.opts.port, self.opts.common_opts.iface.as_ref(), - #[cfg(any(target_os = "linux", target_os = "android"))] - None, + self.opts.common_opts.so_mark, ) .map_err(|x| { io::Error::new( @@ -311,8 +310,7 @@ impl OutboundHandler for Handler { let socket = new_udp_socket( None, self.opts.common_opts.iface.as_ref(), - #[cfg(any(target_os = "linux", target_os = "android"))] - None, + self.opts.common_opts.so_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..dbca52583 100644 --- a/clash_lib/src/proxy/socks/inbound/stream.rs +++ b/clash_lib/src/proxy/socks/inbound/stream.rs @@ -138,13 +138,7 @@ pub async fn handle_tcp<'a>( } socks_command::UDP_ASSOCIATE => { let udp_addr = SocketAddr::new(s.local_addr()?.ip(), 0); - let udp_inbound = new_udp_socket( - Some(&udp_addr), - None, - #[cfg(any(target_os = "linux", target_os = "android"))] - None, - ) - .await?; + let udp_inbound = new_udp_socket(Some(&udp_addr), None, None).await?; trace!( "Got a UDP_ASSOCIATE request from {}, UDP assigned at {}", @@ -166,8 +160,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/trojan/mod.rs b/clash_lib/src/proxy/trojan/mod.rs index 6a00e1727..96cc0c57d 100644 --- a/clash_lib/src/proxy/trojan/mod.rs +++ b/clash_lib/src/proxy/trojan/mod.rs @@ -156,8 +156,7 @@ impl OutboundHandler for Handler { self.opts.server.as_str(), self.opts.port, self.opts.common_opts.iface.as_ref(), - #[cfg(any(target_os = "linux", target_os = "android"))] - None, + self.opts.common_opts.so_mark, ) .map_err(|x| { io::Error::new( @@ -196,8 +195,7 @@ impl OutboundHandler for Handler { self.opts.server.as_str(), self.opts.port, self.opts.common_opts.iface.as_ref(), - #[cfg(any(target_os = "linux", target_os = "android"))] - None, + self.opts.common_opts.so_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..128eba5b7 100644 --- a/clash_lib/src/proxy/tun/inbound.rs +++ b/clash_lib/src/proxy/tun/inbound.rs @@ -26,7 +26,6 @@ async fn handle_inbound_stream( typ: Type::Tun, source: local_addr, destination: remote_addr.into(), - ..Default::default() }; dispatcher.dispatch_stream(sess, stream).await; diff --git a/clash_lib/src/proxy/utils/socket_helpers.rs b/clash_lib/src/proxy/utils/socket_helpers.rs index 1f163db5c..619c4300d 100644 --- a/clash_lib/src/proxy/utils/socket_helpers.rs +++ b/clash_lib/src/proxy/utils/socket_helpers.rs @@ -67,12 +67,12 @@ fn must_bind_socket_on_interface(socket: &socket2::Socket, iface: &Interface) -> } } -pub async fn new_tcp_stream<'a>( +pub async fn new_tcp_stream( resolver: ThreadSafeDNSResolver, - address: &'a str, + address: &str, port: u16, - iface: Option<&'a Interface>, - #[cfg(any(target_os = "linux", target_os = "android"))] packet_mark: Option, + iface: Option<&Interface>, + #[allow(unused_variables)] packet_mark: Option, ) -> io::Result { let dial_addr = resolver .resolve(address, false) @@ -99,12 +99,13 @@ pub async fn new_tcp_stream<'a>( } }; - if let Some(iface) = iface { + let global_iface = crate::get_iface(); + if let Some(iface) = iface.or(global_iface.as_ref()) { must_bind_socket_on_interface(&socket, iface)?; } - #[cfg(any(target_os = "linux", target_os = "android"))] - if let Some(packet_mark) = packet_mark { + #[cfg(target_os = "linux")] + if let Some(packet_mark) = packet_mark.or_else(crate::get_somark) { socket.set_mark(packet_mark)?; } @@ -124,7 +125,7 @@ pub async fn new_tcp_stream<'a>( pub async fn new_udp_socket( src: Option<&SocketAddr>, iface: Option<&Interface>, - #[cfg(any(target_os = "linux", target_os = "android"))] packet_mark: Option, + #[allow(unused_variables)] packet_mark: Option, ) -> io::Result { let socket = match src { Some(src) => { @@ -141,12 +142,13 @@ pub async fn new_udp_socket( socket.bind(&(*src).into())?; } - if let Some(iface) = iface { + let global_iface = crate::get_iface(); + if let Some(iface) = iface.or(global_iface.as_ref()) { must_bind_socket_on_interface(&socket, iface)?; } - #[cfg(any(target_os = "linux", target_os = "android"))] - if let Some(packet_mark) = packet_mark { + #[cfg(target_os = "linux")] + if let Some(packet_mark) = packet_mark.or_else(crate::get_somark) { socket.set_mark(packet_mark)?; } @@ -155,6 +157,28 @@ pub async fn new_udp_socket( UdpSocket::from_std(socket.into()) } +/// An extension to std::net::{UdpSocket, TcpStream} +pub trait StdSocketExt { + fn set_mark(&self, mark: u32) -> io::Result<()>; +} +impl StdSocketExt for std::net::UdpSocket { + fn set_mark(&self, mark: u32) -> io::Result<()> { + set_mark(socket2::SockRef::from(self), mark) + } +} +impl StdSocketExt for std::net::TcpStream { + fn set_mark(&self, mark: u32) -> io::Result<()> { + set_mark(socket2::SockRef::from(self), mark) + } +} + +#[allow(unused_variables)] +fn set_mark(socket: socket2::SockRef<'_>, mark: u32) -> io::Result<()> { + #[cfg(target_os = "linux")] + return socket.set_mark(mark); + #[cfg(not(target_os = "linux"))] + return Ok(()); +} #[cfg(test)] mod tests { diff --git a/clash_lib/src/proxy/vmess/mod.rs b/clash_lib/src/proxy/vmess/mod.rs index 178ddc1c7..e85b3672f 100644 --- a/clash_lib/src/proxy/vmess/mod.rs +++ b/clash_lib/src/proxy/vmess/mod.rs @@ -2,7 +2,6 @@ use std::{collections::HashMap, io, net::IpAddr, sync::Arc}; use async_trait::async_trait; use futures::TryFutureExt; -use tracing::debug; mod vmess_impl; @@ -169,14 +168,12 @@ impl OutboundHandler for Handler { sess: &Session, resolver: ThreadSafeDNSResolver, ) -> io::Result { - debug!("Connecting to {} via VMess", sess); let stream = new_tcp_stream( resolver, self.opts.server.as_str(), self.opts.port, self.opts.common_opts.iface.as_ref(), - #[cfg(any(target_os = "linux", target_os = "android"))] - None, + self.opts.common_opts.so_mark, ) .map_err(|x| { io::Error::new( @@ -215,8 +212,7 @@ impl OutboundHandler for Handler { self.opts.server.as_str(), self.opts.port, self.opts.common_opts.iface.as_ref(), - #[cfg(any(target_os = "linux", target_os = "android"))] - None, + self.opts.common_opts.so_mark, ) .map_err(|x| { io::Error::new( diff --git a/clash_lib/src/proxy/wg/wireguard.rs b/clash_lib/src/proxy/wg/wireguard.rs index 2a1b51cee..6bd331731 100644 --- a/clash_lib/src/proxy/wg/wireguard.rs +++ b/clash_lib/src/proxy/wg/wireguard.rs @@ -80,13 +80,7 @@ impl WireguardTunnel { let remote_endpoint = config.remote_endpoint; - let udp = new_udp_socket( - None, - None, - #[cfg(any(target_os = "linux", target_os = "android"))] - None, - ) - .await?; + let udp = new_udp_socket(None, None, None).await?; Ok(Self { source_peer_ip: config.source_peer_ip, diff --git a/clash_lib/src/session.rs b/clash_lib/src/session.rs index be5a17399..bb593033c 100644 --- a/clash_lib/src/session.rs +++ b/clash_lib/src/session.rs @@ -1,5 +1,8 @@ use std::collections::HashMap; use std::fmt::{Debug, Display, Formatter}; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::RwLock; use std::{ io, net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, @@ -375,10 +378,6 @@ pub struct Session { pub source: SocketAddr, /// The proxy target address of a proxy connection. pub destination: SocksAddr, - /// The packet mark SO_MARK - pub packet_mark: Option, - /// The bind interface - pub iface: Option, } impl Session { @@ -409,8 +408,6 @@ 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, - iface: None, } } } @@ -431,8 +428,6 @@ impl Debug for Session { .field("network", &self.network) .field("source", &self.source) .field("destination", &self.destination) - .field("packet_mark", &self.packet_mark) - .field("iface", &self.iface) .finish() } } @@ -444,8 +439,6 @@ impl Clone for Session { typ: self.typ, source: self.source, destination: self.destination.clone(), - packet_mark: self.packet_mark, - iface: self.iface.as_ref().cloned(), } } } @@ -461,3 +454,51 @@ fn invalid_atyp() -> io::Error { fn insuff_bytes() -> io::Error { io::Error::new(io::ErrorKind::Other, "insufficient bytes") } + +static GLOBAL_MARK: AtomicU32 = AtomicU32::new(0xff); +static ENABLE_MARK: AtomicBool = AtomicBool::new(false); +pub fn set_somark(mark: Option) { + match mark { + Some(mark) => { + GLOBAL_MARK.store(mark, Ordering::SeqCst); + ENABLE_MARK.store(true, Ordering::SeqCst); + } + None => ENABLE_MARK.store(false, Ordering::SeqCst), + } +} +/// get socket SO_MARK that outgoing socket should use +pub fn get_somark() -> Option { + if ENABLE_MARK.load(Ordering::Relaxed) { + Some(GLOBAL_MARK.load(Ordering::Relaxed)) + } else { + None + } +} + +static GLOBAL_IFACE: RwLock> = RwLock::new(None); +static ENABLE_IFACE: AtomicBool = AtomicBool::new(false); +pub fn set_iface(iface: Option) { + match iface { + Some(iface) => match GLOBAL_IFACE.write() { + Ok(mut guard) => { + *guard = Some(iface); + ENABLE_IFACE.store(true, Ordering::SeqCst); + } + // fail if the `RwLock` is poisoned. which only happens when writer holds lock and panics + Err(_) => ENABLE_IFACE.store(false, Ordering::SeqCst), + }, + None => ENABLE_IFACE.store(false, Ordering::SeqCst), + } +} +/// get iface that outgoing socket should use +pub fn get_iface() -> Option { + if ENABLE_IFACE.load(Ordering::Relaxed) { + match GLOBAL_IFACE.read() { + Ok(guard) => guard.deref().to_owned(), + // fail if the `RwLock` is poisoned. which only happens when writer holds lock and panics + _ => None, + } + } else { + None + } +}