From cababdcfdea7612ed892107690610f53d376431c Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 27 Apr 2026 21:49:16 +0800 Subject: [PATCH 01/22] remove --- Cargo.lock | 231 ++++++++++++++++-- clash-lib/Cargo.toml | 2 + clash-lib/src/proxy/tuic/mod.rs | 203 +++++++-------- clash-lib/src/proxy/tuic/test_utils.rs | 125 ++++++++++ .../utils/test_utils/docker_utils/consts.rs | 2 - 5 files changed, 449 insertions(+), 114 deletions(-) create mode 100644 clash-lib/src/proxy/tuic/test_utils.rs diff --git a/Cargo.lock b/Cargo.lock index a998e576b..bffd4eb69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,7 +248,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -259,7 +259,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -625,6 +625,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-core", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.5.0" @@ -1355,6 +1377,7 @@ dependencies = [ "md-5", "memory-stats", "mockall", + "moka", "murmur3", "network-interface", "opentelemetry", @@ -1413,6 +1436,7 @@ dependencies = [ "tracing-subscriber", "tracing-test", "tuic-core", + "tuic-server", "tun-rs", "unix-udp-sock", "url", @@ -2217,7 +2241,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "caef6056a5788d05d173cdc3c562ac28ae093828f851f69378b74e4e3d578e41" dependencies = [ "heck", - "indexmap 2.13.1", + "indexmap 1.9.3", "itertools 0.14.0", "proc-macro-crate", "proc-macro2", @@ -2373,7 +2397,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2774,7 +2798,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2876,12 +2900,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic 0.6.1", + "pear", "serde", + "serde_yaml", "toml 0.8.23", "uncased", "version_check", ] +[[package]] +name = "figment-json5" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6982da09e166efe7dc3c5cf1fe01ef85419733eb188c0df0b571eda9e8a81" +dependencies = [ + "figment", + "json5 0.4.1", + "serde", +] + [[package]] name = "filetime" version = "0.2.27" @@ -3856,7 +3893,9 @@ dependencies = [ "hyper", "hyper-util", "rustls", + "rustls-native-certs", "rustls-pki-types", + "rustls-platform-verifier", "tokio", "tokio-rustls", "tower-service", @@ -4090,6 +4129,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "inotify" version = "0.11.1" @@ -4130,6 +4175,32 @@ dependencies = [ "hybrid-array", ] +[[package]] +name = "instant-acme" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" +dependencies = [ + "async-trait", + "aws-lc-rs", + "base64 0.22.1", + "bytes", + "http", + "http-body", + "http-body-util", + "httpdate", + "hyper", + "hyper-rustls", + "hyper-util", + "rcgen", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", +] + [[package]] name = "internal-russh-forked-ssh-key" version = "0.6.18+upstream-0.6.7" @@ -4180,6 +4251,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + [[package]] name = "ip_network_table-deps-treebitmap" version = "0.5.0" @@ -4280,7 +4357,7 @@ dependencies = [ "libc", "socket2 0.6.3", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4409,6 +4486,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "json5" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c" +dependencies = [ + "serde", + "ucd-trie", +] + [[package]] name = "kameo" version = "0.19.2" @@ -4873,10 +4971,13 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ + "async-lock", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", "equivalent", + "event-listener", + "futures-util", "parking_lot", "portable-atomic", "smallvec", @@ -5216,7 +5317,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5852,6 +5953,39 @@ dependencies = [ "hmac 0.13.0", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -6364,6 +6498,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "version_check", + "yansi", +] + [[package]] name = "prost" version = "0.14.3" @@ -6469,7 +6616,7 @@ dependencies = [ "derive-deftly", "libc", "paste", - "thiserror 2.0.18", + "thiserror 1.0.69", ] [[package]] @@ -6754,6 +6901,8 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10b99e0098aa4082912d4c649628623db6aba77335e4f4569ff5083a6448b32e" dependencies = [ + "aws-lc-rs", + "pem", "ring", "rustls-pki-types", "time", @@ -7256,7 +7405,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7339,7 +7488,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8302,7 +8451,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8679,7 +8828,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8689,7 +8838,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" dependencies = [ "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10850,6 +10999,53 @@ dependencies = [ "uuid", ] +[[package]] +name = "tuic-server" +version = "1.6.7" +source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.6.7#4843d04b1584f49b503e344e2a96041e16fe8938" +dependencies = [ + "arc-swap", + "aws-lc-rs", + "axum", + "axum-extra", + "bytes", + "clap", + "derive_more", + "educe 0.6.0", + "eyre", + "figment", + "figment-json5", + "humantime", + "humantime-serde", + "instant-acme", + "ip_network", + "ipnet", + "json5 1.3.1", + "moka", + "pest", + "pest_derive", + "quinn", + "rand 0.9.2", + "rcgen", + "regex", + "register-count", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "sha2 0.10.9", + "socket2 0.6.3", + "thiserror 2.0.18", + "time", + "tokio", + "toml 0.9.12+spec-1.1.0", + "tracing", + "tracing-subscriber", + "tuic-core", + "uuid", + "x509-parser", +] + [[package]] name = "tun-rs" version = "2.8.1" @@ -11499,7 +11695,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12111,6 +12307,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ "asn1-rs", + "aws-lc-rs", "data-encoding", "der-parser", "lazy_static", @@ -12143,6 +12340,12 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "yasna" version = "0.5.2" diff --git a/clash-lib/Cargo.toml b/clash-lib/Cargo.toml index 69b018e7d..42d26ec7b 100644 --- a/clash-lib/Cargo.toml +++ b/clash-lib/Cargo.toml @@ -221,6 +221,8 @@ tracing-test = "0.2" http-body-util = "0.1" reqwest = { version = "0.13", features = ["socks"] } sysinfo = { version = "0.38", features = ["network"]} +moka = { version = "0.12", features = ["future"] } +tuic-server = { tag = "v1.6.7", git = "https://github.com/Itsusinn/tuic.git" } [build-dependencies] prost-build = "0.14" diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index 17b9cbb37..6d6ccc3b3 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -409,89 +409,28 @@ impl TuicDatagramOutbound { } } -#[cfg(all(test, docker_test))] +#[cfg(test)] +pub(crate) mod test_utils; + +#[cfg(test)] mod tests { - use std::io::Write; + use std::{sync::Arc, time::Duration}; - use super::super::utils::test_utils::{ - consts::*, docker_runner::DockerTestRunner, + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpListener, }; + + use super::{test_utils::TuicServerProcess, *}; use crate::{ - proxy::utils::{ - GLOBAL_DIRECT_CONNECTOR, - test_utils::{ - Suite, - config_helper::test_config_base_dir, - docker_runner::{DockerTestRunnerBuilder, alloc_docker_port}, - run_test_suites_and_cleanup, - }, - }, - tests::initialize, + proxy::utils::{GLOBAL_DIRECT_CONNECTOR, test_utils::noop::NoopResolver}, + session::Session, }; - use super::*; - - const TUIC_SERVER_CONFIG: &str = r#"server = "0.0.0.0:10002" - -data_dir = "" - -zero_rtt_handshake = false -dual_stack = false - -acl = ''' -direct 0.0.0.0/0 -direct ::/0 -''' - -[users] -00000000-0000-0000-0000-000000000001 = "passwd" - -[tls] -certificate = "/opt/tuic/fullchain.pem" -private_key = "/opt/tuic/privkey.pem" -alpn = ["h3"] - -[outbound.default] -type = "direct" -ip_mode = "auto" -"#; - - async fn get_tuic_runner(host_port: u16) -> anyhow::Result { - let test_config_dir = test_config_base_dir(); - let cert = test_config_dir.join("certs/example.org.pem"); - let key = test_config_dir.join("certs/example.org-key.pem"); - - let mut tmp = tempfile::NamedTempFile::new()?; - tmp.write_all(TUIC_SERVER_CONFIG.as_bytes())?; - - let result = DockerTestRunnerBuilder::new() - .image(IMAGE_TUIC) - .mounts(&[ - (tmp.path().to_str().unwrap(), "/etc/tuic/config.json"), - (cert.to_str().unwrap(), "/opt/tuic/fullchain.pem"), - (key.to_str().unwrap(), "/opt/tuic/privkey.pem"), - ]) - .env(&["TUIC_FORCE_TOML=1"]) - .host_port(host_port, 10002) - .build() - .await; - drop(tmp); - result - } - - fn gen_options( - container_ip: Option, - host_port: u16, - skip_cert_verify: bool, - ) -> anyhow::Result { - let port = if container_ip.is_some() { - 10002 - } else { - host_port - }; + fn gen_options(port: u16) -> anyhow::Result { Ok(HandlerOptions { name: "test-tuic".to_owned(), - server: container_ip.unwrap_or(LOCAL_ADDR.to_owned()), + server: "127.0.0.1".to_owned(), port, common_opts: Default::default(), uuid: "00000000-0000-0000-0000-000000000001".parse()?, @@ -506,9 +445,9 @@ ip_mode = "auto" congestion_controller: CongestionControl::Bbr, max_udp_relay_packet_size: 1500, max_open_stream: VarInt::from_u64(32)?, - ip: None, - skip_cert_verify, - sni: Some("example.org".to_owned()), + ip: Some("127.0.0.1".to_owned()), + skip_cert_verify: true, + sni: Some("localhost".to_owned()), gc_interval: Duration::from_millis(3000), gc_lifetime: Duration::from_millis(15000), send_window: 8 * 1024 * 1024 * 2, @@ -516,41 +455,109 @@ ip_mode = "auto" }) } + /// TCP ping-pong test: start an echo server, connect through tuic, send + /// "hello" and verify we receive "world" back. #[tokio::test] - async fn test_tuic_skip_cert_verify() -> anyhow::Result<()> { - initialize(); - let host_port = alloc_docker_port(); - - let container = get_tuic_runner(host_port).await?; - let opts = gen_options(container.container_ip(), host_port, true)?; + async fn test_tuic_ping_pong_tcp() -> anyhow::Result<()> { + crate::tests::initialize(); + let server = TuicServerProcess::start().await?; + let port = server.port(); + + // Start a local echo server as the target + let listener = TcpListener::bind("127.0.0.1:0").await?; + let target_port = listener.local_addr()?.port(); + + let target_handle = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let mut buf = vec![0u8; 5]; + for _ in 0..10 { + stream.read_exact(&mut buf).await.unwrap(); + assert_eq!(&buf, b"hello"); + stream.write_all(b"world").await.unwrap(); + stream.flush().await.unwrap(); + } + }); + let opts = gen_options(port)?; let handler = Arc::new(Handler::new(opts)); handler .register_connector(GLOBAL_DIRECT_CONNECTOR.clone()) .await; - run_test_suites_and_cleanup(handler, container, Suite::all()).await + + let resolver = Arc::new(NoopResolver); + + let session = Session { + network: crate::session::Network::Tcp, + typ: crate::session::Type::Socks5, + source: "127.0.0.1:54321".parse()?, + destination: format!("127.0.0.1:{target_port}").parse()?, + resolved_ip: None, + so_mark: None, + iface: None, + asn: None, + traffic_stats: None, + inbound_user: None, + }; + + let mut stream = handler.connect_stream(&session, resolver).await?; + + for _ in 0..10 { + stream.write_all(b"hello").await?; + stream.flush().await?; + let mut buf = vec![0u8; 5]; + stream.read_exact(&mut buf).await?; + assert_eq!(&buf, b"world"); + } + + target_handle.await?; + Ok(()) } + /// Verify that connecting with an invalid password fails. #[tokio::test] - async fn test_tuic_cert_verify_expect_fail() -> anyhow::Result<()> { - initialize(); - let host_port = alloc_docker_port(); + async fn test_tuic_auth_failure() -> anyhow::Result<()> { + crate::tests::initialize(); + let server = TuicServerProcess::start().await?; + let port = server.port(); - let container = get_tuic_runner(host_port).await?; - - let opts = gen_options(container.container_ip(), host_port, false)?; + let mut opts = gen_options(port)?; + opts.password = "wrong_password".into(); let handler = Arc::new(Handler::new(opts)); handler .register_connector(GLOBAL_DIRECT_CONNECTOR.clone()) .await; - let res = - run_test_suites_and_cleanup(handler, container, Suite::all()).await; - assert!(res.is_err()); - assert!(res.unwrap_err().to_string().contains( - "the cryptographic handshake failed: error 45: invalid peer \ - certificate: certificate expired" - )); + + let resolver = Arc::new(NoopResolver); + + let session = Session { + network: crate::session::Network::Tcp, + typ: crate::session::Type::Socks5, + source: "127.0.0.1:54321".parse()?, + destination: "127.0.0.1:9999".parse()?, + resolved_ip: None, + so_mark: None, + iface: None, + asn: None, + traffic_stats: None, + inbound_user: None, + }; + + let result = handler.connect_stream(&session, resolver).await; + // The stream connect may succeed initially (auth is async), but + // reading/writing should fail after the server rejects authentication. + if let Ok(mut stream) = result { + let mut buf = [0u8; 5]; + // Give the server time to process auth and close + tokio::time::sleep(Duration::from_secs(1)).await; + let write_result = stream.write_all(b"hello").await; + let read_result = stream.read_exact(&mut buf).await; + assert!( + write_result.is_err() || read_result.is_err(), + "expected IO error after auth failure, but both read and write \ + succeeded" + ); + } Ok(()) } } diff --git a/clash-lib/src/proxy/tuic/test_utils.rs b/clash-lib/src/proxy/tuic/test_utils.rs new file mode 100644 index 000000000..ae76c976c --- /dev/null +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -0,0 +1,125 @@ +use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; + +use tokio::sync::oneshot; + +/// A running tuic-server instance that cleans up on drop. +pub struct TuicServerProcess { + handle: Option>, + port: u16, +} + +impl TuicServerProcess { + /// Start a tuic-server instance on a random port. + pub async fn start() -> anyhow::Result { + let port = alloc_port(); + let server_addr: SocketAddr = format!("127.0.0.1:{port}").parse()?; + + let cfg = tuic_server::Config { + server: server_addr, + log_level: tuic_server::config::LogLevel::Info, + users: HashMap::from([( + "00000000-0000-0000-0000-000000000001".parse()?, + "passwd".into(), + )]), + tls: tuic_server::config::TlsConfig { + self_sign: true, + hostname: "localhost".into(), + alpn: vec!["h3".into()], + ..Default::default() + }, + zero_rtt_handshake: false, + dual_stack: false, + outbound: tuic_server::config::OutboundConfig { + default: tuic_server::config::OutboundRule { + kind: "direct".into(), + ..Default::default() + }, + named: HashMap::new(), + }, + acl: vec![], + udp_relay_ipv6: false, + experimental: tuic_server::config::ExperimentalConfig { + drop_loopback: false, + drop_private: false, + }, + ..Default::default() + }; + + let (ready_tx, ready_rx) = oneshot::channel(); + + let handle = tokio::spawn(async move { + let mut online_counter = HashMap::new(); + for (user, _) in cfg.users.iter() { + online_counter + .insert(user.to_owned(), std::sync::atomic::AtomicUsize::new(0)); + } + let mut traffic_stats = HashMap::new(); + for (user, _) in cfg.users.iter() { + traffic_stats.insert( + user.to_owned(), + ( + std::sync::atomic::AtomicUsize::new(0), + std::sync::atomic::AtomicUsize::new(0), + ), + ); + } + let capacity = cfg.users.len() as u64; + let ctx = Arc::new(tuic_server::AppContext { + cfg, + online_counter, + online_clients: moka::future::Cache::new(capacity), + traffic_stats, + }); + match tuic_server::server::Server::init(ctx).await { + Ok(server) => { + let _ = ready_tx.send(()); + server.start().await; + } + Err(e) => { + tracing::error!("tuic-server init failed: {e}"); + let _ = ready_tx.send(()); + } + } + }); + + // Wait for the server to be ready + tokio::time::timeout(Duration::from_secs(30), ready_rx) + .await + .map_err(|_| { + anyhow::anyhow!( + "tuic-server failed to start on port {port} within 30s" + ) + })? + .ok(); + + // Wait a brief moment for the socket to be fully bound + tokio::time::sleep(Duration::from_millis(100)).await; + + tracing::info!("tuic-server started on port {port}"); + + Ok(Self { + handle: Some(handle), + port, + }) + } + + pub fn port(&self) -> u16 { + self.port + } +} + +impl Drop for TuicServerProcess { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + handle.abort(); + tracing::info!("tuic-server task aborted"); + } + } +} + +/// Allocate a free UDP port (tuic works over QUIC/UDP). +fn alloc_port() -> u16 { + let socket = std::net::UdpSocket::bind("127.0.0.1:0") + .expect("failed to allocate a free port"); + socket.local_addr().unwrap().port() +} diff --git a/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs b/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs index e0aa90cf0..3a9796b77 100644 --- a/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs +++ b/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs @@ -17,8 +17,6 @@ pub const IMAGE_SOCKS5: &str = "v2fly/v2fly-core:v4.45.2"; #[cfg(all(feature = "ssh", docker_test))] pub const IMAGE_OPENSSH: &str = "docker.io/linuxserver/openssh-server:latest"; pub const IMAGE_HYSTERIA: &str = "tobyxdd/hysteria:latest"; -#[cfg(feature = "tuic")] -pub const IMAGE_TUIC: &str = "ghcr.io/itsusinn/tuic-server:latest"; #[cfg(feature = "shadowquic")] pub const IMAGE_SHADOWQUIC: &str = "ghcr.io/spongebob888/shadowquic:latest"; pub const IMAGE_SINGBOX: &str = "ghcr.io/sagernet/sing-box:v1.13.8"; From 0d8ecde1996188b341be618908581f0082e2755b Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 27 Apr 2026 22:03:15 +0800 Subject: [PATCH 02/22] ci --- .github/workflows/coverage.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c3209c9a4..1c54bb6c7 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -33,14 +33,9 @@ jobs: ~/.cargo/registry/cache/ ~/.cargo/git/db/ key: coverage-${{ hashFiles('**/Cargo.toml') }}-${{ matrix.os }} - - uses: ilammy/setup-nasm@v1 + - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - - name: Install Protoc - uses: arduino/setup-protoc@v3 - with: - version: "23.x" - repo-token: ${{ secrets.GITHUB_TOKEN }} - name: Cargo test and coverage uses: clechasseur/rs-cargo@v4 From daf0ae2229ff5fe3d4fcdfb687a2e7ab4bdf662f Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 27 Apr 2026 22:19:34 +0800 Subject: [PATCH 03/22] fix time race --- clash-lib/src/proxy/tuic/mod.rs | 17 ++++- clash-lib/src/proxy/tuic/test_utils.rs | 86 ++++++++++++++------------ 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index 6d6ccc3b3..242632a0e 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -520,6 +520,21 @@ mod tests { let server = TuicServerProcess::start().await?; let port = server.port(); + // Start a local TCP target so failure cannot be blamed on an + // unreachable upstream. + let listener = TcpListener::bind("127.0.0.1:0").await?; + let target_port = listener.local_addr()?.port(); + let _target_handle = tokio::spawn(async move { + // Accept one connection, then idle — the test should never + // actually reach this point because auth fails first. + if let Ok((mut stream, _)) = listener.accept().await { + let mut buf = [0u8; 5]; + while stream.read_exact(&mut buf).await.is_ok() { + stream.write_all(b"world").await.ok(); + } + } + }); + let mut opts = gen_options(port)?; opts.password = "wrong_password".into(); @@ -534,7 +549,7 @@ mod tests { network: crate::session::Network::Tcp, typ: crate::session::Type::Socks5, source: "127.0.0.1:54321".parse()?, - destination: "127.0.0.1:9999".parse()?, + destination: format!("127.0.0.1:{target_port}").parse()?, resolved_ip: None, so_mark: None, iface: None, diff --git a/clash-lib/src/proxy/tuic/test_utils.rs b/clash-lib/src/proxy/tuic/test_utils.rs index ae76c976c..b94706208 100644 --- a/clash-lib/src/proxy/tuic/test_utils.rs +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -11,43 +11,51 @@ pub struct TuicServerProcess { impl TuicServerProcess { /// Start a tuic-server instance on a random port. pub async fn start() -> anyhow::Result { - let port = alloc_port(); - let server_addr: SocketAddr = format!("127.0.0.1:{port}").parse()?; + // We use a channel to receive the actual bound port from the task. + let (port_tx, port_rx) = oneshot::channel(); + let (ready_tx, ready_rx) = oneshot::channel(); - let cfg = tuic_server::Config { - server: server_addr, - log_level: tuic_server::config::LogLevel::Info, - users: HashMap::from([( - "00000000-0000-0000-0000-000000000001".parse()?, - "passwd".into(), - )]), - tls: tuic_server::config::TlsConfig { - self_sign: true, - hostname: "localhost".into(), - alpn: vec!["h3".into()], - ..Default::default() - }, - zero_rtt_handshake: false, - dual_stack: false, - outbound: tuic_server::config::OutboundConfig { - default: tuic_server::config::OutboundRule { - kind: "direct".into(), + let handle = tokio::spawn(async move { + let sock = std::net::UdpSocket::bind("127.0.0.1:0") + .expect("failed to allocate a free port"); + let port = sock.local_addr().unwrap().port(); + let server_addr: SocketAddr = + format!("127.0.0.1:{port}").parse().unwrap(); + drop(sock); // socket released; tuic-server's init will re-bind + + let _ = port_tx.send(port); + + let cfg = tuic_server::Config { + server: server_addr, + log_level: tuic_server::config::LogLevel::Info, + users: HashMap::from([( + "00000000-0000-0000-0000-000000000001".parse().unwrap(), + "passwd".into(), + )]), + tls: tuic_server::config::TlsConfig { + self_sign: true, + hostname: "localhost".into(), + alpn: vec!["h3".into()], ..Default::default() }, - named: HashMap::new(), - }, - acl: vec![], - udp_relay_ipv6: false, - experimental: tuic_server::config::ExperimentalConfig { - drop_loopback: false, - drop_private: false, - }, - ..Default::default() - }; - - let (ready_tx, ready_rx) = oneshot::channel(); + zero_rtt_handshake: false, + dual_stack: false, + outbound: tuic_server::config::OutboundConfig { + default: tuic_server::config::OutboundRule { + kind: "direct".into(), + ..Default::default() + }, + named: HashMap::new(), + }, + acl: vec![], + udp_relay_ipv6: false, + experimental: tuic_server::config::ExperimentalConfig { + drop_loopback: false, + drop_private: false, + }, + ..Default::default() + }; - let handle = tokio::spawn(async move { let mut online_counter = HashMap::new(); for (user, _) in cfg.users.iter() { online_counter @@ -82,6 +90,11 @@ impl TuicServerProcess { } }); + let port = tokio::time::timeout(Duration::from_secs(5), port_rx) + .await + .map_err(|_| anyhow::anyhow!("tuic-server failed to report a port"))? + .map_err(|_| anyhow::anyhow!("tuic-server task panicked before reporting port"))?; + // Wait for the server to be ready tokio::time::timeout(Duration::from_secs(30), ready_rx) .await @@ -116,10 +129,3 @@ impl Drop for TuicServerProcess { } } } - -/// Allocate a free UDP port (tuic works over QUIC/UDP). -fn alloc_port() -> u16 { - let socket = std::net::UdpSocket::bind("127.0.0.1:0") - .expect("failed to allocate a free port"); - socket.local_addr().unwrap().port() -} From 6898057fb8b201171efdf971a121e0b3f489a5a0 Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 27 Apr 2026 22:23:31 +0800 Subject: [PATCH 04/22] applied --- clash-lib/src/proxy/tuic/test_utils.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/clash-lib/src/proxy/tuic/test_utils.rs b/clash-lib/src/proxy/tuic/test_utils.rs index b94706208..3b8c1678a 100644 --- a/clash-lib/src/proxy/tuic/test_utils.rs +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -13,7 +13,8 @@ impl TuicServerProcess { pub async fn start() -> anyhow::Result { // We use a channel to receive the actual bound port from the task. let (port_tx, port_rx) = oneshot::channel(); - let (ready_tx, ready_rx) = oneshot::channel(); + + let (ready_tx, ready_rx) = oneshot::channel::>(); let handle = tokio::spawn(async move { let sock = std::net::UdpSocket::bind("127.0.0.1:0") @@ -80,22 +81,24 @@ impl TuicServerProcess { }); match tuic_server::server::Server::init(ctx).await { Ok(server) => { - let _ = ready_tx.send(()); + let _ = ready_tx.send(Ok(())); server.start().await; } Err(e) => { tracing::error!("tuic-server init failed: {e}"); - let _ = ready_tx.send(()); + let _ = ready_tx.send(Err(anyhow::anyhow!("{e}"))); } } }); + // Wait for the server to be ready (or for init to fail). let port = tokio::time::timeout(Duration::from_secs(5), port_rx) .await .map_err(|_| anyhow::anyhow!("tuic-server failed to report a port"))? - .map_err(|_| anyhow::anyhow!("tuic-server task panicked before reporting port"))?; + .map_err(|_| { + anyhow::anyhow!("tuic-server task panicked before reporting port") + })?; - // Wait for the server to be ready tokio::time::timeout(Duration::from_secs(30), ready_rx) .await .map_err(|_| { @@ -103,10 +106,7 @@ impl TuicServerProcess { "tuic-server failed to start on port {port} within 30s" ) })? - .ok(); - - // Wait a brief moment for the socket to be fully bound - tokio::time::sleep(Duration::from_millis(100)).await; + .map_err(|e| anyhow::anyhow!("tuic-server init failed: {e}"))??; tracing::info!("tuic-server started on port {port}"); From 0ffaff50e4cac22c2ee24d4fd29bd4ee3e578019 Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 27 Apr 2026 22:35:14 +0800 Subject: [PATCH 05/22] update tuic --- Cargo.lock | 511 +++++++++++++++++++++++++++++++++++++------ clash-lib/Cargo.toml | 4 +- 2 files changed, 445 insertions(+), 70 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bffd4eb69..603c026a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -364,13 +364,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "asn1-rs" version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" dependencies = [ - "asn1-rs-derive", + "asn1-rs-derive 0.6.0", "asn1-rs-impl", "displaydoc", "nom 7.1.3", @@ -380,6 +396,18 @@ dependencies = [ "time", ] +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "asn1-rs-derive" version = "0.6.0" @@ -443,6 +471,37 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-http-codec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "096146020b08dbc4587685b0730a7ba905625af13c65f8028035cdfd69573c91" +dependencies = [ + "anyhow", + "futures", + "http", + "httparse", + "log", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-lock" version = "3.4.2" @@ -454,6 +513,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-object-pool" version = "0.2.0" @@ -492,6 +562,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-web-client" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37381fb4fad3cd9b579628c21a58f528ef029d1f072d10f16cb9431aa2236d29" +dependencies = [ + "async-http-codec", + "async-net", + "futures", + "futures-rustls", + "http", + "lazy_static", + "log", + "rustls-pki-types", + "thiserror 1.0.69", + "webpki-roots 0.26.11", +] + [[package]] name = "async_executors" version = "0.7.0" @@ -658,6 +746,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "tokio", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -1389,8 +1494,8 @@ dependencies = [ "prost-build", "protox", "public-suffix", - "quinn", - "quinn-proto", + "quinn 0.11.9", + "quinn-proto 0.11.14", "rand 0.9.2", "rand_chacha 0.10.0", "regex", @@ -1444,7 +1549,7 @@ dependencies = [ "watfaq-dns", "watfaq-netstack", "watfaq-rustls", - "webpki-roots", + "webpki-roots 1.0.6", "windows 0.62.2", "zip", "zstd", @@ -2199,13 +2304,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der-parser" version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "cookie-factory", "displaydoc", "nom 7.1.3", @@ -2862,6 +2981,19 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastbloom" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" +dependencies = [ + "foldhash 0.2.0", + "libm", + "portable-atomic", + "rand 0.9.2", + "siphasher", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -3007,6 +3139,16 @@ dependencies = [ "futures-core", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs-mistrust" version = "0.13.2" @@ -3107,7 +3249,10 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -3380,7 +3525,7 @@ dependencies = [ "bytes", "futures", "h3 0.0.7", - "quinn", + "quinn 0.11.9", "tokio", "tokio-util", ] @@ -3394,7 +3539,7 @@ dependencies = [ "bytes", "futures", "h3 0.0.8", - "quinn", + "quinn 0.11.9", "tokio", "tokio-util", ] @@ -3523,6 +3668,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -3578,7 +3729,7 @@ dependencies = [ "ipnet", "once_cell", "pin-project-lite", - "quinn", + "quinn 0.11.9", "rand 0.9.2", "ring", "rustls", @@ -3606,7 +3757,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "quinn", + "quinn 0.11.9", "rand 0.9.2", "resolv-conf", "rustls", @@ -3893,13 +4044,11 @@ dependencies = [ "hyper", "hyper-util", "rustls", - "rustls-native-certs", "rustls-pki-types", - "rustls-platform-verifier", "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -4175,32 +4324,6 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "instant-acme" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f05ad37c421b962354c358d347d4a6130151df9407978372d3ad7f0c8f71a64" -dependencies = [ - "async-trait", - "aws-lc-rs", - "base64 0.22.1", - "bytes", - "http", - "http-body", - "http-body-util", - "httpdate", - "hyper", - "hyper-rustls", - "hyper-util", - "rcgen", - "rustls", - "rustls-pki-types", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", -] - [[package]] name = "internal-russh-forked-ssh-key" version = "0.6.18+upstream-0.6.7" @@ -4436,6 +4559,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -5592,13 +5745,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + [[package]] name = "oid-registry" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", ] [[package]] @@ -6244,6 +6406,20 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -6619,6 +6795,18 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "qlog" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13932f4f3e6b912a43b3af40a3174614b8408c85059b79ef23fe1b3c87e5d04a" +dependencies = [ + "humantime", + "serde", + "serde_json", + "serde_with", +] + [[package]] name = "quinn" version = "0.11.9" @@ -6629,8 +6817,28 @@ dependencies = [ "cfg_aliases", "futures-io", "pin-project-lite", - "quinn-proto", - "quinn-udp", + "quinn-proto 0.11.14", + "quinn-udp 0.5.14", + "rustc-hash 2.1.2", + "rustls", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn" +version = "0.12.0" +source = "git+https://github.com/Tipuch/quinn.git?branch=bbrv3#ce60e5b5c115db2a6053f4e0ca7fc52103cb76b9" +dependencies = [ + "bytes", + "cfg_aliases", + "futures-io", + "pin-project-lite", + "quinn-proto 0.12.0", + "quinn-udp 0.6.1", "rustc-hash 2.1.2", "rustls", "socket2 0.6.3", @@ -6640,6 +6848,16 @@ dependencies = [ "web-time", ] +[[package]] +name = "quinn-congestions" +version = "0.1.0" +source = "git+https://github.com/proxy-rs/quinn-congestions.git#2bdc356f57a7fc3ae1a49ab963a221f42d61012f" +dependencies = [ + "quinn 0.12.0", + "quinn-proto 0.12.0", + "rand 0.9.2", +] + [[package]] name = "quinn-jls" version = "0.3.3" @@ -6682,6 +6900,31 @@ dependencies = [ "web-time", ] +[[package]] +name = "quinn-proto" +version = "0.12.0" +source = "git+https://github.com/Tipuch/quinn.git?branch=bbrv3#ce60e5b5c115db2a6053f4e0ca7fc52103cb76b9" +dependencies = [ + "aws-lc-rs", + "bytes", + "fastbloom", + "getrandom 0.3.4", + "lru-slab", + "qlog", + "rand 0.9.2", + "rand_pcg", + "ring", + "rustc-hash 2.1.2", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier 0.7.0", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + [[package]] name = "quinn-proto-jls" version = "0.3.3" @@ -6717,6 +6960,18 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "quinn-udp" +version = "0.6.1" +source = "git+https://github.com/Tipuch/quinn.git?branch=bbrv3#ce60e5b5c115db2a6053f4e0ca7fc52103cb76b9" +dependencies = [ + "cfg_aliases", + "libc", + "socket2 0.6.3", + "tracing", + "windows-sys 0.52.0", +] + [[package]] name = "quinn-udp-jls" version = "0.3.3" @@ -6875,6 +7130,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "rand_pcg" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b48ac3f7ffaab7fac4d2376632268aa5f89abdb55f7ebf8f4d11fffccb2320f7" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rayon" version = "1.11.0" @@ -6895,6 +7159,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "yasna", +] + [[package]] name = "rcgen" version = "0.14.7" @@ -6906,7 +7183,7 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "x509-parser", + "x509-parser 0.18.1", "yasna", ] @@ -7030,7 +7307,7 @@ dependencies = [ "log", "percent-encoding", "pin-project-lite", - "quinn", + "quinn 0.11.9", "rustls", "rustls-pki-types", "serde", @@ -7046,7 +7323,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -7071,10 +7348,10 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "quinn", + "quinn 0.11.9", "rustls", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.6.2", "sync_wrapper", "tokio", "tokio-rustls", @@ -7424,6 +7701,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-acme" +version = "0.15.1" +source = "git+https://github.com/rust-proxy/rustls-acme?branch=feat%2Fip#27c0b4ebfe1e0ec4711e8e63eface99b8b6bf2c8" +dependencies = [ + "async-io", + "async-trait", + "async-web-client", + "aws-lc-rs", + "base64 0.22.1", + "blocking", + "chrono", + "futures", + "futures-rustls", + "http", + "log", + "pem", + "rcgen 0.13.2", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower-service", + "webpki-roots 1.0.6", + "x509-parser 0.16.0", +] + [[package]] name = "rustls-jls" version = "1.3.1" @@ -7478,7 +7783,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", - "jni", + "jni 0.21.1", "log", "once_cell", "rustls", @@ -7491,6 +7796,27 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni 0.22.4", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.10", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.52.0", +] + [[package]] name = "rustls-platform-verifier-android" version = "0.1.1" @@ -8160,7 +8486,7 @@ dependencies = [ "notify", "quinn-jls", "quinn-proto-jls", - "rcgen", + "rcgen 0.14.7", "ring", "rustls", "rustls-jls", @@ -8175,7 +8501,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "webpki-roots", + "webpki-roots 1.0.6", "yaml-rust2", ] @@ -8319,6 +8645,22 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -9163,10 +9505,13 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" dependencies = [ + "indexmap 2.13.1", "serde_core", "serde_spanned 1.1.1", "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", "toml_writer", + "winnow 1.0.1", ] [[package]] @@ -9891,7 +10236,7 @@ dependencies = [ "base64ct", "ctr 0.9.2", "curve25519-dalek 4.1.3", - "der-parser", + "der-parser 10.0.0", "derive-deftly", "derive_more", "digest 0.10.7", @@ -10910,7 +11255,7 @@ dependencies = [ "tokio", "tokio-rustls", "url", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -10983,14 +11328,14 @@ dependencies = [ [[package]] name = "tuic-core" -version = "1.6.7" -source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.6.7#4843d04b1584f49b503e344e2a96041e16fe8938" +version = "1.7.2" +source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.7.2#18b74bcf11fe33caf9dcfc9e2d6685c5230a2e0a" dependencies = [ "bytes", "eyre", "futures-util", "parking_lot", - "quinn", + "quinn 0.12.0", "register-count", "serde", "thiserror 2.0.18", @@ -11001,13 +11346,14 @@ dependencies = [ [[package]] name = "tuic-server" -version = "1.6.7" -source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.6.7#4843d04b1584f49b503e344e2a96041e16fe8938" +version = "1.7.2" +source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.7.2#18b74bcf11fe33caf9dcfc9e2d6685c5230a2e0a" dependencies = [ "arc-swap", "aws-lc-rs", "axum", "axum-extra", + "axum-server", "bytes", "clap", "derive_more", @@ -11017,33 +11363,36 @@ dependencies = [ "figment-json5", "humantime", "humantime-serde", - "instant-acme", "ip_network", "ipnet", "json5 1.3.1", "moka", "pest", "pest_derive", - "quinn", - "rand 0.9.2", - "rcgen", + "quinn 0.12.0", + "quinn-congestions", + "rand 0.10.0", + "rcgen 0.14.7", "regex", "register-count", "rustls", + "rustls-acme", "rustls-pemfile", "serde", "serde_json", - "sha2 0.10.9", + "sha2 0.11.0", "socket2 0.6.3", "thiserror 2.0.18", "time", "tokio", - "toml 0.9.12+spec-1.1.0", + "tokio-stream", + "tokio-util", + "toml 1.1.2+spec-1.1.0", "tracing", "tracing-subscriber", "tuic-core", "uuid", - "x509-parser", + "x509-parser 0.18.1", ] [[package]] @@ -11292,7 +11641,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -11582,7 +11931,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", - "webpki-roots", + "webpki-roots 1.0.6", ] [[package]] @@ -11658,6 +12007,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.6", +] + [[package]] name = "webpki-roots" version = "1.0.6" @@ -12300,19 +12658,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom 7.1.3", + "oid-registry 0.7.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "x509-parser" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.1", "aws-lc-rs", "data-encoding", - "der-parser", + "der-parser 10.0.0", "lazy_static", "nom 7.1.3", - "oid-registry", + "oid-registry 0.8.1", "ring", "rusticata-macros", "thiserror 2.0.18", diff --git a/clash-lib/Cargo.toml b/clash-lib/Cargo.toml index 42d26ec7b..a88390c4e 100644 --- a/clash-lib/Cargo.toml +++ b/clash-lib/Cargo.toml @@ -173,7 +173,7 @@ arti-client = { version = "0.39", optional = true, default-features = false, fea tor-rtcompat = { version = "0.39", optional = true, default-features = false } # tuic -tuic-core= { tag = "v1.6.7", optional = true, git = "https://github.com/Itsusinn/tuic.git", features = ["async_marshal", "marshal", "model"] } +tuic-core= { tag = "v1.7.2", optional = true, git = "https://github.com/Itsusinn/tuic.git", features = ["async_marshal", "marshal", "model"] } register-count = { version = "0.1", optional = true } quinn = { version = "0.11", default-features = false, features = ["futures-io", "runtime-tokio", "rustls"] } @@ -222,7 +222,7 @@ http-body-util = "0.1" reqwest = { version = "0.13", features = ["socks"] } sysinfo = { version = "0.38", features = ["network"]} moka = { version = "0.12", features = ["future"] } -tuic-server = { tag = "v1.6.7", git = "https://github.com/Itsusinn/tuic.git" } +tuic-server = { tag = "v1.7.2", git = "https://github.com/Itsusinn/tuic.git" } [build-dependencies] prost-build = "0.14" From 5e610a3772789d92a511dbcee5e9f999ddbdec61 Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 25 May 2026 08:24:09 +0800 Subject: [PATCH 06/22] update tuic --- Cargo.lock | 542 +++++++++++++++++++++++++++++++++++++------ clash-lib/Cargo.toml | 2 +- 2 files changed, 475 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1181ffe5a..fc5ced881 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -366,13 +366,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "asn1-rs" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ - "asn1-rs-derive", + "asn1-rs-derive 0.6.0", "asn1-rs-impl", "displaydoc", "nom 7.1.3", @@ -382,6 +398,18 @@ dependencies = [ "time", ] +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "asn1-rs-derive" version = "0.6.0" @@ -445,6 +473,37 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-http-codec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "096146020b08dbc4587685b0730a7ba905625af13c65f8028035cdfd69573c91" +dependencies = [ + "anyhow", + "futures", + "http", + "httparse", + "log", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-lock" version = "3.4.2" @@ -456,6 +515,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-object-pool" version = "0.2.0" @@ -494,6 +564,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-web-client" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37381fb4fad3cd9b579628c21a58f528ef029d1f072d10f16cb9431aa2236d29" +dependencies = [ + "async-http-codec", + "async-net", + "futures", + "futures-rustls", + "http", + "lazy_static", + "log", + "rustls-pki-types", + "thiserror 1.0.69", + "webpki-roots 0.26.11", +] + [[package]] name = "async_executors" version = "0.7.0" @@ -627,6 +715,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-core", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.5.1" @@ -638,6 +748,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "tokio", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -958,15 +1085,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - [[package]] name = "bstr" version = "1.12.1" @@ -1366,6 +1484,7 @@ dependencies = [ "md-5", "memory-stats", "mockall", + "moka", "murmur3", "network-interface", "opentelemetry", @@ -1381,7 +1500,7 @@ dependencies = [ "quinn-proto 0.11.14", "rand 0.10.1", "rand_chacha 0.10.0", - "rcgen", + "rcgen 0.14.8", "regex", "register-count", "reqwest 0.13.3", @@ -1426,13 +1545,14 @@ dependencies = [ "tracing-subscriber", "tracing-test", "tuic-core", + "tuic-server", "tun-rs", "unix-udp-sock", "url", "uuid", "watfaq-dns", "watfaq-netstack", - "webpki-roots", + "webpki-roots 1.0.7", "windows 0.62.2", "zip", "zstd", @@ -1998,16 +2118,6 @@ dependencies = [ "darling_macro 0.21.3", ] -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - [[package]] name = "darling_core" version = "0.14.4" @@ -2043,18 +2153,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", - "ident_case", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ "ident_case", "proc-macro2", "quote", @@ -2095,17 +2193,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.117", -] - [[package]] name = "dashmap" version = "6.2.1" @@ -2210,13 +2297,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der-parser" version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.2", "cookie-factory", "displaydoc", "nom 7.1.3", @@ -2926,12 +3027,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic 0.6.1", + "pear", "serde", + "serde_yaml", "toml 0.8.23", "uncased", "version_check", ] +[[package]] +name = "figment-json5" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6982da09e166efe7dc3c5cf1fe01ef85419733eb188c0df0b571eda9e8a81" +dependencies = [ + "figment", + "json5 0.4.1", + "serde", +] + [[package]] name = "filetime" version = "0.2.29" @@ -3019,6 +3133,16 @@ dependencies = [ "futures-core", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs-mistrust" version = "0.14.1" @@ -3119,7 +3243,10 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -3515,6 +3642,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -3893,7 +4026,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -3961,7 +4094,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -4133,6 +4266,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "inotify" version = "0.11.1" @@ -4223,6 +4362,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + [[package]] name = "ip_network_table-deps-treebitmap" version = "0.5.0" @@ -4448,6 +4593,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "json5" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c" +dependencies = [ + "serde", + "ucd-trie", +] + [[package]] name = "kameo" version = "0.19.2" @@ -4909,10 +5075,13 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ + "async-lock", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", "equivalent", + "event-listener", + "futures-util", "parking_lot", "portable-atomic", "smallvec", @@ -5314,6 +5483,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.6" @@ -5534,13 +5713,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + [[package]] name = "oid-registry" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.2", ] [[package]] @@ -5895,6 +6083,29 @@ dependencies = [ "hmac 0.13.0", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.117", +] + [[package]] name = "peekable" version = "0.6.1" @@ -6169,6 +6380,20 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -6424,6 +6649,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "version_check", + "yansi", +] + [[package]] name = "prost" version = "0.14.3" @@ -6630,6 +6868,7 @@ name = "quinn-proto" version = "0.12.0" source = "git+https://github.com/Tipuch/quinn.git?branch=bbrv3#ce60e5b5c115db2a6053f4e0ca7fc52103cb76b9" dependencies = [ + "aws-lc-rs", "bytes", "fastbloom", "getrandom 0.3.4", @@ -6873,6 +7112,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "yasna 0.5.2", +] + [[package]] name = "rcgen" version = "0.14.8" @@ -6884,8 +7136,8 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "x509-parser", - "yasna", + "x509-parser 0.18.1", + "yasna 0.6.0", ] [[package]] @@ -6989,6 +7241,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", + "h2", "http", "http-body", "http-body-util", @@ -7008,14 +7261,16 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -7431,6 +7686,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-acme" +version = "0.15.1" +source = "git+https://github.com/rust-proxy/rustls-acme?branch=feat%2Fip#cc306ff72acd5f4b47cda426137ae80d10a425ce" +dependencies = [ + "async-io", + "async-trait", + "async-web-client", + "aws-lc-rs", + "base64 0.22.1", + "blocking", + "chrono", + "futures", + "futures-rustls", + "http", + "log", + "pem", + "rcgen 0.13.2", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower-service", + "webpki-roots 1.0.7", + "x509-parser 0.16.0", +] + [[package]] name = "rustls-jls" version = "1.3.1" @@ -7855,7 +8138,7 @@ checksum = "191a4f997fef5e095212c5790898516e9567d2d8502c4159317603ff0321e394" dependencies = [ "ahash", "annotate-snippets", - "base64 0.22.1", + "base64 0.21.7", "encoding_rs_io", "figment", "garde", @@ -7990,12 +8273,11 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.20.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" +checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" dependencies = [ "base64 0.22.1", - "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -8010,11 +8292,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.20.0" +version = "3.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" +checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" dependencies = [ - "darling 0.23.0", + "darling 0.21.3", "proc-macro2", "quote", "syn 2.0.117", @@ -8167,7 +8449,7 @@ dependencies = [ "iroh-quinn-proto", "quinn-jls", "quinn-proto-jls", - "rcgen", + "rcgen 0.14.8", "ring", "rustls", "rustls-jls", @@ -8181,7 +8463,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "webpki-roots", + "webpki-roots 1.0.7", "yaml-rust2", ] @@ -9934,7 +10216,7 @@ dependencies = [ "base64ct", "ctr 0.9.2", "curve25519-dalek 4.1.3", - "der-parser", + "der-parser 10.0.0", "derive-deftly", "derive_more", "digest 0.10.7", @@ -10494,6 +10776,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -10504,6 +10796,8 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", @@ -10511,6 +10805,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -10944,7 +11239,7 @@ dependencies = [ "tokio", "tokio-rustls", "url", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -11035,6 +11330,63 @@ dependencies = [ "uuid", ] +[[package]] +name = "tuic-server" +version = "1.8.4" +source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.8.4#649c10fe4efd557c5b5cb2cbc555c0e3f9e67298" +dependencies = [ + "arc-swap", + "aws-lc-rs", + "axum", + "axum-extra", + "axum-server", + "bytes", + "clap", + "derive_more", + "educe 0.6.0", + "eyre", + "figment", + "figment-json5", + "futures", + "futures-util", + "h3", + "humantime", + "humantime-serde", + "ip_network", + "ipnet", + "json5 1.3.1", + "moka", + "num_cpus", + "peekable", + "pest", + "pest_derive", + "rand 0.10.1", + "rcgen 0.14.8", + "regex", + "register-count", + "reqwest 0.12.28", + "rustls", + "rustls-acme", + "rustls-pemfile", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "socket2 0.6.3", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "toml 1.1.2+spec-1.1.0", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tuic-core", + "uuid", + "x509-parser 0.18.1", +] + [[package]] name = "tun-rs" version = "2.8.1" @@ -11264,7 +11616,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -11518,6 +11870,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -11548,7 +11913,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -11617,6 +11982,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + [[package]] name = "webpki-roots" version = "1.0.7" @@ -12199,19 +12573,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom 7.1.3", + "oid-registry 0.7.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "x509-parser" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.2", "aws-lc-rs", "data-encoding", - "der-parser", + "der-parser 10.0.0", "lazy_static", "nom 7.1.3", - "oid-registry", + "oid-registry 0.8.1", "ring", "rusticata-macros", "thiserror 2.0.18", @@ -12239,6 +12630,21 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yasna" version = "0.6.0" diff --git a/clash-lib/Cargo.toml b/clash-lib/Cargo.toml index 05d675976..b6c5a40c0 100644 --- a/clash-lib/Cargo.toml +++ b/clash-lib/Cargo.toml @@ -231,7 +231,7 @@ http-body-util = "0.1" reqwest = { version = "0.13", features = ["socks"] } sysinfo = { version = "0.39", features = ["network"]} moka = { version = "0.12", features = ["future"] } -tuic-server = { tag = "v1.7.2", git = "https://github.com/Itsusinn/tuic.git" } +tuic-server = { tag = "v1.8.4", git = "https://github.com/Itsusinn/tuic.git" } [build-dependencies] prost-build = "0.14" From a8b8c4cb6b7a1310cf3ade6b2baf2471a42f587c Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 25 May 2026 08:25:03 +0800 Subject: [PATCH 07/22] update deps --- Cargo.lock | 180 +++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 128 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fc5ced881..23b646527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,7 +83,7 @@ dependencies = [ "aead 0.6.0-rc.10", "aes 0.9.0", "cipher 0.5.2", - "ctr 0.10.0", + "ctr 0.10.1", "ghash 0.6.0", "subtle", ] @@ -633,9 +633,9 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" [[package]] name = "autocfg" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" @@ -1085,6 +1085,15 @@ dependencies = [ "tracing", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bstr" version = "1.12.1" @@ -1098,9 +1107,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.20.2" +version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" [[package]] name = "by_address" @@ -1197,9 +1206,9 @@ dependencies = [ [[package]] name = "cbc" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98db6aeaef0eeef2c1e3ce9a27b739218825dae116076352ac3777076aa22225" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ "cipher 0.5.2", ] @@ -1239,9 +1248,9 @@ dependencies = [ [[package]] name = "cfb-mode" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ad6745f4f269c4d93d7f414eeaaaf16705571a68dee59557f5b80a82e3e76be" +checksum = "ac64b0984be8510caae81455ea2c8c23e5af6be61c36129df62f3380d5d64e1f" dependencies = [ "cipher 0.5.2", ] @@ -2028,9 +2037,9 @@ dependencies = [ [[package]] name = "ctr" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" +checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ "cipher 0.5.2", ] @@ -2118,6 +2127,16 @@ dependencies = [ "darling_macro 0.21.3", ] +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + [[package]] name = "darling_core" version = "0.14.4" @@ -2153,6 +2172,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" dependencies = [ "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ "ident_case", "proc-macro2", "quote", @@ -2193,6 +2224,17 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + [[package]] name = "dashmap" version = "6.2.1" @@ -2683,9 +2725,9 @@ dependencies = [ [[package]] name = "either" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" [[package]] name = "elliptic-curve" @@ -3286,9 +3328,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-timer" -version = "3.0.3" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968" [[package]] name = "futures-util" @@ -4094,7 +4136,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.61.2", + "windows-core 0.62.2", ] [[package]] @@ -4583,9 +4625,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" dependencies = [ "cfg-if", "futures-util", @@ -4803,7 +4845,16 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" dependencies = [ - "logos-derive", + "logos-derive 0.15.1", +] + +[[package]] +name = "logos" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2c55a318a87600ea870ff8c2012148b44bf18b74fad48d0f835c38c7d07c5f" +dependencies = [ + "logos-derive 0.16.1", ] [[package]] @@ -4822,13 +4873,36 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "logos-codegen" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58b3ffaa284e1350d017a57d04ada118c4583cf260c8fb01e0fe28a2e9cf8970" +dependencies = [ + "fnv", + "proc-macro2", + "quote", + "regex-automata", + "regex-syntax", + "syn 2.0.117", +] + [[package]] name = "logos-derive" version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" dependencies = [ - "logos-codegen", + "logos-codegen 0.15.1", +] + +[[package]] +name = "logos-derive" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d3a9855747c17eaf4383823f135220716ab49bea5fbea7dd42cc9a92f8aa31" +dependencies = [ + "logos-codegen 0.16.1", ] [[package]] @@ -5991,18 +6065,19 @@ dependencies = [ [[package]] name = "pageant" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b537f975f6d8dcf48db368d7ec209d583b015713b5df0f5d92d2631e4ff5595" +checksum = "4f3a5ae18f65a85c67a77d18d42d3606c07948e3c17c1e5f74852b26589e88a5" dependencies = [ + "base16ct 1.0.0", "byteorder", "bytes", "delegate", "futures", "log", - "rand 0.8.6", - "sha2 0.10.9", - "thiserror 1.0.69", + "rand 0.10.1", + "sha2 0.11.0", + "thiserror 2.0.18", "tokio", "windows 0.62.2", "windows-strings 0.5.1", @@ -6315,7 +6390,7 @@ checksum = "c5a777c6e26664bc9504b3ce3f6133f8f20d9071f130a4f9fcbd3186959d8dd6" dependencies = [ "aes 0.9.0", "aes-gcm 0.11.0-rc.3", - "cbc 0.2.0", + "cbc 0.2.1", "der 0.8.0", "pbkdf2 0.13.0", "rand_core 0.10.1", @@ -6706,11 +6781,11 @@ dependencies = [ [[package]] name = "prost-reflect" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b89455ef41ed200cafc47c76c552ee7792370ac420497e551f16123a9135f76e" +checksum = "590aa145fee8f7a26b5a6055365e7c5e89a5c1caae9869de76ec0ee73181a2f9" dependencies = [ - "logos", + "logos 0.16.1", "miette", "prost", "prost-types", @@ -6746,7 +6821,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "072eee358134396a4643dff81cfff1c255c9fbd3fb296be14bdb6a26f9156366" dependencies = [ - "logos", + "logos 0.15.1", "miette", "prost-types", "thiserror 2.0.18", @@ -7476,10 +7551,10 @@ dependencies = [ "byteorder", "bytes", "cbc 0.1.2", - "cbc 0.2.0", + "cbc 0.2.1", "cipher 0.5.2", "crypto-bigint 0.7.0-rc.28", - "ctr 0.10.0", + "ctr 0.10.1", "ctr 0.9.2", "curve25519-dalek 5.0.0-pre.6", "data-encoding", @@ -8138,7 +8213,7 @@ checksum = "191a4f997fef5e095212c5790898516e9567d2d8502c4159317603ff0321e394" dependencies = [ "ahash", "annotate-snippets", - "base64 0.21.7", + "base64 0.22.1", "encoding_rs_io", "figment", "garde", @@ -8197,9 +8272,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.149" +version = "1.0.150" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" dependencies = [ "indexmap 2.14.0", "itoa", @@ -8273,11 +8348,12 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.17.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "381b283ce7bc6b476d903296fb59d0d36633652b633b27f64db4fb46dcbfc3b9" +checksum = "e72c1c2cb7b223fafb600a619537a871c2818583d619401b785e7c0b746ccde2" dependencies = [ "base64 0.22.1", + "bs58", "chrono", "hex", "indexmap 1.9.3", @@ -8292,11 +8368,11 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.17.0" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6d4e30573c8cb306ed6ab1dca8423eec9a463ea0e155f45399455e0368b27e0" +checksum = "b90c488738ecb4fb0262f41f43bc40efc5868d9fb744319ddf5f5317f417bfac" dependencies = [ - "darling 0.21.3", + "darling 0.23.0", "proc-macro2", "quote", "syn 2.0.117", @@ -11795,9 +11871,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" dependencies = [ "cfg-if", "once_cell", @@ -11808,9 +11884,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.71" +version = "0.4.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96492d0d3ffba25305a7dc88720d250b1401d7edca02cc3bcd50633b424673b8" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" dependencies = [ "js-sys", "wasm-bindgen", @@ -11818,9 +11894,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -11828,9 +11904,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" dependencies = [ "bumpalo", "proc-macro2", @@ -11841,9 +11917,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.121" +version = "0.2.122" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" dependencies = [ "unicode-ident", ] @@ -11946,9 +12022,9 @@ checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" [[package]] name = "web-sys" -version = "0.3.98" +version = "0.3.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" dependencies = [ "js-sys", "wasm-bindgen", From e4c5d87d75035fa6d9361261883b4739fe6a58fd Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 25 May 2026 08:45:17 +0800 Subject: [PATCH 08/22] fix TOCTOU --- Cargo.lock | 16 ++++++++-------- clash-lib/Cargo.toml | 4 ++-- clash-lib/src/proxy/tuic/mod.rs | 2 ++ clash-lib/src/proxy/tuic/test_utils.rs | 17 ++++++----------- 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 23b646527..c13f1e8d0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7316,7 +7316,6 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", "http", "http-body", "http-body-util", @@ -7336,14 +7335,12 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", - "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams", "web-sys", "webpki-roots 1.0.7", ] @@ -7358,6 +7355,7 @@ dependencies = [ "bytes", "encoding_rs", "futures-core", + "futures-util", "h2", "http", "http-body", @@ -7377,12 +7375,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -11389,7 +11389,7 @@ dependencies = [ [[package]] name = "tuic-core" version = "1.8.4" -source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.8.4#649c10fe4efd557c5b5cb2cbc555c0e3f9e67298" +source = "git+https://github.com/Itsusinn/tuic.git?branch=main#1fcb929243c0936bcde1c5b6c6ac9ffedec83d12" dependencies = [ "bytes", "eyre", @@ -11409,7 +11409,7 @@ dependencies = [ [[package]] name = "tuic-server" version = "1.8.4" -source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.8.4#649c10fe4efd557c5b5cb2cbc555c0e3f9e67298" +source = "git+https://github.com/Itsusinn/tuic.git?branch=main#1fcb929243c0936bcde1c5b6c6ac9ffedec83d12" dependencies = [ "arc-swap", "aws-lc-rs", @@ -11440,7 +11440,7 @@ dependencies = [ "rcgen 0.14.8", "regex", "register-count", - "reqwest 0.12.28", + "reqwest 0.13.3", "rustls", "rustls-acme", "rustls-pemfile", @@ -11948,9 +11948,9 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" dependencies = [ "futures-util", "js-sys", diff --git a/clash-lib/Cargo.toml b/clash-lib/Cargo.toml index b6c5a40c0..206254f7a 100644 --- a/clash-lib/Cargo.toml +++ b/clash-lib/Cargo.toml @@ -182,7 +182,7 @@ tor-rtcompat = { version = "0.42", optional = true, default-features = false, fe # tuic -tuic-core= { tag = "v1.8.4", optional = true, git = "https://github.com/Itsusinn/tuic.git", features = ["async_marshal", "marshal", "model"] } +tuic-core = { branch = "main", optional = true, git = "https://github.com/Itsusinn/tuic.git", features = ["async_marshal", "marshal", "model"] } register-count = { version = "0.1", optional = true } quinn = { version = "0.11", default-features = false, features = ["futures-io", "runtime-tokio"] } @@ -231,7 +231,7 @@ http-body-util = "0.1" reqwest = { version = "0.13", features = ["socks"] } sysinfo = { version = "0.39", features = ["network"]} moka = { version = "0.12", features = ["future"] } -tuic-server = { tag = "v1.8.4", git = "https://github.com/Itsusinn/tuic.git" } +tuic-server = { branch = "main", git = "https://github.com/Itsusinn/tuic.git" } [build-dependencies] prost-build = "0.14" diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index d3e039711..73ff3768d 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -500,6 +500,7 @@ mod tests { resolved_ip: None, so_mark: None, iface: None, + country: None, asn: None, traffic_stats: None, inbound_user: None, @@ -559,6 +560,7 @@ mod tests { resolved_ip: None, so_mark: None, iface: None, + country: None, asn: None, traffic_stats: None, inbound_user: None, diff --git a/clash-lib/src/proxy/tuic/test_utils.rs b/clash-lib/src/proxy/tuic/test_utils.rs index 3b8c1678a..c4e068895 100644 --- a/clash-lib/src/proxy/tuic/test_utils.rs +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -1,6 +1,7 @@ -use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration}; +use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::oneshot; +use tokio_util::sync::CancellationToken; /// A running tuic-server instance that cleans up on drop. pub struct TuicServerProcess { @@ -17,17 +18,8 @@ impl TuicServerProcess { let (ready_tx, ready_rx) = oneshot::channel::>(); let handle = tokio::spawn(async move { - let sock = std::net::UdpSocket::bind("127.0.0.1:0") - .expect("failed to allocate a free port"); - let port = sock.local_addr().unwrap().port(); - let server_addr: SocketAddr = - format!("127.0.0.1:{port}").parse().unwrap(); - drop(sock); // socket released; tuic-server's init will re-bind - - let _ = port_tx.send(port); - let cfg = tuic_server::Config { - server: server_addr, + server: "127.0.0.1:0".parse().unwrap(), log_level: tuic_server::config::LogLevel::Info, users: HashMap::from([( "00000000-0000-0000-0000-000000000001".parse().unwrap(), @@ -78,9 +70,12 @@ impl TuicServerProcess { online_counter, online_clients: moka::future::Cache::new(capacity), traffic_stats, + cancel: CancellationToken::new(), }); match tuic_server::server::Server::init(ctx).await { Ok(server) => { + let port = server.local_addr().unwrap().port(); + let _ = port_tx.send(port); let _ = ready_tx.send(Ok(())); server.start().await; } From 7e947cf0afd1e4374a3aa4686ef2c2c3952e7bd3 Mon Sep 17 00:00:00 2001 From: iHsin Date: Mon, 25 May 2026 08:57:01 +0800 Subject: [PATCH 09/22] extract tcp echo to common test utils --- clash-lib/src/proxy/tuic/mod.rs | 54 ++++------- clash-lib/src/proxy/utils/test_utils/echo.rs | 98 ++++++++++++++++++++ clash-lib/src/proxy/utils/test_utils/mod.rs | 1 + 3 files changed, 119 insertions(+), 34 deletions(-) create mode 100644 clash-lib/src/proxy/utils/test_utils/echo.rs diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index 73ff3768d..f8d4d449e 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -420,14 +420,17 @@ pub(crate) mod test_utils; mod tests { use std::{sync::Arc, time::Duration}; - use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::TcpListener, - }; + use tokio::io::{AsyncReadExt, AsyncWriteExt}; use super::{test_utils::TuicServerProcess, *}; use crate::{ - proxy::utils::{GLOBAL_DIRECT_CONNECTOR, test_utils::noop::NoopResolver}, + proxy::utils::{ + GLOBAL_DIRECT_CONNECTOR, + test_utils::{ + echo::{TcpEchoConfig, TcpEchoServer}, + noop::NoopResolver, + }, + }, session::Session, }; @@ -469,20 +472,8 @@ mod tests { let server = TuicServerProcess::start().await?; let port = server.port(); - // Start a local echo server as the target - let listener = TcpListener::bind("127.0.0.1:0").await?; - let target_port = listener.local_addr()?.port(); - - let target_handle = tokio::spawn(async move { - let (mut stream, _) = listener.accept().await.unwrap(); - let mut buf = vec![0u8; 5]; - for _ in 0..10 { - stream.read_exact(&mut buf).await.unwrap(); - assert_eq!(&buf, b"hello"); - stream.write_all(b"world").await.unwrap(); - stream.flush().await.unwrap(); - } - }); + let echo = TcpEchoServer::start().await?; + let target_port = echo.port(); let opts = gen_options(port)?; let handler = Arc::new(Handler::new(opts)); @@ -516,7 +507,7 @@ mod tests { assert_eq!(&buf, b"world"); } - target_handle.await?; + drop(echo); Ok(()) } @@ -527,20 +518,14 @@ mod tests { let server = TuicServerProcess::start().await?; let port = server.port(); - // Start a local TCP target so failure cannot be blamed on an - // unreachable upstream. - let listener = TcpListener::bind("127.0.0.1:0").await?; - let target_port = listener.local_addr()?.port(); - let _target_handle = tokio::spawn(async move { - // Accept one connection, then idle — the test should never - // actually reach this point because auth fails first. - if let Ok((mut stream, _)) = listener.accept().await { - let mut buf = [0u8; 5]; - while stream.read_exact(&mut buf).await.is_ok() { - stream.write_all(b"world").await.ok(); - } - } - }); + let echo = TcpEchoServer::start_with(TcpEchoConfig { + response: b"world", + expected_request: None, + read_size: 5, + iterations: None, + }) + .await?; + let target_port = echo.port(); let mut opts = gen_options(port)?; opts.password = "wrong_password".into(); @@ -581,6 +566,7 @@ mod tests { succeeded" ); } + drop(echo); Ok(()) } } diff --git a/clash-lib/src/proxy/utils/test_utils/echo.rs b/clash-lib/src/proxy/utils/test_utils/echo.rs new file mode 100644 index 000000000..5ffe106d4 --- /dev/null +++ b/clash-lib/src/proxy/utils/test_utils/echo.rs @@ -0,0 +1,98 @@ +use std::time::Duration; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +const DEFAULT_ACCEPT_TIMEOUT: Duration = Duration::from_secs(30); + +pub struct TcpEchoConfig { + pub response: &'static [u8], + pub expected_request: Option<&'static [u8]>, + pub read_size: usize, + pub iterations: Option, +} + +impl Default for TcpEchoConfig { + fn default() -> Self { + Self { + response: b"world", + expected_request: Some(b"hello"), + read_size: 5, + iterations: Some(10), + } + } +} + +pub struct TcpEchoServer { + handle: Option>, + port: u16, +} + +impl TcpEchoServer { + pub async fn start() -> anyhow::Result { + Self::start_with(TcpEchoConfig::default()).await + } + + pub async fn start_with(config: TcpEchoConfig) -> anyhow::Result { + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + + let handle = tokio::spawn(async move { + let stream = match tokio::time::timeout( + DEFAULT_ACCEPT_TIMEOUT, + listener.accept(), + ) + .await + { + Ok(Ok((stream, _))) => stream, + _ => return, + }; + let (mut reader, mut writer) = stream.into_split(); + let mut buf = vec![0u8; config.read_size]; + + match config.iterations { + Some(n) => { + for _ in 0..n { + if reader.read_exact(&mut buf).await.is_err() { + break; + } + if let Some(expected) = config.expected_request { + assert_eq!(buf.as_slice(), expected); + } + if writer.write_all(config.response).await.is_err() { + break; + } + let _ = writer.flush().await; + } + } + None => { + while reader.read_exact(&mut buf).await.is_ok() { + if let Some(expected) = config.expected_request { + assert_eq!(buf.as_slice(), expected); + } + if writer.write_all(config.response).await.is_err() { + break; + } + let _ = writer.flush().await; + } + } + } + }); + + Ok(Self { + handle: Some(handle), + port, + }) + } + + pub fn port(&self) -> u16 { + self.port + } +} + +impl Drop for TcpEchoServer { + fn drop(&mut self) { + if let Some(handle) = self.handle.take() { + handle.abort(); + } + } +} diff --git a/clash-lib/src/proxy/utils/test_utils/mod.rs b/clash-lib/src/proxy/utils/test_utils/mod.rs index 4adb252dc..583323d8c 100644 --- a/clash-lib/src/proxy/utils/test_utils/mod.rs +++ b/clash-lib/src/proxy/utils/test_utils/mod.rs @@ -1,3 +1,4 @@ +pub mod echo; pub mod noop; #[cfg(docker_test)] From 6d66ac34d38772c1c55f15879d21ead0feb14d53 Mon Sep 17 00:00:00 2001 From: iHsin Date: Thu, 4 Jun 2026 17:21:31 +0800 Subject: [PATCH 10/22] add test --- Cargo.lock | 507 +++++++++++++++++-- clash-lib/Cargo.toml | 2 + clash-lib/src/proxy/tuic/mod.rs | 127 ++++- clash-lib/src/proxy/tuic/test_utils.rs | 26 +- clash-lib/src/proxy/utils/test_utils/echo.rs | 5 +- 5 files changed, 627 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 853754422..1e5aca512 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -250,7 +250,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -261,7 +261,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -368,13 +368,29 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive 0.5.1", + "asn1-rs-impl", + "displaydoc", + "nom 7.1.3", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "asn1-rs" version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" dependencies = [ - "asn1-rs-derive", + "asn1-rs-derive 0.6.0", "asn1-rs-impl", "displaydoc", "nom 7.1.3", @@ -384,6 +400,18 @@ dependencies = [ "time", ] +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + [[package]] name = "asn1-rs-derive" version = "0.6.0" @@ -447,6 +475,37 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-http-codec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "096146020b08dbc4587685b0730a7ba905625af13c65f8028035cdfd69573c91" +dependencies = [ + "anyhow", + "futures", + "http", + "httparse", + "log", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + [[package]] name = "async-lock" version = "3.4.2" @@ -458,6 +517,17 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + [[package]] name = "async-object-pool" version = "0.2.0" @@ -496,6 +566,24 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "async-web-client" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37381fb4fad3cd9b579628c21a58f528ef029d1f072d10f16cb9431aa2236d29" +dependencies = [ + "async-http-codec", + "async-net", + "futures", + "futures-rustls", + "http", + "lazy_static", + "log", + "rustls-pki-types", + "thiserror 1.0.69", + "webpki-roots 0.26.11", +] + [[package]] name = "async_executors" version = "0.7.0" @@ -629,6 +717,28 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-extra" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-core", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "axum-macros" version = "0.5.1" @@ -640,6 +750,23 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "axum-server" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1df331683d982a0b9492b38127151e6453639cd34926eb9c07d4cd8c6d22bfc" +dependencies = [ + "bytes", + "either", + "fs-err", + "http", + "http-body", + "hyper", + "hyper-util", + "tokio", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.76" @@ -1354,6 +1481,7 @@ dependencies = [ "md-5", "memory-stats", "mockall", + "moka", "murmur3", "network-interface", "opentelemetry", @@ -1369,7 +1497,7 @@ dependencies = [ "quinn-proto 0.11.14", "rand 0.10.1", "rand_chacha 0.10.0", - "rcgen", + "rcgen 0.14.8", "regex", "register-count", "reqwest", @@ -1415,13 +1543,14 @@ dependencies = [ "tracing-subscriber", "tracing-test", "tuic-core", + "tuic-server", "tun-rs", "unix-udp-sock", "url", "uuid", "watfaq-dns", "watfaq-netstack", - "webpki-roots", + "webpki-roots 1.0.7", "windows 0.62.2", "zip", "zstd", @@ -2128,13 +2257,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs 0.6.2", + "displaydoc", + "nom 7.1.3", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "der-parser" version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.2", "cookie-factory", "displaydoc", "nom 7.1.3", @@ -2170,7 +2313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd5850ec9ad2d9ba0aa33fb22c0b0ef4d91e524566be497ac2a6e40b847e67bb" dependencies = [ "heck", - "indexmap 2.14.0", + "indexmap 1.9.3", "itertools 0.14.0", "proc-macro-crate", "proc-macro2", @@ -2334,7 +2477,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2729,7 +2872,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -2853,12 +2996,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" dependencies = [ "atomic 0.6.1", + "pear", "serde", + "serde_yaml", "toml 0.8.23", "uncased", "version_check", ] +[[package]] +name = "figment-json5" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f6982da09e166efe7dc3c5cf1fe01ef85419733eb188c0df0b571eda9e8a81" +dependencies = [ + "figment", + "json5 0.4.1", + "serde", +] + [[package]] name = "filetime" version = "0.2.29" @@ -2946,6 +3102,16 @@ dependencies = [ "futures-core", ] +[[package]] +name = "fs-err" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +dependencies = [ + "autocfg", + "tokio", +] + [[package]] name = "fs-mistrust" version = "0.14.1" @@ -3046,7 +3212,10 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -3443,6 +3612,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" @@ -4060,6 +4235,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inlinable_string" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fae54786f62fb2918dcfae3d568594e50eb9b5c25bf04371af6fe7516452fb" + [[package]] name = "inotify" version = "0.11.1" @@ -4121,6 +4302,12 @@ dependencies = [ "rustversion", ] +[[package]] +name = "ip_network" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2f047c0a98b2f299aa5d6d7088443570faae494e9ae1305e48be000c9e0eb1" + [[package]] name = "ip_network_table-deps-treebitmap" version = "0.5.0" @@ -4212,7 +4399,7 @@ dependencies = [ "libc", "socket2 0.6.4", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4340,6 +4527,27 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "json5" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "733a844dbd6fef128e98cb4487b887cb55454d92cd9994b1bafe004fabbe670c" +dependencies = [ + "serde", + "ucd-trie", +] + [[package]] name = "kameo" version = "0.19.2" @@ -4794,10 +5002,13 @@ version = "0.12.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "957228ad12042ee839f93c8f257b62b4c0ab5eaae1d4fa60de53b27c9d7c5046" dependencies = [ + "async-lock", "crossbeam-channel", "crossbeam-epoch", "crossbeam-utils", "equivalent", + "event-listener", + "futures-util", "parking_lot", "portable-atomic", "smallvec", @@ -5133,7 +5344,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -5198,6 +5409,16 @@ dependencies = [ "libm", ] +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "num_enum" version = "0.7.6" @@ -5418,13 +5639,22 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs 0.6.2", +] + [[package]] name = "oid-registry" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.2", ] [[package]] @@ -5759,6 +5989,29 @@ dependencies = [ "hmac 0.13.0", ] +[[package]] +name = "pear" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdeeaa00ce488657faba8ebf44ab9361f9365a97bd39ffb8a60663f57ff4b467" +dependencies = [ + "inlinable_string", + "pear_codegen", + "yansi", +] + +[[package]] +name = "pear_codegen" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bab5b985dc082b345f812b7df84e1bef27e7207b39e448439ba8bd69c93f147" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn 2.0.117", +] + [[package]] name = "peekable" version = "0.6.1" @@ -6042,6 +6295,20 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "poly1305" version = "0.8.0" @@ -6308,6 +6575,19 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "version_check", + "yansi", +] + [[package]] name = "prost" version = "0.14.3" @@ -6413,7 +6693,7 @@ dependencies = [ "derive-deftly", "libc", "paste", - "thiserror 2.0.18", + "thiserror 1.0.69", ] [[package]] @@ -6514,6 +6794,7 @@ name = "quinn-proto" version = "0.12.0" source = "git+https://github.com/Tipuch/quinn.git?branch=bbrv3#ce60e5b5c115db2a6053f4e0ca7fc52103cb76b9" dependencies = [ + "aws-lc-rs", "bytes", "fastbloom", "getrandom 0.3.4", @@ -6577,7 +6858,7 @@ dependencies = [ "libc", "socket2 0.6.4", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -6757,6 +7038,19 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rcgen" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "yasna 0.5.2", +] + [[package]] name = "rcgen" version = "0.14.8" @@ -6768,8 +7062,8 @@ dependencies = [ "ring", "rustls-pki-types", "time", - "x509-parser", - "yasna", + "x509-parser 0.18.1", + "yasna 0.6.0", ] [[package]] @@ -6895,12 +7189,14 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-rustls", + "tokio-util", "tower", "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", ] @@ -7251,7 +7547,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7270,6 +7566,34 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-acme" +version = "0.15.1" +source = "git+https://github.com/rust-proxy/rustls-acme?branch=feat%2Fip#cc306ff72acd5f4b47cda426137ae80d10a425ce" +dependencies = [ + "async-io", + "async-trait", + "async-web-client", + "aws-lc-rs", + "base64 0.22.1", + "blocking", + "chrono", + "futures", + "futures-rustls", + "http", + "log", + "pem", + "rcgen 0.13.2", + "serde", + "serde_json", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower-service", + "webpki-roots 1.0.7", + "x509-parser 0.16.0", +] + [[package]] name = "rustls-jls" version = "1.3.1" @@ -7335,7 +7659,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -7973,7 +8297,7 @@ dependencies = [ "libc", "quinn-jls", "quinn-proto-jls", - "rcgen", + "rcgen 0.14.8", "ring", "rustls", "rustls-jls", @@ -7987,7 +8311,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", - "webpki-roots", + "webpki-roots 1.0.7", "windows 0.56.0", "yaml-rust2", ] @@ -8266,7 +8590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8729,7 +9053,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -9752,7 +10076,7 @@ dependencies = [ "base64ct", "ctr 0.9.2", "curve25519-dalek 4.1.3", - "der-parser", + "der-parser 10.0.0", "derive-deftly", "derive_more", "digest 0.10.7", @@ -10312,6 +10636,16 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -10322,6 +10656,8 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", @@ -10329,6 +10665,7 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -10767,7 +11104,7 @@ dependencies = [ "tokio-rustls", "tracing", "url", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -10858,6 +11195,62 @@ dependencies = [ "uuid", ] +[[package]] +name = "tuic-server" +version = "1.8.5" +source = "git+https://github.com/Itsusinn/tuic.git?tag=v1.8.5#e4ae97bca5d5386ae964f09631e72c3bd392ca6b" +dependencies = [ + "arc-swap", + "aws-lc-rs", + "axum", + "axum-extra", + "axum-server", + "bytes", + "clap", + "derive_more", + "educe 0.6.0", + "eyre", + "figment", + "figment-json5", + "futures", + "futures-util", + "h3", + "humantime", + "humantime-serde", + "ip_network", + "ipnet", + "json5 1.3.1", + "moka", + "num_cpus", + "peekable", + "pest", + "pest_derive", + "rand 0.10.1", + "rcgen 0.14.8", + "regex", + "reqwest", + "rustls", + "rustls-acme", + "rustls-pemfile", + "serde", + "serde_json", + "sha2 0.11.0", + "smallvec", + "socket2 0.6.4", + "thiserror 2.0.18", + "time", + "tokio", + "tokio-stream", + "tokio-util", + "toml 1.1.2+spec-1.1.0", + "tracing", + "tracing-appender", + "tracing-subscriber", + "tuic-core", + "uuid", + "x509-parser 0.18.1", +] + [[package]] name = "tun-rs" version = "2.8.1" @@ -11081,7 +11474,7 @@ dependencies = [ "rustls-pki-types", "ureq-proto", "utf8-zero", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -11305,6 +11698,19 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wasmparser" version = "0.244.0" @@ -11335,7 +11741,7 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", - "webpki-roots", + "webpki-roots 1.0.7", ] [[package]] @@ -11404,6 +11810,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.7", +] + [[package]] name = "webpki-roots" version = "1.0.7" @@ -11441,7 +11856,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -12039,19 +12454,36 @@ dependencies = [ "zeroize", ] +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs 0.6.2", + "data-encoding", + "der-parser 9.0.0", + "lazy_static", + "nom 7.1.3", + "oid-registry 0.7.1", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "x509-parser" version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" dependencies = [ - "asn1-rs", + "asn1-rs 0.7.2", "aws-lc-rs", "data-encoding", - "der-parser", + "der-parser 10.0.0", "lazy_static", "nom 7.1.3", - "oid-registry", + "oid-registry 0.8.1", "ring", "rusticata-macros", "thiserror 2.0.18", @@ -12079,6 +12511,21 @@ dependencies = [ "hashlink", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + [[package]] name = "yasna" version = "0.6.0" diff --git a/clash-lib/Cargo.toml b/clash-lib/Cargo.toml index 94f3836cb..d7710d207 100644 --- a/clash-lib/Cargo.toml +++ b/clash-lib/Cargo.toml @@ -231,6 +231,8 @@ tracing-test = "0.2" http-body-util = "0.1" reqwest = { version = "0.13", features = ["socks"] } sysinfo = { version = "0.39", features = ["network"]} +moka = { version = "0.12", features = ["future"] } +tuic-server = { tag = "v1.8.5", git = "https://github.com/Itsusinn/tuic.git" } [build-dependencies] prost-build = "0.14" diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index f8d4d449e..94286e78f 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -435,9 +435,21 @@ mod tests { }; fn gen_options(port: u16) -> anyhow::Result { + gen_options_with(port, "127.0.0.1", "127.0.0.1") + } + + fn gen_options_v6(port: u16) -> anyhow::Result { + gen_options_with(port, "::1", "::1") + } + + fn gen_options_with( + port: u16, + server: &str, + ip: &str, + ) -> anyhow::Result { Ok(HandlerOptions { name: "test-tuic".to_owned(), - server: "127.0.0.1".to_owned(), + server: server.to_owned(), port, common_opts: Default::default(), uuid: "00000000-0000-0000-0000-000000000001".parse()?, @@ -452,7 +464,7 @@ mod tests { congestion_controller: CongestionControl::Bbr, max_udp_relay_packet_size: 1500, max_open_stream: VarInt::from_u64(32)?, - ip: Some("127.0.0.1".to_owned()), + ip: Some(ip.to_owned()), skip_cert_verify: true, sni: Some("localhost".to_owned()), gc_interval: Duration::from_millis(3000), @@ -464,6 +476,12 @@ mod tests { }) } + fn ipv6_resolver() -> crate::app::dns::ThreadSafeDNSResolver { + let mut mock = crate::app::dns::MockClashResolver::new(); + mock.expect_ipv6().return_const(true); + Arc::new(mock) + } + /// TCP ping-pong test: start an echo server, connect through tuic, send /// "hello" and verify we receive "world" back. #[tokio::test] @@ -523,6 +541,7 @@ mod tests { expected_request: None, read_size: 5, iterations: None, + ..Default::default() }) .await?; let target_port = echo.port(); @@ -569,6 +588,110 @@ mod tests { drop(echo); Ok(()) } + + /// TCP ping-pong over IPv6 loopback. + #[tokio::test] + async fn test_tuic_ping_pong_tcp_ipv6() -> anyhow::Result<()> { + if std::net::UdpSocket::bind("[::1]:0").is_err() { + eprintln!("skipping: no IPv6 loopback"); + return Ok(()); + } + crate::tests::initialize(); + let server = TuicServerProcess::start_v6().await?; + let port = server.port(); + + let echo = TcpEchoServer::start_with(TcpEchoConfig { + bind_addr: "::1", + ..Default::default() + }) + .await?; + let target_port = echo.port(); + + let opts = gen_options_v6(port)?; + let handler = Arc::new(Handler::new(opts)); + handler + .register_connector(GLOBAL_DIRECT_CONNECTOR.clone()) + .await; + + let resolver = ipv6_resolver(); + + let session = Session { + network: crate::session::Network::Tcp, + typ: crate::session::Type::Socks5, + source: "[::1]:54321".parse()?, + destination: format!("[::1]:{target_port}").parse()?, + resolved_ip: None, + so_mark: None, + iface: None, + country: None, + asn: None, + traffic_stats: None, + inbound_user: None, + }; + + let mut stream = handler.connect_stream(&session, resolver).await?; + + for _ in 0..10 { + stream.write_all(b"hello").await?; + stream.flush().await?; + let mut buf = vec![0u8; 5]; + stream.read_exact(&mut buf).await?; + assert_eq!(&buf, b"world"); + } + + drop(echo); + Ok(()) + } + + /// TCP ping-pong with dual-stack server (client connects via IPv4). + #[tokio::test] + async fn test_tuic_ping_pong_tcp_dual_stack() -> anyhow::Result<()> { + if std::net::UdpSocket::bind("[::1]:0").is_err() { + eprintln!("skipping: no IPv6 loopback"); + return Ok(()); + } + crate::tests::initialize(); + let server = TuicServerProcess::start_dual_stack().await?; + let port = server.port(); + + let echo = TcpEchoServer::start().await?; + let target_port = echo.port(); + + let opts = gen_options(port)?; + let handler = Arc::new(Handler::new(opts)); + handler + .register_connector(GLOBAL_DIRECT_CONNECTOR.clone()) + .await; + + let resolver = ipv6_resolver(); + + let session = Session { + network: crate::session::Network::Tcp, + typ: crate::session::Type::Socks5, + source: "127.0.0.1:54321".parse()?, + destination: format!("127.0.0.1:{target_port}").parse()?, + resolved_ip: None, + so_mark: None, + iface: None, + country: None, + asn: None, + traffic_stats: None, + inbound_user: None, + }; + + let mut stream = handler.connect_stream(&session, resolver).await?; + + for _ in 0..10 { + stream.write_all(b"hello").await?; + stream.flush().await?; + let mut buf = vec![0u8; 5]; + stream.read_exact(&mut buf).await?; + assert_eq!(&buf, b"world"); + } + + drop(echo); + Ok(()) + } } #[cfg(all(test, docker_test, throughput_test))] diff --git a/clash-lib/src/proxy/tuic/test_utils.rs b/clash-lib/src/proxy/tuic/test_utils.rs index c4e068895..e6947b3b8 100644 --- a/clash-lib/src/proxy/tuic/test_utils.rs +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -10,16 +10,29 @@ pub struct TuicServerProcess { } impl TuicServerProcess { - /// Start a tuic-server instance on a random port. pub async fn start() -> anyhow::Result { - // We use a channel to receive the actual bound port from the task. - let (port_tx, port_rx) = oneshot::channel(); + Self::start_with_config("127.0.0.1:0", false, false).await + } + + pub async fn start_v6() -> anyhow::Result { + Self::start_with_config("[::1]:0", false, false).await + } + pub async fn start_dual_stack() -> anyhow::Result { + Self::start_with_config("[::]:0", true, true).await + } + + async fn start_with_config( + server_bind: &'static str, + dual_stack: bool, + udp_relay_ipv6: bool, + ) -> anyhow::Result { + let (port_tx, port_rx) = oneshot::channel(); let (ready_tx, ready_rx) = oneshot::channel::>(); let handle = tokio::spawn(async move { let cfg = tuic_server::Config { - server: "127.0.0.1:0".parse().unwrap(), + server: server_bind.parse().unwrap(), log_level: tuic_server::config::LogLevel::Info, users: HashMap::from([( "00000000-0000-0000-0000-000000000001".parse().unwrap(), @@ -32,7 +45,7 @@ impl TuicServerProcess { ..Default::default() }, zero_rtt_handshake: false, - dual_stack: false, + dual_stack, outbound: tuic_server::config::OutboundConfig { default: tuic_server::config::OutboundRule { kind: "direct".into(), @@ -41,7 +54,7 @@ impl TuicServerProcess { named: HashMap::new(), }, acl: vec![], - udp_relay_ipv6: false, + udp_relay_ipv6, experimental: tuic_server::config::ExperimentalConfig { drop_loopback: false, drop_private: false, @@ -86,7 +99,6 @@ impl TuicServerProcess { } }); - // Wait for the server to be ready (or for init to fail). let port = tokio::time::timeout(Duration::from_secs(5), port_rx) .await .map_err(|_| anyhow::anyhow!("tuic-server failed to report a port"))? diff --git a/clash-lib/src/proxy/utils/test_utils/echo.rs b/clash-lib/src/proxy/utils/test_utils/echo.rs index 5ffe106d4..5efeceab7 100644 --- a/clash-lib/src/proxy/utils/test_utils/echo.rs +++ b/clash-lib/src/proxy/utils/test_utils/echo.rs @@ -9,6 +9,7 @@ pub struct TcpEchoConfig { pub expected_request: Option<&'static [u8]>, pub read_size: usize, pub iterations: Option, + pub bind_addr: &'static str, } impl Default for TcpEchoConfig { @@ -18,6 +19,7 @@ impl Default for TcpEchoConfig { expected_request: Some(b"hello"), read_size: 5, iterations: Some(10), + bind_addr: "127.0.0.1", } } } @@ -33,7 +35,8 @@ impl TcpEchoServer { } pub async fn start_with(config: TcpEchoConfig) -> anyhow::Result { - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let bind_to = format!("{}:0", config.bind_addr); + let listener = tokio::net::TcpListener::bind(bind_to.as_str()).await?; let port = listener.local_addr()?.port(); let handle = tokio::spawn(async move { From d03dbba30ab70e2ad829c3f9c670d6a2e04eeb95 Mon Sep 17 00:00:00 2001 From: iHsin Date: Thu, 4 Jun 2026 19:14:43 +0800 Subject: [PATCH 11/22] test(api): replace flaky httpbin.yba.dev with a local drip server MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The connection-chain test was hitting an external httpbin instance that is currently returning 502 Bad Gateway on CI (and live as I write this), causing the shadowsocks-feature api_tests job to fail across all platforms. Run a tiny local hyper HTTP/1 server bound to 127.0.0.1:0 and swap the upstream-domain rule for a DST-PORT match against that server's port — the `["DIRECT", "url-test", "test 🌏"]` chain still fires, but the test is now hermetic. Co-Authored-By: Claude Opus 4.7 --- clash-lib/tests/api_tests.rs | 20 ++++++++- clash-lib/tests/common/mod.rs | 80 ++++++++++++++++++++++++++++++++++- 2 files changed, 97 insertions(+), 3 deletions(-) diff --git a/clash-lib/tests/api_tests.rs b/clash-lib/tests/api_tests.rs index 3554422eb..0db952940 100644 --- a/clash-lib/tests/api_tests.rs +++ b/clash-lib/tests/api_tests.rs @@ -2,6 +2,8 @@ use crate::common::{ ClashInstance, alloc_ports, make_client_config_str, send_http_request, wait_port_ready, }; +#[cfg(feature = "shadowsocks")] +use crate::common::DripServer; use bytes::{Buf, Bytes}; use clash_lib::{Config, Options}; use http_body_util::BodyExt; @@ -288,6 +290,14 @@ async fn test_connections_returns_proxy_chain_names() { // Allocate unique ports for the client (7-port block) let client_base = alloc_ports(CLIENT_PORT_BLOCK); + // Spin up a local HTTP server that delays the response so the connection + // stays open long enough for the /connections poll to observe it. Using a + // local server keeps the test hermetic — no flaky external dependency. + let drip = DripServer::start(Duration::from_secs(3), 500) + .await + .expect("Failed to start drip server"); + let drip_port = drip.port; + // Build server config dynamically from server.yaml let server_config_str = { let tpl = std::fs::read_to_string(wd_server.join("server.yaml")) @@ -297,9 +307,15 @@ async fn test_connections_returns_proxy_chain_names() { }; // Build client config dynamically: template substitution + SS server port + // and swap the upstream-domain rule for one that matches our local drip + // server's destination port, so the `test 🌏` chain still fires. let client_config_str = make_client_config_str(client_base) .replace("127.0.0.1:8901", &format!("127.0.0.1:{}", server_base + 1)) - .replace("port: 8901", &format!("port: {}", server_base + 1)); + .replace("port: 8901", &format!("port: {}", server_base + 1)) + .replace( + "DOMAIN,httpbin.yba.dev,test 🌏", + &format!("DST-PORT,{},test 🌏", drip_port), + ); // Start server instance with RAII guard let _server = ClashInstance::start( @@ -344,7 +360,7 @@ async fn test_connections_returns_proxy_chain_names() { .expect("Failed to build reqwest client"); let response = client - .get("https://httpbin.yba.dev/drip?duration=2&delay=1&numbytes=500") + .get(format!("http://127.0.0.1:{}/", drip_port)) .send() .await .expect("Failed to send request through proxy"); diff --git a/clash-lib/tests/common/mod.rs b/clash-lib/tests/common/mod.rs index bb751efa5..0b9537f95 100644 --- a/clash-lib/tests/common/mod.rs +++ b/clash-lib/tests/common/mod.rs @@ -1,9 +1,12 @@ +use bytes::Bytes; use futures::TryFutureExt; -use hyper::body::Incoming; +use http_body_util::Full; +use hyper::{Response, body::Incoming}; use hyper_util::rt::TokioIo; use std::{ net::{Shutdown, TcpStream}, sync::atomic::{AtomicU16, Ordering}, + time::Duration, }; /// Backward-compatible wrapper used by integration_tests.rs. @@ -317,3 +320,78 @@ impl Socks5UdpSession { (pkt[pos..].to_vec(), src) } } + +// ── Local "drip" HTTP server ───────────────────────────────────────────────── + +/// RAII guard for a local HTTP server bound to 127.0.0.1 on a random port. +/// Each request blocks for `delay` before returning a body of `num_bytes` of +/// `b'*'`. The blocking keeps the connection alive long enough for the +/// /connections API to observe it. +/// +/// Cancelled on drop. +#[allow(dead_code)] +pub struct DripServer { + pub port: u16, + token: tokio_util::sync::CancellationToken, +} + +#[allow(dead_code)] +impl DripServer { + /// Bind a hyper HTTP/1 server on 127.0.0.1:0 and spawn the accept loop. + /// Returns once the listener is bound — the port is immediately usable. + pub async fn start(delay: Duration, num_bytes: usize) -> std::io::Result { + use hyper::server::conn::http1; + use hyper::service::service_fn; + + let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; + let port = listener.local_addr()?.port(); + let token = tokio_util::sync::CancellationToken::new(); + let cancel = token.clone(); + + tokio::spawn(async move { + loop { + let (stream, _) = tokio::select! { + _ = cancel.cancelled() => break, + accept = listener.accept() => match accept { + Ok(v) => v, + Err(_) => continue, + }, + }; + let io = TokioIo::new(stream); + let conn_cancel = cancel.clone(); + tokio::spawn(async move { + let svc = service_fn(move |_req| { + let token = conn_cancel.clone(); + async move { + tokio::select! { + _ = token.cancelled() => {} + _ = tokio::time::sleep(delay) => {} + } + let body = + Full::new(Bytes::from(vec![b'*'; num_bytes])); + Ok::<_, std::convert::Infallible>( + Response::builder() + .status(200) + .header( + "content-type", + "application/octet-stream", + ) + .body(body) + .unwrap(), + ) + } + }); + let _ = http1::Builder::new().serve_connection(io, svc).await; + }); + } + }); + + Ok(Self { port, token }) + } +} + +impl Drop for DripServer { + fn drop(&mut self) { + self.token.cancel(); + } +} From 468173adec423138a2d3791fdd9313a29c265572 Mon Sep 17 00:00:00 2001 From: iHsin Date: Thu, 4 Jun 2026 21:23:36 +0800 Subject: [PATCH 12/22] style: satisfy nightly rustfmt for drip-server helpers `cargo +nightly fmt --all -- --check` flagged three formatting issues in the previous commit: import ordering in api_tests.rs, a split `use` inside DripServer::start, and a wrapped `let body = Full::new(...)` line that fits on a single line. Co-Authored-By: Claude Opus 4.7 --- clash-lib/tests/api_tests.rs | 4 ++-- clash-lib/tests/common/mod.rs | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/clash-lib/tests/api_tests.rs b/clash-lib/tests/api_tests.rs index 0db952940..6afb83f87 100644 --- a/clash-lib/tests/api_tests.rs +++ b/clash-lib/tests/api_tests.rs @@ -1,9 +1,9 @@ +#[cfg(feature = "shadowsocks")] +use crate::common::DripServer; use crate::common::{ ClashInstance, alloc_ports, make_client_config_str, send_http_request, wait_port_ready, }; -#[cfg(feature = "shadowsocks")] -use crate::common::DripServer; use bytes::{Buf, Bytes}; use clash_lib::{Config, Options}; use http_body_util::BodyExt; diff --git a/clash-lib/tests/common/mod.rs b/clash-lib/tests/common/mod.rs index 0b9537f95..1fdd7d6a5 100644 --- a/clash-lib/tests/common/mod.rs +++ b/clash-lib/tests/common/mod.rs @@ -340,8 +340,7 @@ impl DripServer { /// Bind a hyper HTTP/1 server on 127.0.0.1:0 and spawn the accept loop. /// Returns once the listener is bound — the port is immediately usable. pub async fn start(delay: Duration, num_bytes: usize) -> std::io::Result { - use hyper::server::conn::http1; - use hyper::service::service_fn; + use hyper::{server::conn::http1, service::service_fn}; let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; let port = listener.local_addr()?.port(); @@ -367,8 +366,7 @@ impl DripServer { _ = token.cancelled() => {} _ = tokio::time::sleep(delay) => {} } - let body = - Full::new(Bytes::from(vec![b'*'; num_bytes])); + let body = Full::new(Bytes::from(vec![b'*'; num_bytes])); Ok::<_, std::convert::Infallible>( Response::builder() .status(200) From 14735c8e4b1b0d3cf572bd52389f2d8b81d558bb Mon Sep 17 00:00:00 2001 From: iHsin Date: Thu, 4 Jun 2026 21:43:01 +0800 Subject: [PATCH 13/22] fix(test): restore IMAGE_TUIC for the docker-based throughput e2e `cababdcf "remove"` dropped the `IMAGE_TUIC` constant from `docker_utils/consts.rs`, but `e2e_throughput_tuic_bbr` in `clash-lib/src/proxy/tuic/mod.rs` (gated on `cfg(all(test, docker_test, throughput_test))`) still references it. That breaks the Proxy E2E Throughput job with `cannot find value IMAGE_TUIC in this scope`. The non-docker unit tests this PR adds do not use this constant, so re-add it under the same docker+throughput cfg the consumer is gated on. Co-Authored-By: Claude Opus 4.7 --- clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs b/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs index ae4b28c74..8b3993a82 100644 --- a/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs +++ b/clash-lib/src/proxy/utils/test_utils/docker_utils/consts.rs @@ -17,6 +17,8 @@ pub const IMAGE_SOCKS5: &str = "v2fly/v2fly-core:v4.45.2"; #[cfg(all(feature = "ssh", docker_test))] pub const IMAGE_OPENSSH: &str = "docker.io/linuxserver/openssh-server:latest"; pub const IMAGE_HYSTERIA: &str = "tobyxdd/hysteria:latest"; +#[cfg(all(feature = "tuic", docker_test, throughput_test))] +pub const IMAGE_TUIC: &str = "ghcr.io/itsusinn/tuic-server:latest"; #[cfg(feature = "shadowquic")] pub const IMAGE_SHADOWQUIC: &str = "ghcr.io/spongebob888/shadowquic:latest"; pub const IMAGE_SINGBOX: &str = "ghcr.io/sagernet/sing-box:v1.13.8"; From 390b6b7ca88df5c8e5578062d2af5c5b57b12c5a Mon Sep 17 00:00:00 2001 From: iHsin Date: Thu, 4 Jun 2026 23:00:07 +0800 Subject: [PATCH 14/22] fix(test): drop `--compatibility=false` from throughput runner Commit 6f7c3599 ("feat: silently ignore unknown top-level/dns config fields; add --strict-config") landed a follow-up "chore: default --compatibility to false" that reverted clap's `ArgAction::Set` on the flag, so `--compatibility` is now a plain boolean switch. Passing `--compatibility=false` to the binary now errors with "unexpected value 'false' for '--compatibility' found; no more were expected", failing every clash-rs subprocess spawned by the E2E throughput tests. The flag's new default is already `false`, which is what the tests want, so just drop the arg. Co-Authored-By: Claude Opus 4.7 --- .../proxy/utils/test_utils/docker_utils/mod.rs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/clash-lib/src/proxy/utils/test_utils/docker_utils/mod.rs b/clash-lib/src/proxy/utils/test_utils/docker_utils/mod.rs index d8e81d7aa..90e1be899 100644 --- a/clash-lib/src/proxy/utils/test_utils/docker_utils/mod.rs +++ b/clash-lib/src/proxy/utils/test_utils/docker_utils/mod.rs @@ -789,18 +789,16 @@ pub async fn clash_process_e2e_throughput( let cfg_path = cfg_file.path().to_owned(); // --- spawn clash-rs subprocess --- - // Pass --compatibility=false to disable compatibility mode. - // When enabled (default), it auto-sets `geosite = "geosite.dat"` which - // triggers a network download on CI when the file is absent, causing - // concurrent clash-rs instances to race-write the same file and corrupt it - // ("geosite decode failed: buffer underflow"). Tests set all required - // config values explicitly, so compatibility mode is not needed. - // Note: `--compatibility=false` (with `=`) is required for clap bool - // value_parser; separate args (`--compatibility false`) are misinterpreted. + // Compatibility mode auto-sets `geosite = "geosite.dat"` which triggers a + // network download on CI when the file is absent, causing concurrent + // clash-rs instances to race-write the same file and corrupt it + // ("geosite decode failed: buffer underflow"). Tests set all required + // config values explicitly, so compatibility mode is not needed — and + // since the binary now defaults `--compatibility` to false, we just omit + // the flag entirely. let mut child = tokio::process::Command::new(binary) .arg("-c") .arg(&cfg_path) - .arg("--compatibility=false") .kill_on_drop(true) .stdout(std::process::Stdio::inherit()) .stderr(std::process::Stdio::inherit()) From a4478baaa5df668ccadc25671cb1ffb1409bbb8d Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 01:27:18 +0800 Subject: [PATCH 15/22] test(tuic): skip TCP ping-pong tests under qemu-user emulation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_tuic_ping_pong_tcp{,_ipv6,_dual_stack}` reliably fail on cross targets (aarch64-gnu/musl, armv7-gnu/musl, riscv64gc) with "Error: early eof" — auth completes, then the QUIC stream gets reset mid-relay. The same tests pass on every native target we run (Linux x86_64, macOS aarch64, Windows). QUIC under qemu-user is unreliable: packet timing, MTU discovery and timers all drift enough to race against TUIC's idle / request timeouts. Add `running_under_qemu_user()` to the TUIC test_utils — it asks the kernel for `utsname.machine` and compares it to the binary's compile-time `target_arch`. A mismatch means we're a foreign-arch binary running through qemu-user (which is exactly the `cross test` setup). Native runs match and proceed normally. Plumb a `skip_under_qemu_user!` macro into the three failing tests so they emit a one-line skip notice instead of flaking. `test_tuic_auth_failure` keeps running everywhere — it doesn't depend on relay timing. Co-Authored-By: Claude Opus 4.7 --- clash-lib/src/proxy/tuic/mod.rs | 3 ++ clash-lib/src/proxy/tuic/test_utils.rs | 64 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index 94286e78f..0ec9d33e6 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -486,6 +486,7 @@ mod tests { /// "hello" and verify we receive "world" back. #[tokio::test] async fn test_tuic_ping_pong_tcp() -> anyhow::Result<()> { + crate::skip_under_qemu_user!("test_tuic_ping_pong_tcp"); crate::tests::initialize(); let server = TuicServerProcess::start().await?; let port = server.port(); @@ -592,6 +593,7 @@ mod tests { /// TCP ping-pong over IPv6 loopback. #[tokio::test] async fn test_tuic_ping_pong_tcp_ipv6() -> anyhow::Result<()> { + crate::skip_under_qemu_user!("test_tuic_ping_pong_tcp_ipv6"); if std::net::UdpSocket::bind("[::1]:0").is_err() { eprintln!("skipping: no IPv6 loopback"); return Ok(()); @@ -646,6 +648,7 @@ mod tests { /// TCP ping-pong with dual-stack server (client connects via IPv4). #[tokio::test] async fn test_tuic_ping_pong_tcp_dual_stack() -> anyhow::Result<()> { + crate::skip_under_qemu_user!("test_tuic_ping_pong_tcp_dual_stack"); if std::net::UdpSocket::bind("[::1]:0").is_err() { eprintln!("skipping: no IPv6 loopback"); return Ok(()); diff --git a/clash-lib/src/proxy/tuic/test_utils.rs b/clash-lib/src/proxy/tuic/test_utils.rs index e6947b3b8..6bb183451 100644 --- a/clash-lib/src/proxy/tuic/test_utils.rs +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -136,3 +136,67 @@ impl Drop for TuicServerProcess { } } } + +/// Detect that the test binary is running under qemu-user emulation (i.e. +/// `cross test` on a non-native target). QUIC under qemu-user is unreliable +/// — packet timing, MTU discovery and timers all drift enough that the TUIC +/// stream ping-pong races against the idle / request timeouts and the stream +/// gets reset mid-relay. We use this to skip the relay tests on cross-built +/// targets while keeping native arch coverage (Linux x86_64, macOS aarch64). +/// +/// The signal: under qemu-user the binary's compile-time `target_arch` differs +/// from the kernel's reported `utsname.machine`. Native runs match. +#[cfg(unix)] +pub fn running_under_qemu_user() -> bool { + use std::ffi::CStr; + + // SAFETY: `uname` writes into a zeroed `utsname` and returns 0 on success. + let mut uts = unsafe { std::mem::zeroed::() }; + if unsafe { libc::uname(&mut uts) } != 0 { + return false; + } + let machine_ptr = uts.machine.as_ptr() as *const std::os::raw::c_char; + let host = match unsafe { CStr::from_ptr(machine_ptr) }.to_str() { + Ok(s) => s, + Err(_) => return false, + }; + let target = std::env::consts::ARCH; + !arch_matches(target, host) +} + +#[cfg(not(unix))] +pub fn running_under_qemu_user() -> bool { + false +} + +#[cfg(unix)] +fn arch_matches(target: &str, host: &str) -> bool { + match target { + "x86_64" => host == "x86_64" || host == "amd64", + "x86" => host == "i386" || host == "i686", + "aarch64" => host == "aarch64" || host == "arm64", + "arm" => host.starts_with("arm"), + "riscv64" => host == "riscv64", + "powerpc64" => host == "ppc64" || host == "ppc64le", + "powerpc" => host == "ppc", + "mips" => host == "mips", + "mips64" => host == "mips64", + "s390x" => host == "s390x", + _ => target == host, + } +} + +/// Skip the current test (with an explanatory message) when running under +/// qemu-user. Returns `true` if the caller should bail out early. +#[macro_export] +macro_rules! skip_under_qemu_user { + ($name:expr) => {{ + if $crate::proxy::tuic::test_utils::running_under_qemu_user() { + eprintln!( + "skipping {} under qemu-user emulation (QUIC timing unreliable)", + $name + ); + return Ok(()); + } + }}; +} From 0134dcce15b9860a9fc1c4d12f1e051f6db8aaf6 Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 01:40:41 +0800 Subject: [PATCH 16/22] feat(cli): enable compatibility mode via CLASH_RS_COMPATIBILITY_MODE env For environments where the command line is fixed (containers, init systems, GUI launchers) it's useful to flip on `--compatibility` from outside. Read `CLASH_RS_COMPATIBILITY_MODE` right after Cli::parse_from and treat `1` or `true` (case-insensitive, trimmed) as equivalent to passing `--compatibility`. The CLI flag still takes precedence when already set, and any other value (including `0`, `false`, empty) is a no-op. Add a small `env_truthy` helper with its own unit tests so the accept / reject sets are pinned down. Co-Authored-By: Claude Opus 4.7 --- clash-bin/src/main.rs | 61 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/clash-bin/src/main.rs b/clash-bin/src/main.rs index a0b34b07b..ec825c447 100644 --- a/clash-bin/src/main.rs +++ b/clash-bin/src/main.rs @@ -94,6 +94,14 @@ struct Cli { strict_config: bool, } +/// Returns `true` if the env var is set to `1` or `true` (case-insensitive). +fn env_truthy(name: &str) -> bool { + match std::env::var(name) { + Ok(v) => matches!(v.trim().to_ascii_lowercase().as_str(), "1" | "true"), + Err(_) => false, + } +} + fn main() -> anyhow::Result<()> { #[cfg(feature = "dhat-heap")] let _profiler = dhat::Profiler::new_heap(); @@ -107,7 +115,14 @@ fn main() -> anyhow::Result<()> { _ => arg, }) .collect(); - let cli = Cli::parse_from(args); + let mut cli = Cli::parse_from(args); + + // Allow turning on compatibility mode via env var as well as `--compatibility`. + // Accepts `1` or `true` (case-insensitive). Useful for environments where + // the CLI is fixed (containers, init systems, GUI launchers). + if !cli.compatibility && env_truthy("CLASH_RS_COMPATIBILITY_MODE") { + cli.compatibility = true; + } if cli.version { println!( @@ -237,3 +252,47 @@ fn main() -> anyhow::Result<()> { .inspect_err(|err| eprintln!("Failed to start clash: {err}"))?; Ok(()) } + +#[cfg(test)] +mod env_truthy_tests { + use super::env_truthy; + use std::sync::Mutex; + + const KEY: &str = "CLASH_RS_COMPATIBILITY_MODE_TEST"; + // Cargo runs tests in parallel — serialize env-var mutation within this + // module so the three cases don't observe each other's writes. + static GUARD: Mutex<()> = Mutex::new(()); + + fn with(value: Option<&str>, f: F) { + let _g = GUARD.lock().unwrap_or_else(|e| e.into_inner()); + match value { + Some(v) => unsafe { std::env::set_var(KEY, v) }, + None => unsafe { std::env::remove_var(KEY) }, + } + f(); + unsafe { std::env::remove_var(KEY) }; + } + + #[test] + fn unset_is_false() { + with(None, || assert!(!env_truthy(KEY))); + } + + #[test] + fn accepts_one_and_true_case_insensitive() { + for v in ["1", "true", "TRUE", "True", " true ", " 1 "] { + with(Some(v), || { + assert!(env_truthy(KEY), "{v:?} should be truthy") + }); + } + } + + #[test] + fn rejects_other_values() { + for v in ["0", "false", "yes", "on", "", "2", "truthy"] { + with(Some(v), || { + assert!(!env_truthy(KEY), "{v:?} should be falsy") + }); + } + } +} From 50af0386c8404801757165df588b7b75aadfd410 Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 01:42:25 +0800 Subject: [PATCH 17/22] style: rewrite compatibility-env merge as explicit OR The previous `if !cli.compatibility && env_truthy(...)` form was semantically OR but read at-a-glance like an AND. Replace with the direct expression `cli.compatibility = cli.compatibility || env_truthy(...)` to make the precedence obvious. Behaviour unchanged. Co-Authored-By: Claude Opus 4.7 --- clash-bin/src/main.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/clash-bin/src/main.rs b/clash-bin/src/main.rs index ec825c447..a0c225bc9 100644 --- a/clash-bin/src/main.rs +++ b/clash-bin/src/main.rs @@ -117,12 +117,11 @@ fn main() -> anyhow::Result<()> { .collect(); let mut cli = Cli::parse_from(args); - // Allow turning on compatibility mode via env var as well as `--compatibility`. - // Accepts `1` or `true` (case-insensitive). Useful for environments where - // the CLI is fixed (containers, init systems, GUI launchers). - if !cli.compatibility && env_truthy("CLASH_RS_COMPATIBILITY_MODE") { - cli.compatibility = true; - } + // Either `--compatibility` OR `CLASH_RS_COMPATIBILITY_MODE=1|true` enables + // compatibility mode. The env var is useful when the command line is fixed + // (containers, init systems, GUI launchers). + cli.compatibility = + cli.compatibility || env_truthy("CLASH_RS_COMPATIBILITY_MODE"); if cli.version { println!( From d24c234df30802b5d8c7ce792854dd6df216cc1b Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 01:51:27 +0800 Subject: [PATCH 18/22] Revert "test(api): replace flaky httpbin.yba.dev with a local drip server" Remove DripServer and restore the test's original https://httpbin.yba.dev/drip request, per request. clash-lib/tests/api_tests.rs and clash-lib/tests/common/mod.rs are now identical to master for the two files touched by the drip work. Co-Authored-By: Claude Opus 4.7 --- clash-lib/tests/api_tests.rs | 20 +-------- clash-lib/tests/common/mod.rs | 78 +---------------------------------- 2 files changed, 3 insertions(+), 95 deletions(-) diff --git a/clash-lib/tests/api_tests.rs b/clash-lib/tests/api_tests.rs index 6afb83f87..3554422eb 100644 --- a/clash-lib/tests/api_tests.rs +++ b/clash-lib/tests/api_tests.rs @@ -1,5 +1,3 @@ -#[cfg(feature = "shadowsocks")] -use crate::common::DripServer; use crate::common::{ ClashInstance, alloc_ports, make_client_config_str, send_http_request, wait_port_ready, @@ -290,14 +288,6 @@ async fn test_connections_returns_proxy_chain_names() { // Allocate unique ports for the client (7-port block) let client_base = alloc_ports(CLIENT_PORT_BLOCK); - // Spin up a local HTTP server that delays the response so the connection - // stays open long enough for the /connections poll to observe it. Using a - // local server keeps the test hermetic — no flaky external dependency. - let drip = DripServer::start(Duration::from_secs(3), 500) - .await - .expect("Failed to start drip server"); - let drip_port = drip.port; - // Build server config dynamically from server.yaml let server_config_str = { let tpl = std::fs::read_to_string(wd_server.join("server.yaml")) @@ -307,15 +297,9 @@ async fn test_connections_returns_proxy_chain_names() { }; // Build client config dynamically: template substitution + SS server port - // and swap the upstream-domain rule for one that matches our local drip - // server's destination port, so the `test 🌏` chain still fires. let client_config_str = make_client_config_str(client_base) .replace("127.0.0.1:8901", &format!("127.0.0.1:{}", server_base + 1)) - .replace("port: 8901", &format!("port: {}", server_base + 1)) - .replace( - "DOMAIN,httpbin.yba.dev,test 🌏", - &format!("DST-PORT,{},test 🌏", drip_port), - ); + .replace("port: 8901", &format!("port: {}", server_base + 1)); // Start server instance with RAII guard let _server = ClashInstance::start( @@ -360,7 +344,7 @@ async fn test_connections_returns_proxy_chain_names() { .expect("Failed to build reqwest client"); let response = client - .get(format!("http://127.0.0.1:{}/", drip_port)) + .get("https://httpbin.yba.dev/drip?duration=2&delay=1&numbytes=500") .send() .await .expect("Failed to send request through proxy"); diff --git a/clash-lib/tests/common/mod.rs b/clash-lib/tests/common/mod.rs index 1fdd7d6a5..bb751efa5 100644 --- a/clash-lib/tests/common/mod.rs +++ b/clash-lib/tests/common/mod.rs @@ -1,12 +1,9 @@ -use bytes::Bytes; use futures::TryFutureExt; -use http_body_util::Full; -use hyper::{Response, body::Incoming}; +use hyper::body::Incoming; use hyper_util::rt::TokioIo; use std::{ net::{Shutdown, TcpStream}, sync::atomic::{AtomicU16, Ordering}, - time::Duration, }; /// Backward-compatible wrapper used by integration_tests.rs. @@ -320,76 +317,3 @@ impl Socks5UdpSession { (pkt[pos..].to_vec(), src) } } - -// ── Local "drip" HTTP server ───────────────────────────────────────────────── - -/// RAII guard for a local HTTP server bound to 127.0.0.1 on a random port. -/// Each request blocks for `delay` before returning a body of `num_bytes` of -/// `b'*'`. The blocking keeps the connection alive long enough for the -/// /connections API to observe it. -/// -/// Cancelled on drop. -#[allow(dead_code)] -pub struct DripServer { - pub port: u16, - token: tokio_util::sync::CancellationToken, -} - -#[allow(dead_code)] -impl DripServer { - /// Bind a hyper HTTP/1 server on 127.0.0.1:0 and spawn the accept loop. - /// Returns once the listener is bound — the port is immediately usable. - pub async fn start(delay: Duration, num_bytes: usize) -> std::io::Result { - use hyper::{server::conn::http1, service::service_fn}; - - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await?; - let port = listener.local_addr()?.port(); - let token = tokio_util::sync::CancellationToken::new(); - let cancel = token.clone(); - - tokio::spawn(async move { - loop { - let (stream, _) = tokio::select! { - _ = cancel.cancelled() => break, - accept = listener.accept() => match accept { - Ok(v) => v, - Err(_) => continue, - }, - }; - let io = TokioIo::new(stream); - let conn_cancel = cancel.clone(); - tokio::spawn(async move { - let svc = service_fn(move |_req| { - let token = conn_cancel.clone(); - async move { - tokio::select! { - _ = token.cancelled() => {} - _ = tokio::time::sleep(delay) => {} - } - let body = Full::new(Bytes::from(vec![b'*'; num_bytes])); - Ok::<_, std::convert::Infallible>( - Response::builder() - .status(200) - .header( - "content-type", - "application/octet-stream", - ) - .body(body) - .unwrap(), - ) - } - }); - let _ = http1::Builder::new().serve_connection(io, svc).await; - }); - } - }); - - Ok(Self { port, token }) - } -} - -impl Drop for DripServer { - fn drop(&mut self) { - self.token.cancel(); - } -} From 2062374033f173dd0fc251e4e768f02743abc918 Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 02:08:59 +0800 Subject: [PATCH 19/22] test(tuic): swap qemu-user runtime check for cfg_attr ignore The previous runtime detector compared the binary's compile-time target_arch to \`utsname.machine\`, expecting a mismatch under qemu-user. qemu-user fakes the uname syscall to return the *target* architecture, so the detector silently returned false and the tests still ran (and flaked) on aarch64 / armv7 / riscv64 cross builds. Replace with a much simpler compile-time gate: on non-x86_64 Linux, mark each ping-pong test \`#[ignore]\`. All such targets in CI are cross-built and run under qemu-user; the assumption holds until a native non-x86 Linux runner is added (at which point this can be revisited). Native Linux x86_64, macOS aarch64, and Windows x86_64 continue to cover the tests. Also drop the now-unused running_under_qemu_user / arch_matches / skip_under_qemu_user! macro from test_utils.rs. Co-Authored-By: Claude Opus 4.7 --- clash-lib/src/proxy/tuic/mod.rs | 25 ++++++++-- clash-lib/src/proxy/tuic/test_utils.rs | 64 -------------------------- 2 files changed, 22 insertions(+), 67 deletions(-) diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index 0ec9d33e6..0a296c527 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -484,9 +484,18 @@ mod tests { /// TCP ping-pong test: start an echo server, connect through tuic, send /// "hello" and verify we receive "world" back. + /// + /// Skipped on non-x86_64 Linux because all such targets in CI are + /// cross-built and run under qemu-user, where QUIC timing is unreliable + /// (packets get reordered/dropped enough to race the TUIC idle / request + /// timeouts and reset the relay stream). Native Linux x86_64, macOS + /// aarch64, and Windows x86_64 still cover it. #[tokio::test] + #[cfg_attr( + all(target_os = "linux", not(target_arch = "x86_64")), + ignore = "QUIC under qemu-user (cross test) is unreliable" + )] async fn test_tuic_ping_pong_tcp() -> anyhow::Result<()> { - crate::skip_under_qemu_user!("test_tuic_ping_pong_tcp"); crate::tests::initialize(); let server = TuicServerProcess::start().await?; let port = server.port(); @@ -591,9 +600,14 @@ mod tests { } /// TCP ping-pong over IPv6 loopback. + /// + /// Skipped on non-x86_64 Linux — see `test_tuic_ping_pong_tcp`. #[tokio::test] + #[cfg_attr( + all(target_os = "linux", not(target_arch = "x86_64")), + ignore = "QUIC under qemu-user (cross test) is unreliable" + )] async fn test_tuic_ping_pong_tcp_ipv6() -> anyhow::Result<()> { - crate::skip_under_qemu_user!("test_tuic_ping_pong_tcp_ipv6"); if std::net::UdpSocket::bind("[::1]:0").is_err() { eprintln!("skipping: no IPv6 loopback"); return Ok(()); @@ -646,9 +660,14 @@ mod tests { } /// TCP ping-pong with dual-stack server (client connects via IPv4). + /// + /// Skipped on non-x86_64 Linux — see `test_tuic_ping_pong_tcp`. #[tokio::test] + #[cfg_attr( + all(target_os = "linux", not(target_arch = "x86_64")), + ignore = "QUIC under qemu-user (cross test) is unreliable" + )] async fn test_tuic_ping_pong_tcp_dual_stack() -> anyhow::Result<()> { - crate::skip_under_qemu_user!("test_tuic_ping_pong_tcp_dual_stack"); if std::net::UdpSocket::bind("[::1]:0").is_err() { eprintln!("skipping: no IPv6 loopback"); return Ok(()); diff --git a/clash-lib/src/proxy/tuic/test_utils.rs b/clash-lib/src/proxy/tuic/test_utils.rs index 6bb183451..e6947b3b8 100644 --- a/clash-lib/src/proxy/tuic/test_utils.rs +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -136,67 +136,3 @@ impl Drop for TuicServerProcess { } } } - -/// Detect that the test binary is running under qemu-user emulation (i.e. -/// `cross test` on a non-native target). QUIC under qemu-user is unreliable -/// — packet timing, MTU discovery and timers all drift enough that the TUIC -/// stream ping-pong races against the idle / request timeouts and the stream -/// gets reset mid-relay. We use this to skip the relay tests on cross-built -/// targets while keeping native arch coverage (Linux x86_64, macOS aarch64). -/// -/// The signal: under qemu-user the binary's compile-time `target_arch` differs -/// from the kernel's reported `utsname.machine`. Native runs match. -#[cfg(unix)] -pub fn running_under_qemu_user() -> bool { - use std::ffi::CStr; - - // SAFETY: `uname` writes into a zeroed `utsname` and returns 0 on success. - let mut uts = unsafe { std::mem::zeroed::() }; - if unsafe { libc::uname(&mut uts) } != 0 { - return false; - } - let machine_ptr = uts.machine.as_ptr() as *const std::os::raw::c_char; - let host = match unsafe { CStr::from_ptr(machine_ptr) }.to_str() { - Ok(s) => s, - Err(_) => return false, - }; - let target = std::env::consts::ARCH; - !arch_matches(target, host) -} - -#[cfg(not(unix))] -pub fn running_under_qemu_user() -> bool { - false -} - -#[cfg(unix)] -fn arch_matches(target: &str, host: &str) -> bool { - match target { - "x86_64" => host == "x86_64" || host == "amd64", - "x86" => host == "i386" || host == "i686", - "aarch64" => host == "aarch64" || host == "arm64", - "arm" => host.starts_with("arm"), - "riscv64" => host == "riscv64", - "powerpc64" => host == "ppc64" || host == "ppc64le", - "powerpc" => host == "ppc", - "mips" => host == "mips", - "mips64" => host == "mips64", - "s390x" => host == "s390x", - _ => target == host, - } -} - -/// Skip the current test (with an explanatory message) when running under -/// qemu-user. Returns `true` if the caller should bail out early. -#[macro_export] -macro_rules! skip_under_qemu_user { - ($name:expr) => {{ - if $crate::proxy::tuic::test_utils::running_under_qemu_user() { - eprintln!( - "skipping {} under qemu-user emulation (QUIC timing unreliable)", - $name - ); - return Ok(()); - } - }}; -} From 4061f1197d3fa863036ae4b6c0ee3b93fbf45ba1 Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 02:11:31 +0800 Subject: [PATCH 20/22] test: add likely_qemu_emulated() helper for qemu-user detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A reusable predicate for tests that need to bail out under qemu-user. The rule: \`target_os = "linux"\` AND \`target_arch\` is neither \`x86_64\` nor \`x86\` (i686) — every such target in our CI matrix is produced by \`cross\` and executed under qemu-user, where QUIC and other timing-sensitive paths drift enough to flake. Implemented as a \`pub const fn\` so the value folds at compile time and can be used in \`const\` contexts (e.g. \`const _: () = assert!(...)\`). Pure \`cfg!\` body — no syscalls, no env lookups, no false negatives from qemu's faked \`utsname\`. Add two unit tests: - \`predicate_matches_target_arch\` pins the truth table. - \`usable_in_const_context\` confirms it folds at compile time. Co-Authored-By: Claude Opus 4.7 --- clash-lib/src/proxy/utils/test_utils/mod.rs | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/clash-lib/src/proxy/utils/test_utils/mod.rs b/clash-lib/src/proxy/utils/test_utils/mod.rs index 583323d8c..3b06f206d 100644 --- a/clash-lib/src/proxy/utils/test_utils/mod.rs +++ b/clash-lib/src/proxy/utils/test_utils/mod.rs @@ -5,3 +5,47 @@ pub mod noop; pub mod docker_utils; #[cfg(docker_test)] pub use docker_utils::*; + +/// Returns `true` when this test binary is built for Linux on an architecture +/// other than x86_64 / x86 (i686). In our CI matrix every such target is +/// produced by `cross` and executed under qemu-user — QUIC timing, MTU +/// discovery, and timer granularity all drift enough under emulation that +/// timing-sensitive tests (TUIC ping-pong, hysteria2 handshake, …) become +/// flaky. Gate those tests with `#[cfg_attr(condition, ignore = "…")]` or +/// an early-return on this predicate. +/// +/// The assumption "non-x86 Linux ⇒ qemu" holds as long as we don't add a +/// native aarch64/armv7/riscv64 Linux CI runner; revisit if we do. +/// +/// Evaluated purely from `cfg`, so it's `const` and folds at compile time — +/// the ignored tests are statically excluded on the affected targets. +#[allow(dead_code)] +pub const fn likely_qemu_emulated() -> bool { + cfg!(all( + target_os = "linux", + not(any(target_arch = "x86_64", target_arch = "x86")) + )) +} + +#[cfg(test)] +mod tests { + use super::likely_qemu_emulated; + + #[test] + fn predicate_matches_target_arch() { + // Linux x86_64 / x86 (i686) and every non-Linux target: false. + // Linux on aarch64 / arm / riscv64 / mips / s390x / …: true. + let expected = cfg!(target_os = "linux") + && !cfg!(target_arch = "x86_64") + && !cfg!(target_arch = "x86"); + assert_eq!(likely_qemu_emulated(), expected); + } + + /// Const eval: confirm it folds at compile time and can be used in + /// const contexts. + #[test] + fn usable_in_const_context() { + const VALUE: bool = likely_qemu_emulated(); + assert_eq!(VALUE, likely_qemu_emulated()); + } +} From fbf83972ead0d3d5ba7e9ddc84f4ca9c1272ed7d Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 02:15:30 +0800 Subject: [PATCH 21/22] test: expose likely_qemu_emulated as a rustc-cfg from build.rs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit \`cfg_attr\` only accepts cfg-predicate syntax, not function calls — even a const fn — so the existing helper couldn't shorten the long \`all(target_os = "linux", not(any(target_arch = "x86_64", target_arch = "x86")))\` predicate that the three TUIC ping-pong tests repeated. Have \`build.rs\` emit \`--cfg likely_qemu_emulated\` when the same condition is true. Now tests can write \`#[cfg_attr(likely_qemu_emulated, ignore = "…")]\` directly. Rewrite \`likely_qemu_emulated()\` as \`cfg!(likely_qemu_emulated)\` so the const fn and the cfg flag share a single source of truth (build.rs). Apply the new cfg to all three TUIC tests and update the unit test to assert the build.rs emit matches the inline predicate. Co-Authored-By: Claude Opus 4.7 --- clash-lib/build.rs | 12 ++++++++++ clash-lib/src/proxy/tuic/mod.rs | 6 ++--- clash-lib/src/proxy/utils/test_utils/mod.rs | 25 ++++++++++++--------- 3 files changed, 29 insertions(+), 14 deletions(-) diff --git a/clash-lib/build.rs b/clash-lib/build.rs index e8c8d44c1..442255811 100644 --- a/clash-lib/build.rs +++ b/clash-lib/build.rs @@ -5,6 +5,18 @@ fn main() -> anyhow::Result<()> { println!("cargo::rustc-cfg=docker_test"); } + // Emit `--cfg likely_qemu_emulated` for Linux targets that are neither + // x86_64 nor x86 (i686). Every such target in our CI is `cross`-built + // and runs under qemu-user; QUIC and other timing-sensitive paths flake + // there. Tests can then write `#[cfg_attr(likely_qemu_emulated, + // ignore = "…")]` instead of repeating the long target predicate. + println!("cargo::rustc-check-cfg=cfg(likely_qemu_emulated)"); + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); + if target_os == "linux" && target_arch != "x86_64" && target_arch != "x86" { + println!("cargo::rustc-cfg=likely_qemu_emulated"); + } + build_dashboard()?; println!("cargo:rerun-if-changed=src/common/geodata/geodata.proto"); diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index 0a296c527..f5b3031a9 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -492,7 +492,7 @@ mod tests { /// aarch64, and Windows x86_64 still cover it. #[tokio::test] #[cfg_attr( - all(target_os = "linux", not(target_arch = "x86_64")), + likely_qemu_emulated, ignore = "QUIC under qemu-user (cross test) is unreliable" )] async fn test_tuic_ping_pong_tcp() -> anyhow::Result<()> { @@ -604,7 +604,7 @@ mod tests { /// Skipped on non-x86_64 Linux — see `test_tuic_ping_pong_tcp`. #[tokio::test] #[cfg_attr( - all(target_os = "linux", not(target_arch = "x86_64")), + likely_qemu_emulated, ignore = "QUIC under qemu-user (cross test) is unreliable" )] async fn test_tuic_ping_pong_tcp_ipv6() -> anyhow::Result<()> { @@ -664,7 +664,7 @@ mod tests { /// Skipped on non-x86_64 Linux — see `test_tuic_ping_pong_tcp`. #[tokio::test] #[cfg_attr( - all(target_os = "linux", not(target_arch = "x86_64")), + likely_qemu_emulated, ignore = "QUIC under qemu-user (cross test) is unreliable" )] async fn test_tuic_ping_pong_tcp_dual_stack() -> anyhow::Result<()> { diff --git a/clash-lib/src/proxy/utils/test_utils/mod.rs b/clash-lib/src/proxy/utils/test_utils/mod.rs index 3b06f206d..6f80e5586 100644 --- a/clash-lib/src/proxy/utils/test_utils/mod.rs +++ b/clash-lib/src/proxy/utils/test_utils/mod.rs @@ -17,14 +17,14 @@ pub use docker_utils::*; /// The assumption "non-x86 Linux ⇒ qemu" holds as long as we don't add a /// native aarch64/armv7/riscv64 Linux CI runner; revisit if we do. /// -/// Evaluated purely from `cfg`, so it's `const` and folds at compile time — -/// the ignored tests are statically excluded on the affected targets. +/// Backed by the `likely_qemu_emulated` rustc-cfg set in `build.rs`, so the +/// same flag is available at attribute position: +/// `#[cfg(likely_qemu_emulated)]` / `#[cfg_attr(likely_qemu_emulated, …)]`. +/// Use the const fn for runtime branches; use the cfg flag for compile-time +/// gating (e.g. `ignore`). They stay in sync because both come from build.rs. #[allow(dead_code)] pub const fn likely_qemu_emulated() -> bool { - cfg!(all( - target_os = "linux", - not(any(target_arch = "x86_64", target_arch = "x86")) - )) + cfg!(likely_qemu_emulated) } #[cfg(test)] @@ -32,13 +32,16 @@ mod tests { use super::likely_qemu_emulated; #[test] - fn predicate_matches_target_arch() { - // Linux x86_64 / x86 (i686) and every non-Linux target: false. - // Linux on aarch64 / arm / riscv64 / mips / s390x / …: true. - let expected = cfg!(target_os = "linux") + fn predicate_matches_build_rs_emit() { + // build.rs emits `--cfg likely_qemu_emulated` iff target_os = linux + // AND target_arch is neither x86_64 nor x86 (i686). The const fn must + // agree with that emit so #[cfg(likely_qemu_emulated)] and the runtime + // call can't drift apart. + let expected_from_build = cfg!(target_os = "linux") && !cfg!(target_arch = "x86_64") && !cfg!(target_arch = "x86"); - assert_eq!(likely_qemu_emulated(), expected); + assert_eq!(cfg!(likely_qemu_emulated), expected_from_build); + assert_eq!(likely_qemu_emulated(), expected_from_build); } /// Const eval: confirm it folds at compile time and can be used in From 228520eb6a118496de4b5a3f1bb0cdfd1df32ca7 Mon Sep 17 00:00:00 2001 From: iHsin Date: Fri, 5 Jun 2026 02:19:18 +0800 Subject: [PATCH 22/22] test: rename cfg to qemu_emulated, drop likely_qemu_emulated() const fn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per request: - Shorten the rustc-cfg flag name: \`likely_qemu_emulated\` → \`qemu_emulated\`. - Drop the \`pub const fn likely_qemu_emulated()\` helper. The cfg flag alone covers every call site we have (\`#[cfg_attr(qemu_emulated, …)]\`); the const fn was just an alias. Replace the two helper unit tests with a single sanity check that \`cfg!(qemu_emulated)\` matches build.rs's emit rule, so the flag and the docs can't drift. Also rename the cfg in the three TUIC \`#[cfg_attr]\` sites. Co-Authored-By: Claude Opus 4.7 --- clash-lib/build.rs | 14 +++--- clash-lib/src/proxy/tuic/mod.rs | 6 +-- clash-lib/src/proxy/utils/test_utils/mod.rs | 53 +++++++-------------- 3 files changed, 26 insertions(+), 47 deletions(-) diff --git a/clash-lib/build.rs b/clash-lib/build.rs index 442255811..98db3fce4 100644 --- a/clash-lib/build.rs +++ b/clash-lib/build.rs @@ -5,16 +5,16 @@ fn main() -> anyhow::Result<()> { println!("cargo::rustc-cfg=docker_test"); } - // Emit `--cfg likely_qemu_emulated` for Linux targets that are neither - // x86_64 nor x86 (i686). Every such target in our CI is `cross`-built - // and runs under qemu-user; QUIC and other timing-sensitive paths flake - // there. Tests can then write `#[cfg_attr(likely_qemu_emulated, - // ignore = "…")]` instead of repeating the long target predicate. - println!("cargo::rustc-check-cfg=cfg(likely_qemu_emulated)"); + // Emit `--cfg qemu_emulated` for Linux targets that are neither x86_64 + // nor x86 (i686). Every such target in our CI is `cross`-built and runs + // under qemu-user; QUIC and other timing-sensitive paths flake there. + // Tests can then write `#[cfg_attr(qemu_emulated, ignore = "…")]` + // instead of repeating the long target predicate. + println!("cargo::rustc-check-cfg=cfg(qemu_emulated)"); let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default(); if target_os == "linux" && target_arch != "x86_64" && target_arch != "x86" { - println!("cargo::rustc-cfg=likely_qemu_emulated"); + println!("cargo::rustc-cfg=qemu_emulated"); } build_dashboard()?; diff --git a/clash-lib/src/proxy/tuic/mod.rs b/clash-lib/src/proxy/tuic/mod.rs index f5b3031a9..9f526cd44 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -492,7 +492,7 @@ mod tests { /// aarch64, and Windows x86_64 still cover it. #[tokio::test] #[cfg_attr( - likely_qemu_emulated, + qemu_emulated, ignore = "QUIC under qemu-user (cross test) is unreliable" )] async fn test_tuic_ping_pong_tcp() -> anyhow::Result<()> { @@ -604,7 +604,7 @@ mod tests { /// Skipped on non-x86_64 Linux — see `test_tuic_ping_pong_tcp`. #[tokio::test] #[cfg_attr( - likely_qemu_emulated, + qemu_emulated, ignore = "QUIC under qemu-user (cross test) is unreliable" )] async fn test_tuic_ping_pong_tcp_ipv6() -> anyhow::Result<()> { @@ -664,7 +664,7 @@ mod tests { /// Skipped on non-x86_64 Linux — see `test_tuic_ping_pong_tcp`. #[tokio::test] #[cfg_attr( - likely_qemu_emulated, + qemu_emulated, ignore = "QUIC under qemu-user (cross test) is unreliable" )] async fn test_tuic_ping_pong_tcp_dual_stack() -> anyhow::Result<()> { diff --git a/clash-lib/src/proxy/utils/test_utils/mod.rs b/clash-lib/src/proxy/utils/test_utils/mod.rs index 6f80e5586..403ccead2 100644 --- a/clash-lib/src/proxy/utils/test_utils/mod.rs +++ b/clash-lib/src/proxy/utils/test_utils/mod.rs @@ -6,49 +6,28 @@ pub mod docker_utils; #[cfg(docker_test)] pub use docker_utils::*; -/// Returns `true` when this test binary is built for Linux on an architecture -/// other than x86_64 / x86 (i686). In our CI matrix every such target is -/// produced by `cross` and executed under qemu-user — QUIC timing, MTU -/// discovery, and timer granularity all drift enough under emulation that -/// timing-sensitive tests (TUIC ping-pong, hysteria2 handshake, …) become -/// flaky. Gate those tests with `#[cfg_attr(condition, ignore = "…")]` or -/// an early-return on this predicate. -/// -/// The assumption "non-x86 Linux ⇒ qemu" holds as long as we don't add a -/// native aarch64/armv7/riscv64 Linux CI runner; revisit if we do. -/// -/// Backed by the `likely_qemu_emulated` rustc-cfg set in `build.rs`, so the -/// same flag is available at attribute position: -/// `#[cfg(likely_qemu_emulated)]` / `#[cfg_attr(likely_qemu_emulated, …)]`. -/// Use the const fn for runtime branches; use the cfg flag for compile-time -/// gating (e.g. `ignore`). They stay in sync because both come from build.rs. -#[allow(dead_code)] -pub const fn likely_qemu_emulated() -> bool { - cfg!(likely_qemu_emulated) -} +// `qemu_emulated` rustc-cfg +// ========================= +// Emitted by `build.rs` when the target is Linux on an arch other than +// x86_64 / x86 (i686). In our CI matrix every such target is produced by +// `cross` and executed under qemu-user — QUIC timing, MTU discovery, and +// timer granularity all drift enough under emulation that +// timing-sensitive tests (TUIC ping-pong, hysteria2 handshake, …) flake. +// Gate them with `#[cfg_attr(qemu_emulated, ignore = "…")]`. +// +// The assumption "non-x86 Linux ⇒ qemu" holds as long as we don't add a +// native aarch64/armv7/riscv64 Linux CI runner; revisit if we do. #[cfg(test)] mod tests { - use super::likely_qemu_emulated; - + /// build.rs is the single source of truth for `--cfg qemu_emulated`. + /// Lock its emit rule against the target predicate so the flag and the + /// docs above can't drift apart. #[test] - fn predicate_matches_build_rs_emit() { - // build.rs emits `--cfg likely_qemu_emulated` iff target_os = linux - // AND target_arch is neither x86_64 nor x86 (i686). The const fn must - // agree with that emit so #[cfg(likely_qemu_emulated)] and the runtime - // call can't drift apart. + fn cfg_matches_build_rs_emit_rule() { let expected_from_build = cfg!(target_os = "linux") && !cfg!(target_arch = "x86_64") && !cfg!(target_arch = "x86"); - assert_eq!(cfg!(likely_qemu_emulated), expected_from_build); - assert_eq!(likely_qemu_emulated(), expected_from_build); - } - - /// Const eval: confirm it folds at compile time and can be used in - /// const contexts. - #[test] - fn usable_in_const_context() { - const VALUE: bool = likely_qemu_emulated(); - assert_eq!(VALUE, likely_qemu_emulated()); + assert_eq!(cfg!(qemu_emulated), expected_from_build); } }