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 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-bin/src/main.rs b/clash-bin/src/main.rs index a0b34b07b..a0c225bc9 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,13 @@ fn main() -> anyhow::Result<()> { _ => arg, }) .collect(); - let cli = Cli::parse_from(args); + let mut cli = Cli::parse_from(args); + + // 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!( @@ -237,3 +251,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") + }); + } + } +} 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/build.rs b/clash-lib/build.rs index e8c8d44c1..98db3fce4 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 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=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 2720855da..9f526cd44 100644 --- a/clash-lib/src/proxy/tuic/mod.rs +++ b/clash-lib/src/proxy/tuic/mod.rs @@ -413,89 +413,43 @@ 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}; + + 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, + echo::{TcpEchoConfig, TcpEchoServer}, + noop::NoopResolver, }, }, - tests::initialize, + 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())?; + fn gen_options(port: u16) -> anyhow::Result { + gen_options_with(port, "127.0.0.1", "127.0.0.1") + } - 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_v6(port: u16) -> anyhow::Result { + gen_options_with(port, "::1", "::1") } - fn gen_options( - container_ip: Option, - host_port: u16, - skip_cert_verify: bool, + fn gen_options_with( + port: u16, + server: &str, + ip: &str, ) -> anyhow::Result { - let port = if container_ip.is_some() { - 10002 - } else { - host_port - }; Ok(HandlerOptions { name: "test-tuic".to_owned(), - server: container_ip.unwrap_or(LOCAL_ADDR.to_owned()), + server: server.to_owned(), port, common_opts: Default::default(), uuid: "00000000-0000-0000-0000-000000000001".parse()?, @@ -510,9 +464,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(ip.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, @@ -522,41 +476,242 @@ ip_mode = "auto" }) } + 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. + /// + /// 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] - async fn test_tuic_skip_cert_verify() -> anyhow::Result<()> { - initialize(); - let host_port = alloc_docker_port(); + #[cfg_attr( + qemu_emulated, + ignore = "QUIC under qemu-user (cross test) is unreliable" + )] + async fn test_tuic_ping_pong_tcp() -> anyhow::Result<()> { + crate::tests::initialize(); + let server = TuicServerProcess::start().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 = 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, + 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(()) + } + + /// Verify that connecting with an invalid password fails. + #[tokio::test] + async fn test_tuic_auth_failure() -> anyhow::Result<()> { + crate::tests::initialize(); + let server = TuicServerProcess::start().await?; + let port = server.port(); + + let echo = TcpEchoServer::start_with(TcpEchoConfig { + response: b"world", + expected_request: None, + read_size: 5, + iterations: None, + ..Default::default() + }) + .await?; + let target_port = echo.port(); - let container = get_tuic_runner(host_port).await?; - let opts = gen_options(container.container_ip(), host_port, true)?; + 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; - 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, + country: 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" + ); + } + drop(echo); + Ok(()) } + /// TCP ping-pong over IPv6 loopback. + /// + /// Skipped on non-x86_64 Linux — see `test_tuic_ping_pong_tcp`. #[tokio::test] - async fn test_tuic_cert_verify_expect_fail() -> anyhow::Result<()> { - initialize(); - let host_port = alloc_docker_port(); + #[cfg_attr( + qemu_emulated, + ignore = "QUIC under qemu-user (cross test) is unreliable" + )] + 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(()) + } - let container = get_tuic_runner(host_port).await?; + /// 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( + qemu_emulated, + ignore = "QUIC under qemu-user (cross test) is unreliable" + )] + 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 opts = gen_options(container.container_ip(), host_port, false)?; + 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 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 = 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(()) } } 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..e6947b3b8 --- /dev/null +++ b/clash-lib/src/proxy/tuic/test_utils.rs @@ -0,0 +1,138 @@ +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 { + handle: Option>, + port: u16, +} + +impl TuicServerProcess { + pub async fn start() -> anyhow::Result { + 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: server_bind.parse().unwrap(), + 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() + }, + zero_rtt_handshake: false, + dual_stack, + outbound: tuic_server::config::OutboundConfig { + default: tuic_server::config::OutboundRule { + kind: "direct".into(), + ..Default::default() + }, + named: HashMap::new(), + }, + acl: vec![], + udp_relay_ipv6, + experimental: tuic_server::config::ExperimentalConfig { + drop_loopback: false, + drop_private: false, + }, + ..Default::default() + }; + + 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, + 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; + } + Err(e) => { + tracing::error!("tuic-server init failed: {e}"); + let _ = ready_tx.send(Err(anyhow::anyhow!("{e}"))); + } + } + }); + + 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") + })?; + + tokio::time::timeout(Duration::from_secs(30), ready_rx) + .await + .map_err(|_| { + anyhow::anyhow!( + "tuic-server failed to start on port {port} within 30s" + ) + })? + .map_err(|e| anyhow::anyhow!("tuic-server init failed: {e}"))??; + + 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"); + } + } +} 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 b0aa045ad..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,7 +17,7 @@ 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")] +#[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"; 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()) 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..5efeceab7 --- /dev/null +++ b/clash-lib/src/proxy/utils/test_utils/echo.rs @@ -0,0 +1,101 @@ +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, + pub bind_addr: &'static str, +} + +impl Default for TcpEchoConfig { + fn default() -> Self { + Self { + response: b"world", + expected_request: Some(b"hello"), + read_size: 5, + iterations: Some(10), + bind_addr: "127.0.0.1", + } + } +} + +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 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 { + 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..403ccead2 100644 --- a/clash-lib/src/proxy/utils/test_utils/mod.rs +++ b/clash-lib/src/proxy/utils/test_utils/mod.rs @@ -1,6 +1,33 @@ +pub mod echo; pub mod noop; #[cfg(docker_test)] pub mod docker_utils; #[cfg(docker_test)] pub use docker_utils::*; + +// `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 { + /// 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 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!(qemu_emulated), expected_from_build); + } +}