diff --git a/Cargo.lock b/Cargo.lock index 75b7174d4..b2e7db852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,19 +4,13 @@ version = 3 [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375" dependencies = [ "gimli", ] -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "adler2" version = "2.0.0" @@ -468,17 +462,17 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", - "miniz_oxide 0.7.4", + "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -796,9 +790,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.16" +version = "1.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d013ecb737093c0e86b151a7b837993cf9ec6c502946cfb44bedc392421e0b" +checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476" dependencies = [ "shlex", ] @@ -1182,9 +1176,9 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -1780,11 +1774,11 @@ dependencies = [ [[package]] name = "enum-as-inner" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ffccbb6966c05b32ef8fbac435df276c4ae4d3dc55a8cd0eb9745e6c12f546a" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.77", @@ -1912,7 +1906,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" dependencies = [ "crc32fast", - "miniz_oxide 0.8.0", + "miniz_oxide", ] [[package]] @@ -2134,9 +2128,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64" [[package]] name = "glob" @@ -2305,7 +2299,7 @@ dependencies = [ "async-trait", "cfg-if", "data-encoding", - "enum-as-inner 0.6.0", + "enum-as-inner 0.6.1", "futures-channel", "futures-io", "futures-util", @@ -2332,7 +2326,7 @@ dependencies = [ "bytes", "cfg-if", "data-encoding", - "enum-as-inner 0.6.0", + "enum-as-inner 0.6.1", "futures-channel", "futures-io", "futures-util", @@ -2365,7 +2359,7 @@ dependencies = [ "async-trait", "bytes", "cfg-if", - "enum-as-inner 0.6.0", + "enum-as-inner 0.6.1", "futures-util", "hickory-proto 0.25.0-alpha.2", "hickory-resolver 0.25.0-alpha.2", @@ -2431,7 +2425,7 @@ dependencies = [ "async-trait", "bytes", "cfg-if", - "enum-as-inner 0.6.0", + "enum-as-inner 0.6.1", "futures-util", "h2", "hickory-proto 0.25.0-alpha.2", @@ -3160,15 +3154,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniz_oxide" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" -dependencies = [ - "adler", -] - [[package]] name = "miniz_oxide" version = "0.8.0" @@ -3586,9 +3571,9 @@ dependencies = [ [[package]] name = "parking" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" @@ -3753,9 +3738,9 @@ checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" [[package]] name = "plotters" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" dependencies = [ "num-traits", "plotters-backend", @@ -3766,15 +3751,15 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" [[package]] name = "plotters-svg" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" dependencies = [ "plotters-backend", ] @@ -4429,9 +4414,9 @@ checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" -version = "0.102.7" +version = "0.102.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" dependencies = [ "ring 0.17.8", "rustls-pki-types", @@ -4484,9 +4469,9 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.16" +version = "2.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb7ac86243095b70a7920639507b71d51a63390d1ba26c4f60a552fbb914a37" +checksum = "0c947adb109a8afce5fc9c7bf951f87f146e9147b3a6a58413105628fb1d1e66" dependencies = [ "sdd", ] @@ -4499,9 +4484,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "sdd" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0495e4577c672de8254beb68d01a9b62d0e8a13c099edecdbedccce3223cd29f" +checksum = "60a7b59a5d9b0099720b417b6325d91a52cbf5b3dcb5041d864be53eefa58abc" [[package]] name = "sec1" @@ -4752,8 +4737,7 @@ dependencies = [ [[package]] name = "shadowsocks" version = "1.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6a87d691a190af90706a2846b6d53ab16afbbb582eed8b9e6b9dca2d0a633a" +source = "git+https://github.com/Watfaq/shadowsocks-rust?rev=c6cb7fd906fe9f4126f724ae252f8a67cc1926b1#c6cb7fd906fe9f4126f724ae252f8a67cc1926b1" dependencies = [ "aes", "arc-swap", diff --git a/clash/Cargo.toml b/clash/Cargo.toml index 25fa7b1f9..b318dccda 100644 --- a/clash/Cargo.toml +++ b/clash/Cargo.toml @@ -4,7 +4,15 @@ repository = { workspace = true } version = { workspace = true } edition = { workspace = true } +[features] +default = [] +shadowsocks = ["clash_lib/shadowsocks"] +tuic = ["clash_lib/tuic"] +tracing = ["clash_lib/tracing"] +bench = ["clash_lib/bench"] +onion = ["clash_lib/onion"] + [dependencies] clap = { version = "4.5.17", features = ["derive"] } -clash_lib = { path = "../clash_lib", version = "*" } \ No newline at end of file +clash_lib = { path = "../clash_lib", version = "*", default-features = false } \ No newline at end of file diff --git a/clash/tests/data/config/rules.yaml b/clash/tests/data/config/rules.yaml index 6de6915a5..c1c58c93a 100644 --- a/clash/tests/data/config/rules.yaml +++ b/clash/tests/data/config/rules.yaml @@ -4,7 +4,7 @@ socks-port: 8889 mixed-port: 8899 tun: - enable: true + enable: false device-id: "dev://utun1989" route-all: false gateway: "198.19.0.1/32" @@ -87,7 +87,7 @@ proxy-groups: # - "h2-vmess" # - "tls-vmess" # - "grpc-vmess" - # - "ss-simple" + - "ss-simple" # - "trojan" # - "auto" # - "fallback-auto" @@ -283,6 +283,8 @@ rule-providers: behavior: domain rules: + - IP-CIDR,1.1.1.1/32,udp-relay + - IP-CIDR,8.8.8.8/32,udp-relay - DOMAIN,google.com,ws-vmess - DOMAIN-KEYWORD,httpbin,trojan-grpc - DOMAIN,ipinfo.io,DIRECT diff --git a/clash_lib/Cargo.toml b/clash_lib/Cargo.toml index dd6c881e8..2c8b6f339 100644 --- a/clash_lib/Cargo.toml +++ b/clash_lib/Cargo.toml @@ -9,7 +9,7 @@ default = [] shadowsocks = ["dep:shadowsocks"] tuic = ["dep:tuic", "dep:tuic-quinn", "dep:quinn", "dep:register-count"] tracing = [] -bench = ["criterion"] +bench = ["dep:criterion"] onion = ["arti-client/onion-service-client"] [dependencies] @@ -112,7 +112,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-oslog = { branch = "main", git = "https://github.com/Absolucy/tracing-oslog.git" } tracing-appender = "0.2" -shadowsocks = { version = "1", optional = true, features=["aead-cipher-2022","stream-cipher"] } +shadowsocks = { git = "https://github.com/Watfaq/shadowsocks-rust", rev = "c6cb7fd906fe9f4126f724ae252f8a67cc1926b1", optional = true, features=["aead-cipher-2022","stream-cipher"] } maxminddb = "0.24" public-suffix = "0.1" murmur3 = "0.5" diff --git a/clash_lib/src/proxy/datagram.rs b/clash_lib/src/proxy/datagram.rs index d28f7020f..f768ca683 100644 --- a/clash_lib/src/proxy/datagram.rs +++ b/clash_lib/src/proxy/datagram.rs @@ -1,5 +1,6 @@ use crate::{ app::dns::ThreadSafeDNSResolver, + common::errors::new_io_error, proxy::{socks::Socks5UDPCodec, AnyOutboundDatagram, InboundDatagram}, session::SocksAddr, }; @@ -242,10 +243,10 @@ impl Sink for OutboundDatagramImpl { let res = if wrote_all { Ok(()) } else { - Err(io::Error::new( - io::ErrorKind::Other, - "failed to write entire datagram", - )) + Err(new_io_error(format!( + "failed to send all data, only sent {} bytes", + n + ))) }; Poll::Ready(res) } else { diff --git a/clash_lib/src/proxy/mod.rs b/clash_lib/src/proxy/mod.rs index f0b52926f..bcaff489d 100644 --- a/clash_lib/src/proxy/mod.rs +++ b/clash_lib/src/proxy/mod.rs @@ -86,12 +86,12 @@ pub type AnyInboundDatagram = Box>; pub trait OutboundDatagram: - Stream + Sink + Send + Sync + Unpin + Stream + Sink + Send + Sync + Unpin + 'static { } impl OutboundDatagram for T where - T: Stream + Sink + Send + Sync + Unpin + T: Stream + Sink + Send + Sync + Unpin + 'static { } diff --git a/clash_lib/src/proxy/relay/mod.rs b/clash_lib/src/proxy/relay/mod.rs index d5da9e907..7ae59ae8a 100644 --- a/clash_lib/src/proxy/relay/mod.rs +++ b/clash_lib/src/proxy/relay/mod.rs @@ -158,6 +158,8 @@ impl OutboundHandler for Handler { connector = Box::new(ProxyConnector::new(proxy.clone(), connector)); } + + debug!("relay `{}` via proxy `{}`", self.name(), last[0].name()); let d = last[0] .connect_datagram_with_connector( sess, @@ -228,7 +230,7 @@ mod tests { #[tokio::test] #[serial_test::serial] - async fn test_relay_1_tcp() -> anyhow::Result<()> { + async fn test_relay_1() -> anyhow::Result<()> { let ss_opts = crate::proxy::shadowsocks::HandlerOptions { name: "test-ss".to_owned(), common_opts: Default::default(), @@ -259,14 +261,14 @@ mod tests { run_test_suites_and_cleanup( handler, get_ss_runner(port).await?, - Suite::tcp_tests(), + Suite::all(), ) .await } #[tokio::test] #[serial_test::serial] - async fn test_relay_2_tcp() -> anyhow::Result<()> { + async fn test_relay_2() -> anyhow::Result<()> { let ss_opts = crate::proxy::shadowsocks::HandlerOptions { name: "test-ss".to_owned(), common_opts: Default::default(), @@ -298,7 +300,7 @@ mod tests { run_test_suites_and_cleanup( handler, get_ss_runner(port).await?, - Suite::tcp_tests(), + Suite::all(), ) .await } diff --git a/clash_lib/src/proxy/shadowsocks/datagram.rs b/clash_lib/src/proxy/shadowsocks/datagram.rs index 0e0434788..b20e392bb 100644 --- a/clash_lib/src/proxy/shadowsocks/datagram.rs +++ b/clash_lib/src/proxy/shadowsocks/datagram.rs @@ -4,18 +4,19 @@ use std::{ task::{Context, Poll}, }; -use futures::{ready, Sink, Stream}; +use futures::{ready, Sink, SinkExt, Stream, StreamExt}; use shadowsocks::ProxySocket; use tokio::io::ReadBuf; use tracing::{debug, instrument, trace}; use crate::{ app::dns::ThreadSafeDNSResolver, + common::errors::new_io_error, proxy::{datagram::UdpPacket, AnyOutboundDatagram}, session::SocksAddr, }; -#[must_use = "sinks do nothing unless polled"] +/// the outbound datagram for that shadowsocks returns to us pub struct OutboundDatagramShadowsocks { inner: ProxySocket, remote_addr: SocksAddr, @@ -131,10 +132,10 @@ impl Sink for OutboundDatagramShadowsocks { let res = if wrote_all { Ok(()) } else { - Err(io::Error::new( - io::ErrorKind::Other, - "failed to write entire datagram", - )) + Err(new_io_error(format!( + "failed to write entire datagram, written: {}", + n + ))) }; Poll::Ready(res) } else { @@ -184,3 +185,78 @@ impl Stream for OutboundDatagramShadowsocks { } } } + +/// Shadowsocks UDP I/O that is passed to shadowsocks relay +pub(crate) struct ShadowsocksUdpIo { + inner: AnyOutboundDatagram, +} + +impl ShadowsocksUdpIo { + pub fn new(inner: AnyOutboundDatagram) -> Self { + Self { inner } + } +} + +impl Sink + for ShadowsocksUdpIo +{ + type Error = io::Error; + + fn poll_ready( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.inner.poll_ready_unpin(cx) + } + + fn start_send( + mut self: Pin<&mut Self>, + item: shadowsocks::relay::udprelay::proxy_socket::UdpPacket, + ) -> Result<(), Self::Error> { + self.inner.start_send_unpin(UdpPacket { + data: item.data.to_vec(), + src_addr: item.src.map(|x| x.into()).unwrap_or_default(), + dst_addr: item.dst.map(|x| x.into()).unwrap_or_default(), + }) + } + + fn poll_flush( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.inner.poll_flush_unpin(cx) + } + + fn poll_close( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.inner.poll_close_unpin(cx) + } +} + +impl Stream for ShadowsocksUdpIo { + type Item = shadowsocks::relay::udprelay::proxy_socket::UdpPacket; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match ready!(self.inner.poll_next_unpin(cx)) { + Some(pkt) => { + let (src, dst) = ( + pkt.src_addr.must_into_socket_addr(), + pkt.dst_addr.must_into_socket_addr(), + ); + Poll::Ready(Some( + shadowsocks::relay::udprelay::proxy_socket::UdpPacket { + data: pkt.data.into(), + src: src.into(), + dst: dst.into(), + }, + )) + } + None => Poll::Ready(None), + } + } +} diff --git a/clash_lib/src/proxy/shadowsocks/mod.rs b/clash_lib/src/proxy/shadowsocks/mod.rs index ad2698224..02819dbec 100644 --- a/clash_lib/src/proxy/shadowsocks/mod.rs +++ b/clash_lib/src/proxy/shadowsocks/mod.rs @@ -22,6 +22,7 @@ use crate::{ session::Session, }; use async_trait::async_trait; +use datagram::ShadowsocksUdpIo; use shadowsocks::{ config::ServerType, context::Context, crypto::CipherKind, relay::udprelay::proxy_socket::UdpSocketType, ProxyClientStream, ProxySocket, @@ -236,7 +237,7 @@ impl OutboundHandler for Handler { } async fn support_connector(&self) -> ConnectorType { - ConnectorType::Tcp + ConnectorType::All } async fn connect_stream_with_connector( @@ -261,6 +262,50 @@ impl OutboundHandler for Handler { chained.append_to_chain(self.name()).await; Ok(Box::new(chained)) } + + async fn connect_datagram_with_connector( + &self, + sess: &Session, + resolver: ThreadSafeDNSResolver, + connector: &dyn RemoteConnector, + ) -> io::Result { + let ctx = Context::new_shared(ServerType::Local); + let cfg = self.server_config()?; + + let socket = connector + .connect_datagram( + resolver.clone(), + None, + (self.opts.server.clone(), self.opts.port).try_into()?, + self.opts + .common_opts + .iface + .as_ref() + .or(sess.iface.as_ref()) + .cloned(), + #[cfg(any(target_os = "linux", target_os = "android"))] + None, + ) + .await?; + + let socket = ProxySocket::from_io( + UdpSocketType::Client, + ctx, + &cfg, + Box::new(ShadowsocksUdpIo::new(socket)), + None, + #[cfg(unix)] + None, + ); + let d = OutboundDatagramShadowsocks::new( + socket, + (self.opts.server.to_owned(), self.opts.port), + resolver, + ); + let d = ChainedDatagramWrapper::new(d); + d.append_to_chain(self.name()).await; + Ok(Box::new(d)) + } } #[cfg(all(test, not(ci)))] diff --git a/clash_lib/src/proxy/vmess/vmess_impl/datagram.rs b/clash_lib/src/proxy/vmess/vmess_impl/datagram.rs index d2c2671a6..fd5d69891 100644 --- a/clash_lib/src/proxy/vmess/vmess_impl/datagram.rs +++ b/clash_lib/src/proxy/vmess/vmess_impl/datagram.rs @@ -6,6 +6,7 @@ use tracing::{debug, error, instrument}; use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use crate::{ + common::errors::new_io_error, proxy::{datagram::UdpPacket, AnyStream}, session::SocksAddr, }; @@ -117,10 +118,10 @@ impl Sink for OutboundDatagramVmess { let res = if written.unwrap() == total_len { Ok(()) } else { - Err(io::Error::new( - io::ErrorKind::Other, - "failed to write entire datagram", - )) + Err(new_io_error(format!( + "failed to write entire datagram, written: {}", + written.unwrap() + ))) }; *written = None; Poll::Ready(res) diff --git a/clash_lib/src/proxy/wg/wireguard.rs b/clash_lib/src/proxy/wg/wireguard.rs index 45d166d7f..b028a71c8 100644 --- a/clash_lib/src/proxy/wg/wireguard.rs +++ b/clash_lib/src/proxy/wg/wireguard.rs @@ -101,7 +101,7 @@ impl WireguardTunnel { resolver, None, remote_endpoint.into(), - None, + None, // TODO: wg outbound interface https://github.com/Watfaq/clash-rs/issues/580 #[cfg(any(target_os = "linux", target_os = "android"))] None, ) diff --git a/clash_lib/src/session.rs b/clash_lib/src/session.rs index 88162d3f0..8b41fd83c 100644 --- a/clash_lib/src/session.rs +++ b/clash_lib/src/session.rs @@ -18,6 +18,12 @@ pub enum SocksAddr { Domain(String, u16), } +impl Default for SocksAddr { + fn default() -> Self { + Self::Ip(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), 0)) + } +} + impl Display for SocksAddr { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(