diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..eea4983 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.github +target +.kilocode +cache +tlsfront +*.tar +*.tar.gz diff --git a/Cargo.lock b/Cargo.lock index a704404..a943322 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -13,6 +23,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -55,6 +79,27 @@ version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea" +[[package]] +name = "arc-swap" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +dependencies = [ + "rustversion", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "asn1-rs" version = "0.5.2" @@ -94,6 +139,17 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -112,6 +168,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "bit-set" version = "0.8.0" @@ -139,6 +201,20 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "blake3" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468ef7d57b3fb7e16b576e8377cdbde2320c60e1491e961d11da40fc4f02a2d" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -163,6 +239,12 @@ version = "3.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +[[package]] +name = "byte_string" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11aade7a05aa8c3a351cedc44c3fc45806430543382fcc4743a9b757a2a0b4ed" + [[package]] name = "bytes" version = "1.11.1" @@ -212,6 +294,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + [[package]] name = "chrono" version = "0.4.43" @@ -261,6 +367,7 @@ checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ "crypto-common", "inout", + "zeroize", ] [[package]] @@ -288,6 +395,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -357,6 +476,12 @@ dependencies = [ "itertools", ] +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + [[package]] name = "crossbeam-channel" version = "0.5.15" @@ -413,6 +538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] @@ -444,6 +570,16 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "der-parser" version = "8.2.0" @@ -489,12 +625,54 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "dynosaur" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12303417f378f29ba12cb12fc78a9df0d8e16ccb1ad94abf04d48d96bdda532" +dependencies = [ + "dynosaur_derive", +] + +[[package]] +name = "dynosaur_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b0713d5c1d52e774c5cd7bb8b043d7c0fc4f921abfb678556140bfbe6ab2364" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -709,6 +887,16 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "h2" version = "0.4.13" @@ -783,6 +971,61 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hickory-proto" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna", + "ipnet", + "once_cell", + "rand", + "ring", + "thiserror 2.0.18", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "hickory-resolver" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a" +dependencies = [ + "cfg-if", + "futures-util", + "hickory-proto", + "ipconfig", + "moka", + "once_cell", + "parking_lot", + "rand", + "resolv-conf", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + [[package]] name = "hmac" version = "0.12.1" @@ -1055,6 +1298,17 @@ dependencies = [ "libc", ] +[[package]] +name = "inotify" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" +dependencies = [ + "bitflags 2.10.0", + "inotify-sys", + "libc", +] + [[package]] name = "inotify-sys" version = "0.1.5" @@ -1074,6 +1328,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1226,6 +1492,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lru_time_cache" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9106e1d747ffd48e6be5bb2d97fa706ed25b144fbee4d5c02eae110cd8d6badd" + [[package]] name = "matchers" version = "0.2.0" @@ -1285,10 +1557,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", + "log", "wasi", "windows-sys 0.61.2", ] +[[package]] +name = "moka" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85f8024e1c8e71c778968af91d43700ce1d11b219d127d79fb2934153b82b42b" +dependencies = [ + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "equivalent", + "parking_lot", + "portable-atomic", + "smallvec", + "tagptr", + "uuid", +] + [[package]] name = "nix" version = "0.28.0" @@ -1322,7 +1612,7 @@ dependencies = [ "crossbeam-channel", "filetime", "fsevent-sys", - "inotify", + "inotify 0.9.6", "kqueue", "libc", "log", @@ -1331,6 +1621,33 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "notify" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" +dependencies = [ + "bitflags 2.10.0", + "fsevent-sys", + "inotify 0.11.1", + "kqueue", + "libc", + "log", + "mio 1.1.1", + "notify-types", + "walkdir", + "windows-sys 0.60.2", +] + +[[package]] +name = "notify-types" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "nu-ansi-term" version = "0.50.3" @@ -1388,6 +1705,10 @@ name = "once_cell" version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +dependencies = [ + "critical-section", + "portable-atomic", +] [[package]] name = "oorandom" @@ -1395,6 +1716,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "parking_lot" version = "0.12.5" @@ -1424,6 +1751,26 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -1436,6 +1783,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "plotters" version = "0.3.7" @@ -1464,6 +1821,35 @@ dependencies = [ "plotters-backend", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1609,7 +1995,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1619,7 +2005,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", ] [[package]] @@ -1637,7 +2032,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" dependencies = [ - "rand_core", + "rand_core 0.9.5", ] [[package]] @@ -1745,6 +2140,12 @@ dependencies = [ "webpki-roots 1.0.6", ] +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + [[package]] name = "ring" version = "0.17.14" @@ -1759,6 +2160,19 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ring-compat" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccce7bae150b815f0811db41b8312fcb74bffa4cab9cee5429ee00f356dd5bd4" +dependencies = [ + "aead", + "ed25519", + "generic-array", + "pkcs8", + "ring", +] + [[package]] name = "rustc-hash" version = "2.1.1" @@ -1870,12 +2284,33 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "semver" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +[[package]] +name = "sendfd" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b183bfd5b1bc64ab0c1ef3ee06b008a9ef1b68a7d3a99ba566fbfe7a7c6d745b" +dependencies = [ + "libc", + "tokio", +] + [[package]] name = "serde" version = "1.0.228" @@ -1962,6 +2397,64 @@ dependencies = [ "digest", ] +[[package]] +name = "shadowsocks" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "482831bf9d55acf3c98e211b6c852c3dfdf1d1b0d23fdf1d887c5a4b2acad4e4" +dependencies = [ + "aes", + "arc-swap", + "base64", + "blake3", + "byte_string", + "bytes", + "cfg-if", + "dynosaur", + "futures", + "hickory-resolver", + "libc", + "log", + "lru_time_cache", + "notify 8.2.0", + "percent-encoding", + "pin-project", + "rand", + "sealed", + "sendfd", + "serde", + "serde_json", + "serde_urlencoded", + "shadowsocks-crypto", + "socket2 0.6.2", + "spin", + "thiserror 2.0.18", + "tokio", + "tokio-tfo", + "trait-variant", + "url", + "windows-sys 0.61.2", +] + +[[package]] +name = "shadowsocks-crypto" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d038a3d17586f1c1ab3c1c3b9e4d5ef8fba98fb3890ad740c8487038b2e2ca5" +dependencies = [ + "aes", + "aes-gcm", + "blake3", + "bytes", + "cfg-if", + "chacha20poly1305", + "hkdf", + "md-5", + "rand", + "ring-compat", + "sha1", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1987,6 +2480,12 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" + [[package]] name = "slab" version = "0.4.12" @@ -2019,6 +2518,25 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2085,9 +2603,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "telemt" -version = "3.3.19" +version = "3.3.20" dependencies = [ "aes", "anyhow", @@ -2113,7 +2637,7 @@ dependencies = [ "lru", "md-5", "nix", - "notify", + "notify 6.1.1", "num-bigint", "num-traits", "parking_lot", @@ -2126,6 +2650,7 @@ dependencies = [ "serde_json", "sha1", "sha2", + "shadowsocks", "socket2 0.5.10", "thiserror 2.0.18", "tokio", @@ -2330,6 +2855,23 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-tfo" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6ad2c3b3bb958ad992354a7ebc468fc0f7cdc9af4997bf4d3fd3cb28bad36dc" +dependencies = [ + "cfg-if", + "futures", + "libc", + "log", + "once_cell", + "pin-project", + "socket2 0.6.2", + "tokio", + "windows-sys 0.60.2", +] + [[package]] name = "tokio-util" version = "0.7.18" @@ -2494,6 +3036,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "trait-variant" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.114", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -2524,6 +3077,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2548,6 +3111,17 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.1", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -2743,6 +3317,12 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + [[package]] name = "winapi-util" version = "0.1.11" @@ -3042,6 +3622,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 788bc2e..ee7134f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ zeroize = { version = "1.8", features = ["derive"] } # Network socket2 = { version = "0.5", features = ["all"] } nix = { version = "0.28", default-features = false, features = ["net"] } +shadowsocks = { version = "1.24", features = ["aead-cipher-2022"] } # Serialization serde = { version = "1.0", features = ["derive"] } diff --git a/docs/API.md b/docs/API.md index 9296aff..a1f0f4f 100644 --- a/docs/API.md +++ b/docs/API.md @@ -497,13 +497,14 @@ Note: the request contract is defined, but the corresponding route currently ret | `direct_total` | `usize` | Direct-route upstream entries. | | `socks4_total` | `usize` | SOCKS4 upstream entries. | | `socks5_total` | `usize` | SOCKS5 upstream entries. | +| `shadowsocks_total` | `usize` | Shadowsocks upstream entries. | #### `RuntimeUpstreamQualityUpstreamData` | Field | Type | Description | | --- | --- | --- | | `upstream_id` | `usize` | Runtime upstream index. | -| `route_kind` | `string` | `direct`, `socks4`, `socks5`. | -| `address` | `string` | Upstream address (`direct` literal for direct route kind). | +| `route_kind` | `string` | `direct`, `socks4`, `socks5`, `shadowsocks`. | +| `address` | `string` | Upstream address (`direct` literal for direct route kind, `host:port` only for proxied upstreams). | | `weight` | `u16` | Selection weight. | | `scopes` | `string` | Configured scope selector. | | `healthy` | `bool` | Current health flag. | @@ -757,13 +758,14 @@ Note: the request contract is defined, but the corresponding route currently ret | `direct_total` | `usize` | Number of direct upstream entries. | | `socks4_total` | `usize` | Number of SOCKS4 upstream entries. | | `socks5_total` | `usize` | Number of SOCKS5 upstream entries. | +| `shadowsocks_total` | `usize` | Number of Shadowsocks upstream entries. | #### `UpstreamStatus` | Field | Type | Description | | --- | --- | --- | | `upstream_id` | `usize` | Runtime upstream index. | -| `route_kind` | `string` | Upstream route kind: `direct`, `socks4`, `socks5`. | -| `address` | `string` | Upstream address (`direct` for direct route kind). Authentication fields are intentionally omitted. | +| `route_kind` | `string` | Upstream route kind: `direct`, `socks4`, `socks5`, `shadowsocks`. | +| `address` | `string` | Upstream address (`direct` for direct route kind, `host:port` for Shadowsocks). Authentication fields are intentionally omitted. | | `weight` | `u16` | Selection weight. | | `scopes` | `string` | Configured scope selector string. | | `healthy` | `bool` | Current health flag. | diff --git a/docs/FAQ.en.md b/docs/FAQ.en.md index 25522ad..4af1c34 100644 --- a/docs/FAQ.en.md +++ b/docs/FAQ.en.md @@ -120,3 +120,17 @@ password = "pass" # Password for Auth on SOCKS-server weight = 1 # Set Weight for Scenarios enabled = true ``` + +#### Shadowsocks as Upstream +Requires `use_middle_proxy = false`. + +```toml +[general] +use_middle_proxy = false + +[[upstreams]] +type = "shadowsocks" +url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@1.2.3.4:8388" +weight = 1 +enabled = true +``` diff --git a/docs/FAQ.ru.md b/docs/FAQ.ru.md index 4353e98..ae38cab 100644 --- a/docs/FAQ.ru.md +++ b/docs/FAQ.ru.md @@ -121,3 +121,16 @@ weight = 1 # Set Weight for Scenarios enabled = true ``` +#### Shadowsocks как Upstream +Требует `use_middle_proxy = false`. + +```toml +[general] +use_middle_proxy = false + +[[upstreams]] +type = "shadowsocks" +url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@1.2.3.4:8388" +weight = 1 +enabled = true +``` diff --git a/docs/TUNING.de.md b/docs/TUNING.de.md index 8c3c950..3b0f31d 100644 --- a/docs/TUNING.de.md +++ b/docs/TUNING.de.md @@ -82,7 +82,7 @@ Die unten angegebenen `Default`-Werte sind Code-Defaults (bei fehlendem Schlüss | Feld | Gilt für | Typ | Pflicht | Default | Bedeutung | |---|---|---|---|---|---| -| `[[upstreams]].type` | alle Upstreams | `"direct" \| "socks4" \| "socks5"` | ja | n/a | Upstream-Transporttyp. | +| `[[upstreams]].type` | alle Upstreams | `"direct" \| "socks4" \| "socks5" \| "shadowsocks"` | ja | n/a | Upstream-Transporttyp. | | `[[upstreams]].weight` | alle Upstreams | `u16` | nein | `1` | Basisgewicht für weighted-random Auswahl. | | `[[upstreams]].enabled` | alle Upstreams | `bool` | nein | `true` | Deaktivierte Einträge werden beim Start ignoriert. | | `[[upstreams]].scopes` | alle Upstreams | `String` | nein | `""` | Komma-separierte Scope-Tags für Request-Routing. | @@ -95,6 +95,8 @@ Die unten angegebenen `Default`-Werte sind Code-Defaults (bei fehlendem Schlüss | `interface` | `socks5` | `Option` | nein | `null` | Wird nur genutzt, wenn `address` als `ip:port` angegeben ist. | | `username` | `socks5` | `Option` | nein | `null` | SOCKS5 Benutzername. | | `password` | `socks5` | `Option` | nein | `null` | SOCKS5 Passwort. | +| `url` | `shadowsocks` | `String` | ja | n/a | Shadowsocks-SIP002-URL (`ss://...`). In Runtime-APIs wird nur `host:port` offengelegt. | +| `interface` | `shadowsocks` | `Option` | nein | `null` | Optionales ausgehendes Bind-Interface oder lokale Literal-IP. | ### Runtime-Regeln (wichtig) @@ -115,6 +117,7 @@ Die unten angegebenen `Default`-Werte sind Code-Defaults (bei fehlendem Schlüss 8. Im ME-Modus wird der gewählte Upstream auch für den ME-TCP-Dial-Pfad verwendet. 9. Im ME-Modus ist bei `direct` mit bind/interface die STUN-Reflection bind-aware für KDF-Adressmaterial. 10. Im ME-Modus werden bei SOCKS-Upstream `BND.ADDR/BND.PORT` für KDF verwendet, wenn gültig/öffentlich und gleiche IP-Familie. +11. `shadowsocks`-Upstreams erfordern `general.use_middle_proxy = false`. Mit aktiviertem ME-Modus schlägt das Laden der Config sofort fehl. ## Upstream-Konfigurationsbeispiele @@ -150,7 +153,20 @@ weight = 2 enabled = true ``` -### Beispiel 4: Gemischte Upstreams mit Scopes +### Beispiel 4: Shadowsocks-Upstream + +```toml +[general] +use_middle_proxy = false + +[[upstreams]] +type = "shadowsocks" +url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@198.51.100.50:8388" +weight = 2 +enabled = true +``` + +### Beispiel 5: Gemischte Upstreams mit Scopes ```toml [[upstreams]] diff --git a/docs/TUNING.en.md b/docs/TUNING.en.md index 1bbc439..6a6a320 100644 --- a/docs/TUNING.en.md +++ b/docs/TUNING.en.md @@ -82,7 +82,7 @@ Defaults below are code defaults (used when a key is omitted), not necessarily v | Field | Applies to | Type | Required | Default | Meaning | |---|---|---|---|---|---| -| `[[upstreams]].type` | all upstreams | `"direct" \| "socks4" \| "socks5"` | yes | n/a | Upstream transport type. | +| `[[upstreams]].type` | all upstreams | `"direct" \| "socks4" \| "socks5" \| "shadowsocks"` | yes | n/a | Upstream transport type. | | `[[upstreams]].weight` | all upstreams | `u16` | no | `1` | Base weight for weighted-random selection. | | `[[upstreams]].enabled` | all upstreams | `bool` | no | `true` | Disabled entries are ignored at startup. | | `[[upstreams]].scopes` | all upstreams | `String` | no | `""` | Comma-separated scope tags for request-level routing. | @@ -95,6 +95,8 @@ Defaults below are code defaults (used when a key is omitted), not necessarily v | `interface` | `socks5` | `Option` | no | `null` | Used only for SOCKS server `ip:port` dial path. | | `username` | `socks5` | `Option` | no | `null` | SOCKS5 username auth. | | `password` | `socks5` | `Option` | no | `null` | SOCKS5 password auth. | +| `url` | `shadowsocks` | `String` | yes | n/a | Shadowsocks SIP002 URL (`ss://...`). Only `host:port` is exposed in runtime APIs. | +| `interface` | `shadowsocks` | `Option` | no | `null` | Optional outgoing bind interface or literal local IP. | ### Runtime rules (important) @@ -115,6 +117,7 @@ Defaults below are code defaults (used when a key is omitted), not necessarily v 8. In ME mode, the selected upstream is also used for ME TCP dial path. 9. In ME mode for `direct` upstream with bind/interface, STUN reflection logic is bind-aware for KDF source material. 10. In ME mode for SOCKS upstream, SOCKS `BND.ADDR/BND.PORT` is used for KDF when it is valid/public for the same family. +11. `shadowsocks` upstreams require `general.use_middle_proxy = false`. Config load fails fast if ME mode is enabled. ## Upstream Configuration Examples @@ -150,7 +153,20 @@ weight = 2 enabled = true ``` -### Example 4: Mixed upstreams with scopes +### Example 4: Shadowsocks upstream + +```toml +[general] +use_middle_proxy = false + +[[upstreams]] +type = "shadowsocks" +url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@198.51.100.50:8388" +weight = 2 +enabled = true +``` + +### Example 5: Mixed upstreams with scopes ```toml [[upstreams]] diff --git a/docs/TUNING.ru.md b/docs/TUNING.ru.md index 6ea4d69..bae8fdd 100644 --- a/docs/TUNING.ru.md +++ b/docs/TUNING.ru.md @@ -82,7 +82,7 @@ | Поле | Применимость | Тип | Обязательно | Default | Назначение | |---|---|---|---|---|---| -| `[[upstreams]].type` | все upstream | `"direct" \| "socks4" \| "socks5"` | да | n/a | Тип upstream транспорта. | +| `[[upstreams]].type` | все upstream | `"direct" \| "socks4" \| "socks5" \| "shadowsocks"` | да | n/a | Тип upstream транспорта. | | `[[upstreams]].weight` | все upstream | `u16` | нет | `1` | Базовый вес в weighted-random выборе. | | `[[upstreams]].enabled` | все upstream | `bool` | нет | `true` | Выключенные записи игнорируются на старте. | | `[[upstreams]].scopes` | все upstream | `String` | нет | `""` | Список scope-токенов через запятую для маршрутизации. | @@ -95,6 +95,8 @@ | `interface` | `socks5` | `Option` | нет | `null` | Используется только если `address` задан как `ip:port`. | | `username` | `socks5` | `Option` | нет | `null` | Логин SOCKS5 auth. | | `password` | `socks5` | `Option` | нет | `null` | Пароль SOCKS5 auth. | +| `url` | `shadowsocks` | `String` | да | n/a | Shadowsocks SIP002 URL (`ss://...`). В runtime API раскрывается только `host:port`. | +| `interface` | `shadowsocks` | `Option` | нет | `null` | Необязательный исходящий bind-интерфейс или literal локальный IP. | ### Runtime-правила @@ -115,6 +117,7 @@ 8. В ME-режиме выбранный upstream также используется для ME TCP dial path. 9. В ME-режиме для `direct` upstream с bind/interface STUN-рефлексия выполняется bind-aware для KDF материала. 10. В ME-режиме для SOCKS upstream используются `BND.ADDR/BND.PORT` для KDF, если адрес валиден/публичен и соответствует IP family. +11. `shadowsocks` upstream требует `general.use_middle_proxy = false`. При включенном ME-режиме конфиг отклоняется при загрузке. ## Примеры конфигурации Upstreams @@ -150,7 +153,20 @@ weight = 2 enabled = true ``` -### Пример 4: смешанные upstream с scopes +### Пример 4: Shadowsocks upstream + +```toml +[general] +use_middle_proxy = false + +[[upstreams]] +type = "shadowsocks" +url = "ss://2022-blake3-aes-256-gcm:BASE64_KEY@198.51.100.50:8388" +weight = 2 +enabled = true +``` + +### Пример 5: смешанные upstream с scopes ```toml [[upstreams]] diff --git a/src/api/model.rs b/src/api/model.rs index ac4e297..5e64d2d 100644 --- a/src/api/model.rs +++ b/src/api/model.rs @@ -134,6 +134,7 @@ pub(super) struct UpstreamSummaryData { pub(super) direct_total: usize, pub(super) socks4_total: usize, pub(super) socks5_total: usize, + pub(super) shadowsocks_total: usize, } #[derive(Serialize, Clone)] diff --git a/src/api/runtime_min.rs b/src/api/runtime_min.rs index f334dd0..b217ad0 100644 --- a/src/api/runtime_min.rs +++ b/src/api/runtime_min.rs @@ -159,6 +159,7 @@ pub(super) struct RuntimeUpstreamQualitySummaryData { pub(super) direct_total: usize, pub(super) socks4_total: usize, pub(super) socks5_total: usize, + pub(super) shadowsocks_total: usize, } #[derive(Serialize)] @@ -406,7 +407,9 @@ pub(super) async fn build_runtime_upstream_quality_data( connect_attempt_total: shared.stats.get_upstream_connect_attempt_total(), connect_success_total: shared.stats.get_upstream_connect_success_total(), connect_fail_total: shared.stats.get_upstream_connect_fail_total(), - connect_failfast_hard_error_total: shared.stats.get_upstream_connect_failfast_hard_error_total(), + connect_failfast_hard_error_total: shared + .stats + .get_upstream_connect_failfast_hard_error_total(), }; let Some(snapshot) = shared.upstream_manager.try_api_snapshot() else { @@ -446,6 +449,7 @@ pub(super) async fn build_runtime_upstream_quality_data( direct_total: snapshot.summary.direct_total, socks4_total: snapshot.summary.socks4_total, socks5_total: snapshot.summary.socks5_total, + shadowsocks_total: snapshot.summary.shadowsocks_total, }), upstreams: Some( snapshot @@ -457,6 +461,7 @@ pub(super) async fn build_runtime_upstream_quality_data( crate::transport::UpstreamRouteKind::Direct => "direct", crate::transport::UpstreamRouteKind::Socks4 => "socks4", crate::transport::UpstreamRouteKind::Socks5 => "socks5", + crate::transport::UpstreamRouteKind::Shadowsocks => "shadowsocks", }, address: upstream.address, weight: upstream.weight, @@ -476,7 +481,9 @@ pub(super) async fn build_runtime_upstream_quality_data( crate::transport::upstream::IpPreference::PreferV6 => "prefer_v6", crate::transport::upstream::IpPreference::PreferV4 => "prefer_v4", crate::transport::upstream::IpPreference::BothWork => "both_work", - crate::transport::upstream::IpPreference::Unavailable => "unavailable", + crate::transport::upstream::IpPreference::Unavailable => { + "unavailable" + } }, }) .collect(), @@ -514,14 +521,18 @@ pub(super) async fn build_runtime_nat_stun_data(shared: &ApiShared) -> RuntimeNa live_total: snapshot.live_servers.len(), }, reflection: RuntimeNatStunReflectionBlockData { - v4: snapshot.reflection_v4.map(|entry| RuntimeNatStunReflectionData { - addr: entry.addr.to_string(), - age_secs: entry.age_secs, - }), - v6: snapshot.reflection_v6.map(|entry| RuntimeNatStunReflectionData { - addr: entry.addr.to_string(), - age_secs: entry.age_secs, - }), + v4: snapshot + .reflection_v4 + .map(|entry| RuntimeNatStunReflectionData { + addr: entry.addr.to_string(), + age_secs: entry.age_secs, + }), + v6: snapshot + .reflection_v6 + .map(|entry| RuntimeNatStunReflectionData { + addr: entry.addr.to_string(), + age_secs: entry.age_secs, + }), }, stun_backoff_remaining_ms: snapshot.stun_backoff_remaining_ms, }), diff --git a/src/api/runtime_selftest.rs b/src/api/runtime_selftest.rs index 0dce3dc..0a3bef6 100644 --- a/src/api/runtime_selftest.rs +++ b/src/api/runtime_selftest.rs @@ -1,5 +1,5 @@ -use std::net::IpAddr; use std::collections::HashMap; +use std::net::IpAddr; use std::sync::{Mutex, OnceLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -7,8 +7,8 @@ use serde::Serialize; use crate::config::{ProxyConfig, UpstreamType}; use crate::network::probe::{detect_interface_ipv4, detect_interface_ipv6, is_bogon}; -use crate::transport::middle_proxy::{bnd_snapshot, timeskew_snapshot, upstream_bnd_snapshots}; use crate::transport::UpstreamRouteKind; +use crate::transport::middle_proxy::{bnd_snapshot, timeskew_snapshot, upstream_bnd_snapshots}; use super::ApiShared; @@ -262,8 +262,8 @@ fn update_kdf_ewma(now_epoch_secs: u64, total_errors: u64) -> f64 { let delta_errors = total_errors.saturating_sub(guard.last_total_errors); let instant_rate_per_min = (delta_errors as f64) * 60.0 / (dt_secs as f64); let alpha = 1.0 - f64::exp(-(dt_secs as f64) / KDF_EWMA_TAU_SECS); - guard.ewma_errors_per_min = guard.ewma_errors_per_min - + alpha * (instant_rate_per_min - guard.ewma_errors_per_min); + guard.ewma_errors_per_min = + guard.ewma_errors_per_min + alpha * (instant_rate_per_min - guard.ewma_errors_per_min); guard.last_epoch_secs = now_epoch_secs; guard.last_total_errors = total_errors; guard.ewma_errors_per_min @@ -284,6 +284,7 @@ fn map_route_kind(value: UpstreamRouteKind) -> &'static str { UpstreamRouteKind::Direct => "direct", UpstreamRouteKind::Socks4 => "socks4", UpstreamRouteKind::Socks5 => "socks5", + UpstreamRouteKind::Shadowsocks => "shadowsocks", } } diff --git a/src/api/runtime_stats.rs b/src/api/runtime_stats.rs index f8948d1..b1bc9e3 100644 --- a/src/api/runtime_stats.rs +++ b/src/api/runtime_stats.rs @@ -2,8 +2,8 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use crate::config::ApiConfig; use crate::stats::Stats; -use crate::transport::upstream::IpPreference; use crate::transport::UpstreamRouteKind; +use crate::transport::upstream::IpPreference; use super::ApiShared; use super::model::{ @@ -138,7 +138,8 @@ fn build_zero_upstream_data(stats: &Stats) -> ZeroUpstreamData { .get_upstream_connect_duration_success_bucket_501_1000ms(), connect_duration_success_bucket_gt_1000ms: stats .get_upstream_connect_duration_success_bucket_gt_1000ms(), - connect_duration_fail_bucket_le_100ms: stats.get_upstream_connect_duration_fail_bucket_le_100ms(), + connect_duration_fail_bucket_le_100ms: stats + .get_upstream_connect_duration_fail_bucket_le_100ms(), connect_duration_fail_bucket_101_500ms: stats .get_upstream_connect_duration_fail_bucket_101_500ms(), connect_duration_fail_bucket_501_1000ms: stats @@ -180,6 +181,7 @@ pub(super) fn build_upstreams_data(shared: &ApiShared, api_cfg: &ApiConfig) -> U direct_total: snapshot.summary.direct_total, socks4_total: snapshot.summary.socks4_total, socks5_total: snapshot.summary.socks5_total, + shadowsocks_total: snapshot.summary.shadowsocks_total, }; let upstreams = snapshot .upstreams @@ -395,8 +397,7 @@ async fn get_minimal_payload_cached( adaptive_floor_min_writers_multi_endpoint: runtime .adaptive_floor_min_writers_multi_endpoint, adaptive_floor_recover_grace_secs: runtime.adaptive_floor_recover_grace_secs, - adaptive_floor_writers_per_core_total: runtime - .adaptive_floor_writers_per_core_total, + adaptive_floor_writers_per_core_total: runtime.adaptive_floor_writers_per_core_total, adaptive_floor_cpu_cores_override: runtime.adaptive_floor_cpu_cores_override, adaptive_floor_max_extra_writers_single_per_core: runtime .adaptive_floor_max_extra_writers_single_per_core, @@ -404,12 +405,9 @@ async fn get_minimal_payload_cached( .adaptive_floor_max_extra_writers_multi_per_core, adaptive_floor_max_active_writers_per_core: runtime .adaptive_floor_max_active_writers_per_core, - adaptive_floor_max_warm_writers_per_core: runtime - .adaptive_floor_max_warm_writers_per_core, - adaptive_floor_max_active_writers_global: runtime - .adaptive_floor_max_active_writers_global, - adaptive_floor_max_warm_writers_global: runtime - .adaptive_floor_max_warm_writers_global, + adaptive_floor_max_warm_writers_per_core: runtime.adaptive_floor_max_warm_writers_per_core, + adaptive_floor_max_active_writers_global: runtime.adaptive_floor_max_active_writers_global, + adaptive_floor_max_warm_writers_global: runtime.adaptive_floor_max_warm_writers_global, adaptive_floor_cpu_cores_detected: runtime.adaptive_floor_cpu_cores_detected, adaptive_floor_cpu_cores_effective: runtime.adaptive_floor_cpu_cores_effective, adaptive_floor_global_cap_raw: runtime.adaptive_floor_global_cap_raw, @@ -527,6 +525,7 @@ fn map_route_kind(value: UpstreamRouteKind) -> &'static str { UpstreamRouteKind::Direct => "direct", UpstreamRouteKind::Socks4 => "socks4", UpstreamRouteKind::Socks5 => "socks5", + UpstreamRouteKind::Shadowsocks => "shadowsocks", } } diff --git a/src/config/load.rs b/src/config/load.rs index 6fcbea3..222069c 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -6,8 +6,9 @@ use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use rand::Rng; +use serde::{Deserialize, Serialize}; +use shadowsocks::config::ServerConfig as ShadowsocksServerConfig; use tracing::warn; -use serde::{Serialize, Deserialize}; use crate::error::{ProxyError, Result}; @@ -122,13 +123,37 @@ fn sanitize_ad_tag(ad_tag: &mut Option) { }; if !is_valid_ad_tag(tag) { - warn!( - "Invalid general.ad_tag value, expected exactly 32 hex chars; ad_tag is disabled" - ); + warn!("Invalid general.ad_tag value, expected exactly 32 hex chars; ad_tag is disabled"); *ad_tag = None; } } +fn validate_upstreams(config: &ProxyConfig) -> Result<()> { + let has_enabled_shadowsocks = config.upstreams.iter().any(|upstream| { + upstream.enabled && matches!(upstream.upstream_type, UpstreamType::Shadowsocks { .. }) + }); + + if has_enabled_shadowsocks && config.general.use_middle_proxy { + return Err(ProxyError::Config( + "shadowsocks upstreams require general.use_middle_proxy = false".to_string(), + )); + } + + for upstream in &config.upstreams { + if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type { + let parsed = ShadowsocksServerConfig::from_url(url) + .map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?; + if parsed.plugin().is_some() { + return Err(ProxyError::Config( + "shadowsocks plugins are not supported".to_string(), + )); + } + } + } + + Ok(()) +} + // ============= Main Config ============= #[derive(Debug, Clone, Serialize, Deserialize, Default)] @@ -180,7 +205,8 @@ impl ProxyConfig { pub(crate) fn load_with_metadata>(path: P) -> Result { let path = path.as_ref(); - let content = std::fs::read_to_string(path).map_err(|e| ProxyError::Config(e.to_string()))?; + let content = + std::fs::read_to_string(path).map_err(|e| ProxyError::Config(e.to_string()))?; let base_dir = path.parent().unwrap_or(Path::new(".")); let mut source_files = BTreeSet::new(); source_files.insert(normalize_config_path(path)); @@ -207,15 +233,17 @@ impl ProxyConfig { .map(|table| table.contains_key("stun_servers")) .unwrap_or(false); - let mut config: ProxyConfig = - parsed_toml.try_into().map_err(|e| ProxyError::Config(e.to_string()))?; + let mut config: ProxyConfig = parsed_toml + .try_into() + .map_err(|e| ProxyError::Config(e.to_string()))?; if !update_every_is_explicit && (legacy_secret_is_explicit || legacy_config_is_explicit) { config.general.update_every = None; } let legacy_nat_stun = config.general.middle_proxy_nat_stun.take(); - let legacy_nat_stun_servers = std::mem::take(&mut config.general.middle_proxy_nat_stun_servers); + let legacy_nat_stun_servers = + std::mem::take(&mut config.general.middle_proxy_nat_stun_servers); let legacy_nat_stun_used = legacy_nat_stun.is_some() || !legacy_nat_stun_servers.is_empty(); if stun_servers_is_explicit { let mut explicit_stun_servers = Vec::new(); @@ -225,7 +253,9 @@ impl ProxyConfig { config.network.stun_servers = explicit_stun_servers; if legacy_nat_stun_used { - warn!("general.middle_proxy_nat_stun and general.middle_proxy_nat_stun_servers are ignored because network.stun_servers is explicitly set"); + warn!( + "general.middle_proxy_nat_stun and general.middle_proxy_nat_stun_servers are ignored because network.stun_servers is explicitly set" + ); } } else { // Keep the default STUN pool unless network.stun_servers is explicitly overridden. @@ -240,7 +270,9 @@ impl ProxyConfig { config.network.stun_servers = unified_stun_servers; if legacy_nat_stun_used { - warn!("general.middle_proxy_nat_stun and general.middle_proxy_nat_stun_servers are deprecated; use network.stun_servers"); + warn!( + "general.middle_proxy_nat_stun and general.middle_proxy_nat_stun_servers are deprecated; use network.stun_servers" + ); } } @@ -372,13 +404,15 @@ impl ProxyConfig { if !(4096..=1024 * 1024).contains(&config.general.direct_relay_copy_buf_c2s_bytes) { return Err(ProxyError::Config( - "general.direct_relay_copy_buf_c2s_bytes must be within [4096, 1048576]".to_string(), + "general.direct_relay_copy_buf_c2s_bytes must be within [4096, 1048576]" + .to_string(), )); } if !(8192..=2 * 1024 * 1024).contains(&config.general.direct_relay_copy_buf_s2c_bytes) { return Err(ProxyError::Config( - "general.direct_relay_copy_buf_s2c_bytes must be within [8192, 2097152]".to_string(), + "general.direct_relay_copy_buf_s2c_bytes must be within [8192, 2097152]" + .to_string(), )); } @@ -617,7 +651,8 @@ impl ProxyConfig { if !(1..=100).contains(&config.general.me_route_backpressure_high_watermark_pct) { return Err(ProxyError::Config( - "general.me_route_backpressure_high_watermark_pct must be within [1, 100]".to_string(), + "general.me_route_backpressure_high_watermark_pct must be within [1, 100]" + .to_string(), )); } @@ -779,11 +814,15 @@ impl ProxyConfig { crate::network::dns_overrides::validate_entries(&config.network.dns_overrides)?; if config.general.use_middle_proxy && config.network.ipv6 == Some(true) { - warn!("IPv6 with Middle Proxy is experimental and may cause KDF address mismatch; consider disabling IPv6 or ME"); + warn!( + "IPv6 with Middle Proxy is experimental and may cause KDF address mismatch; consider disabling IPv6 or ME" + ); } // Random fake_cert_len only when default is in use. - if !config.censorship.tls_emulation && config.censorship.fake_cert_len == default_fake_cert_len() { + if !config.censorship.tls_emulation + && config.censorship.fake_cert_len == default_fake_cert_len() + { config.censorship.fake_cert_len = rand::rng().gen_range(1024..4096); } @@ -793,8 +832,7 @@ impl ProxyConfig { let listen_tcp = config.server.listen_tcp.unwrap_or_else(|| { if config.server.listen_unix_sock.is_some() { // Unix socket present: TCP only if user explicitly set addresses or listeners. - config.server.listen_addr_ipv4.is_some() - || !config.server.listeners.is_empty() + config.server.listen_addr_ipv4.is_some() || !config.server.listeners.is_empty() } else { true } @@ -802,7 +840,9 @@ impl ProxyConfig { // Migration: Populate listeners if empty (skip when listen_tcp = false). if config.server.listeners.is_empty() && listen_tcp { - let ipv4_str = config.server.listen_addr_ipv4 + let ipv4_str = config + .server + .listen_addr_ipv4 .as_deref() .unwrap_or("0.0.0.0"); if let Ok(ipv4) = ipv4_str.parse::() { @@ -844,7 +884,10 @@ impl ProxyConfig { // Migration: Populate upstreams if empty (Default Direct). if config.upstreams.is_empty() { config.upstreams.push(UpstreamConfig { - upstream_type: UpstreamType::Direct { interface: None, bind_addresses: None }, + upstream_type: UpstreamType::Direct { + interface: None, + bind_addresses: None, + }, weight: 1, enabled: true, scopes: String::new(), @@ -858,6 +901,8 @@ impl ProxyConfig { .entry("203".to_string()) .or_insert_with(|| vec!["91.105.192.100:443".to_string()]); + validate_upstreams(&config)?; + Ok(LoadedConfig { config, source_files: source_files.into_iter().collect(), @@ -904,6 +949,9 @@ impl ProxyConfig { mod tests { use super::*; + const TEST_SHADOWSOCKS_URL: &str = + "ss://2022-blake3-aes-256-gcm:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=@127.0.0.1:8388"; + #[test] fn serde_defaults_remain_unchanged_for_present_sections() { let toml = r#" @@ -933,10 +981,7 @@ mod tests { cfg.general.me_init_retry_attempts, default_me_init_retry_attempts() ); - assert_eq!( - cfg.general.me2dc_fallback, - default_me2dc_fallback() - ); + assert_eq!(cfg.general.me2dc_fallback, default_me2dc_fallback()); assert_eq!( cfg.general.proxy_config_v4_cache_path, default_proxy_config_v4_cache_path() @@ -1245,11 +1290,12 @@ mod tests { let path = dir.join("telemt_dc_override_test.toml"); std::fs::write(&path, toml).unwrap(); let cfg = ProxyConfig::load(&path).unwrap(); - assert!(cfg - .dc_overrides - .get("203") - .map(|v| v.contains(&"91.105.192.100:443".to_string())) - .unwrap_or(false)); + assert!( + cfg.dc_overrides + .get("203") + .map(|v| v.contains(&"91.105.192.100:443".to_string())) + .unwrap_or(false) + ); let _ = std::fs::remove_file(path); } @@ -1436,11 +1482,9 @@ mod tests { let path = dir.join("telemt_me_adaptive_floor_min_writers_out_of_range_test.toml"); std::fs::write(&path, toml).unwrap(); let err = ProxyConfig::load(&path).unwrap_err().to_string(); - assert!( - err.contains( - "general.me_adaptive_floor_min_writers_single_endpoint must be within [1, 32]" - ) - ); + assert!(err.contains( + "general.me_adaptive_floor_min_writers_single_endpoint must be within [1, 32]" + )); let _ = std::fs::remove_file(path); } @@ -2026,6 +2070,124 @@ mod tests { let _ = std::fs::remove_file(path); } + #[test] + fn shadowsocks_upstream_url_loads_successfully() { + let toml = format!( + r#" + [general] + use_middle_proxy = false + + [censorship] + tls_domain = "example.com" + + [access.users] + user = "00000000000000000000000000000000" + + [[upstreams]] + type = "shadowsocks" + url = "{url}" + interface = "127.0.0.2" + "#, + url = TEST_SHADOWSOCKS_URL, + ); + let dir = std::env::temp_dir(); + let path = dir.join("telemt_shadowsocks_valid_test.toml"); + std::fs::write(&path, toml).unwrap(); + let cfg = ProxyConfig::load(&path).unwrap(); + + assert!(matches!( + &cfg.upstreams[0].upstream_type, + UpstreamType::Shadowsocks { url, interface } + if url == TEST_SHADOWSOCKS_URL && interface.as_deref() == Some("127.0.0.2") + )); + + let _ = std::fs::remove_file(path); + } + + #[test] + fn shadowsocks_requires_direct_mode() { + let toml = format!( + r#" + [general] + use_middle_proxy = true + + [censorship] + tls_domain = "example.com" + + [access.users] + user = "00000000000000000000000000000000" + + [[upstreams]] + type = "shadowsocks" + url = "{url}" + "#, + url = TEST_SHADOWSOCKS_URL, + ); + let dir = std::env::temp_dir(); + let path = dir.join("telemt_shadowsocks_me_reject_test.toml"); + std::fs::write(&path, toml).unwrap(); + let err = ProxyConfig::load(&path).unwrap_err().to_string(); + + assert!(err.contains("shadowsocks upstreams require general.use_middle_proxy = false")); + + let _ = std::fs::remove_file(path); + } + + #[test] + fn invalid_shadowsocks_url_is_rejected() { + let toml = r#" + [general] + use_middle_proxy = false + + [censorship] + tls_domain = "example.com" + + [access.users] + user = "00000000000000000000000000000000" + + [[upstreams]] + type = "shadowsocks" + url = "not-a-valid-ss-url" + "#; + let dir = std::env::temp_dir(); + let path = dir.join("telemt_shadowsocks_invalid_url_test.toml"); + std::fs::write(&path, toml).unwrap(); + let err = ProxyConfig::load(&path).unwrap_err().to_string(); + + assert!(err.contains("invalid shadowsocks url")); + + let _ = std::fs::remove_file(path); + } + + #[test] + fn shadowsocks_plugins_are_rejected() { + let toml = format!( + r#" + [general] + use_middle_proxy = false + + [censorship] + tls_domain = "example.com" + + [access.users] + user = "00000000000000000000000000000000" + + [[upstreams]] + type = "shadowsocks" + url = "{url}?plugin=obfs-local%3Bobfs%3Dhttp" + "#, + url = TEST_SHADOWSOCKS_URL, + ); + let dir = std::env::temp_dir(); + let path = dir.join("telemt_shadowsocks_plugin_reject_test.toml"); + std::fs::write(&path, toml).unwrap(); + let err = ProxyConfig::load(&path).unwrap_err().to_string(); + + assert!(err.contains("shadowsocks plugins are not supported")); + + let _ = std::fs::remove_file(path); + } + #[test] fn invalid_user_ad_tag_reports_access_user_ad_tags_key() { let toml = r#" diff --git a/src/config/types.rs b/src/config/types.rs index e507044..868ac87 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -936,24 +936,38 @@ impl Default for GeneralConfig { me_reconnect_backoff_cap_ms: default_reconnect_backoff_cap_ms(), me_reconnect_fast_retry_count: default_me_reconnect_fast_retry_count(), me_single_endpoint_shadow_writers: default_me_single_endpoint_shadow_writers(), - me_single_endpoint_outage_mode_enabled: default_me_single_endpoint_outage_mode_enabled(), - me_single_endpoint_outage_disable_quarantine: default_me_single_endpoint_outage_disable_quarantine(), - me_single_endpoint_outage_backoff_min_ms: default_me_single_endpoint_outage_backoff_min_ms(), - me_single_endpoint_outage_backoff_max_ms: default_me_single_endpoint_outage_backoff_max_ms(), - me_single_endpoint_shadow_rotate_every_secs: default_me_single_endpoint_shadow_rotate_every_secs(), + me_single_endpoint_outage_mode_enabled: default_me_single_endpoint_outage_mode_enabled( + ), + me_single_endpoint_outage_disable_quarantine: + default_me_single_endpoint_outage_disable_quarantine(), + me_single_endpoint_outage_backoff_min_ms: + default_me_single_endpoint_outage_backoff_min_ms(), + me_single_endpoint_outage_backoff_max_ms: + default_me_single_endpoint_outage_backoff_max_ms(), + me_single_endpoint_shadow_rotate_every_secs: + default_me_single_endpoint_shadow_rotate_every_secs(), me_floor_mode: MeFloorMode::default(), me_adaptive_floor_idle_secs: default_me_adaptive_floor_idle_secs(), - me_adaptive_floor_min_writers_single_endpoint: default_me_adaptive_floor_min_writers_single_endpoint(), - me_adaptive_floor_min_writers_multi_endpoint: default_me_adaptive_floor_min_writers_multi_endpoint(), + me_adaptive_floor_min_writers_single_endpoint: + default_me_adaptive_floor_min_writers_single_endpoint(), + me_adaptive_floor_min_writers_multi_endpoint: + default_me_adaptive_floor_min_writers_multi_endpoint(), me_adaptive_floor_recover_grace_secs: default_me_adaptive_floor_recover_grace_secs(), - me_adaptive_floor_writers_per_core_total: default_me_adaptive_floor_writers_per_core_total(), + me_adaptive_floor_writers_per_core_total: + default_me_adaptive_floor_writers_per_core_total(), me_adaptive_floor_cpu_cores_override: default_me_adaptive_floor_cpu_cores_override(), - me_adaptive_floor_max_extra_writers_single_per_core: default_me_adaptive_floor_max_extra_writers_single_per_core(), - me_adaptive_floor_max_extra_writers_multi_per_core: default_me_adaptive_floor_max_extra_writers_multi_per_core(), - me_adaptive_floor_max_active_writers_per_core: default_me_adaptive_floor_max_active_writers_per_core(), - me_adaptive_floor_max_warm_writers_per_core: default_me_adaptive_floor_max_warm_writers_per_core(), - me_adaptive_floor_max_active_writers_global: default_me_adaptive_floor_max_active_writers_global(), - me_adaptive_floor_max_warm_writers_global: default_me_adaptive_floor_max_warm_writers_global(), + me_adaptive_floor_max_extra_writers_single_per_core: + default_me_adaptive_floor_max_extra_writers_single_per_core(), + me_adaptive_floor_max_extra_writers_multi_per_core: + default_me_adaptive_floor_max_extra_writers_multi_per_core(), + me_adaptive_floor_max_active_writers_per_core: + default_me_adaptive_floor_max_active_writers_per_core(), + me_adaptive_floor_max_warm_writers_per_core: + default_me_adaptive_floor_max_warm_writers_per_core(), + me_adaptive_floor_max_active_writers_global: + default_me_adaptive_floor_max_active_writers_global(), + me_adaptive_floor_max_warm_writers_global: + default_me_adaptive_floor_max_warm_writers_global(), upstream_connect_retry_attempts: default_upstream_connect_retry_attempts(), upstream_connect_retry_backoff_ms: default_upstream_connect_retry_backoff_ms(), upstream_connect_budget_ms: default_upstream_connect_budget_ms(), @@ -968,7 +982,8 @@ impl Default for GeneralConfig { me_socks_kdf_policy: MeSocksKdfPolicy::Strict, me_route_backpressure_base_timeout_ms: default_me_route_backpressure_base_timeout_ms(), me_route_backpressure_high_timeout_ms: default_me_route_backpressure_high_timeout_ms(), - me_route_backpressure_high_watermark_pct: default_me_route_backpressure_high_watermark_pct(), + me_route_backpressure_high_watermark_pct: + default_me_route_backpressure_high_watermark_pct(), me_health_interval_ms_unhealthy: default_me_health_interval_ms_unhealthy(), me_health_interval_ms_healthy: default_me_health_interval_ms_healthy(), me_admission_poll_ms: default_me_admission_poll_ms(), @@ -992,7 +1007,8 @@ impl Default for GeneralConfig { me_hardswap_warmup_delay_min_ms: default_me_hardswap_warmup_delay_min_ms(), me_hardswap_warmup_delay_max_ms: default_me_hardswap_warmup_delay_max_ms(), me_hardswap_warmup_extra_passes: default_me_hardswap_warmup_extra_passes(), - me_hardswap_warmup_pass_backoff_base_ms: default_me_hardswap_warmup_pass_backoff_base_ms(), + me_hardswap_warmup_pass_backoff_base_ms: + default_me_hardswap_warmup_pass_backoff_base_ms(), me_config_stable_snapshots: default_me_config_stable_snapshots(), me_config_apply_cooldown_secs: default_me_config_apply_cooldown_secs(), me_snapshot_require_http_2xx: default_me_snapshot_require_http_2xx(), @@ -1035,8 +1051,10 @@ impl GeneralConfig { /// Resolve the active updater interval for ME infrastructure refresh tasks. /// `update_every` has priority, otherwise legacy proxy_*_auto_reload_secs are used. pub fn effective_update_every_secs(&self) -> u64 { - self.update_every - .unwrap_or_else(|| self.proxy_secret_auto_reload_secs.min(self.proxy_config_auto_reload_secs)) + self.update_every.unwrap_or_else(|| { + self.proxy_secret_auto_reload_secs + .min(self.proxy_config_auto_reload_secs) + }) } /// Resolve periodic zero-downtime reinit interval for ME writers. @@ -1437,6 +1455,11 @@ pub enum UpstreamType { #[serde(default)] password: Option, }, + Shadowsocks { + url: String, + #[serde(default)] + interface: Option, + }, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1517,7 +1540,10 @@ impl ShowLink { } impl Serialize for ShowLink { - fn serialize(&self, serializer: S) -> std::result::Result { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result { match self { ShowLink::None => Vec::::new().serialize(serializer), ShowLink::All => serializer.serialize_str("*"), @@ -1527,7 +1553,9 @@ impl Serialize for ShowLink { } impl<'de> Deserialize<'de> for ShowLink { - fn deserialize>(deserializer: D) -> std::result::Result { + fn deserialize>( + deserializer: D, + ) -> std::result::Result { use serde::de; struct ShowLinkVisitor; @@ -1543,14 +1571,14 @@ impl<'de> Deserialize<'de> for ShowLink { if v == "*" { Ok(ShowLink::All) } else { - Err(de::Error::invalid_value( - de::Unexpected::Str(v), - &r#""*""#, - )) + Err(de::Error::invalid_value(de::Unexpected::Str(v), &r#""*""#)) } } - fn visit_seq>(self, mut seq: A) -> std::result::Result { + fn visit_seq>( + self, + mut seq: A, + ) -> std::result::Result { let mut names = Vec::new(); while let Some(name) = seq.next_element::()? { names.push(name); diff --git a/src/proxy/direct_relay.rs b/src/proxy/direct_relay.rs index b7a1fbf..108949c 100644 --- a/src/proxy/direct_relay.rs +++ b/src/proxy/direct_relay.rs @@ -3,8 +3,7 @@ use std::io::Write; use std::net::SocketAddr; use std::sync::Arc; -use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; -use tokio::net::TcpStream; +use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf, split}; use tokio::sync::watch; use tracing::{debug, info, warn}; @@ -15,7 +14,7 @@ use crate::protocol::constants::*; use crate::proxy::handshake::{HandshakeSuccess, encrypt_tg_nonce_with_ciphers, generate_tg_nonce}; use crate::proxy::relay::relay_bidirectional; use crate::proxy::route_mode::{ - RelayRouteMode, RouteCutoverState, ROUTE_SWITCH_ERROR_MSG, affected_cutover_state, + ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state, cutover_stagger_delay, }; use crate::proxy::adaptive_buffers; @@ -56,7 +55,11 @@ where ); let tg_stream = upstream_manager - .connect(dc_addr, Some(success.dc_idx), user.strip_prefix("scope_").filter(|s| !s.is_empty())) + .connect( + dc_addr, + Some(success.dc_idx), + user.strip_prefix("scope_").filter(|s| !s.is_empty()), + ) .await?; debug!(peer = %success.peer, dc_addr = %dc_addr, "Connected, performing TG handshake"); @@ -93,11 +96,9 @@ where ); tokio::pin!(relay_result); let relay_result = loop { - if let Some(cutover) = affected_cutover_state( - &route_rx, - RelayRouteMode::Direct, - route_snapshot.generation, - ) { + if let Some(cutover) = + affected_cutover_state(&route_rx, RelayRouteMode::Direct, route_snapshot.generation) + { let delay = cutover_stagger_delay(session_id, cutover.generation); warn!( user = %user, @@ -148,7 +149,9 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result { for addr_str in addrs { match addr_str.parse::() { Ok(addr) => parsed.push(addr), - Err(_) => warn!(dc_idx = dc_idx, addr_str = %addr_str, "Invalid DC override address in config, ignoring"), + Err(_) => { + warn!(dc_idx = dc_idx, addr_str = %addr_str, "Invalid DC override address in config, ignoring") + } } } @@ -170,7 +173,10 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result { // Unknown DC requested by client without override: log and fall back. if !config.dc_overrides.contains_key(&dc_key) { - warn!(dc_idx = dc_idx, "Requested non-standard DC with no override; falling back to default cluster"); + warn!( + dc_idx = dc_idx, + "Requested non-standard DC with no override; falling back to default cluster" + ); if config.general.unknown_dc_file_log_enabled && let Some(path) = &config.general.unknown_dc_log_path && let Ok(handle) = tokio::runtime::Handle::try_current() @@ -204,15 +210,15 @@ fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result { )) } -async fn do_tg_handshake_static( - mut stream: TcpStream, +async fn do_tg_handshake_static( + mut stream: S, success: &HandshakeSuccess, config: &ProxyConfig, rng: &SecureRandom, -) -> Result<( - CryptoReader, - CryptoWriter, -)> { +) -> Result<(CryptoReader>, CryptoWriter>)> +where + S: AsyncRead + AsyncWrite + Unpin, +{ let (nonce, _tg_enc_key, _tg_enc_iv, _tg_dec_key, _tg_dec_iv) = generate_tg_nonce( success.proto_tag, success.dc_idx, @@ -235,7 +241,7 @@ async fn do_tg_handshake_static( stream.write_all(&encrypted_nonce).await?; stream.flush().await?; - let (read_half, write_half) = stream.into_split(); + let (read_half, write_half) = split(stream); let max_pending = config.general.crypto_pending_buffer; Ok(( diff --git a/src/tls_front/fetcher.rs b/src/tls_front/fetcher.rs index 38872af..1ee51a2 100644 --- a/src/tls_front/fetcher.rs +++ b/src/tls_front/fetcher.rs @@ -7,33 +7,29 @@ use tokio::net::TcpStream; #[cfg(unix)] use tokio::net::UnixStream; use tokio::time::timeout; -use tokio_rustls::client::TlsStream; use tokio_rustls::TlsConnector; +use tokio_rustls::client::TlsStream; use tracing::{debug, warn}; -use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::client::ClientConfig; +use rustls::client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}; use rustls::pki_types::{CertificateDer, ServerName, UnixTime}; use rustls::{DigitallySignedStruct, Error as RustlsError}; -use x509_parser::prelude::FromDer; use x509_parser::certificate::X509Certificate; +use x509_parser::prelude::FromDer; use crate::crypto::SecureRandom; use crate::network::dns_overrides::resolve_socket_addr; use crate::protocol::constants::{ TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE, }; -use crate::transport::proxy_protocol::{ProxyProtocolV1Builder, ProxyProtocolV2Builder}; use crate::tls_front::types::{ - ParsedCertificateInfo, - ParsedServerHello, - TlsBehaviorProfile, - TlsCertPayload, - TlsExtension, - TlsFetchResult, - TlsProfileSource, + ParsedCertificateInfo, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsExtension, + TlsFetchResult, TlsProfileSource, }; +use crate::transport::UpstreamStream; +use crate::transport::proxy_protocol::{ProxyProtocolV1Builder, ProxyProtocolV2Builder}; /// No-op verifier: accept any certificate (we only need lengths and metadata). #[derive(Debug)] @@ -144,21 +140,27 @@ fn build_client_hello(sni: &str, rng: &SecureRandom) -> Vec { exts.extend_from_slice(&0x000au16.to_be_bytes()); exts.extend_from_slice(&((2 + groups.len() * 2) as u16).to_be_bytes()); exts.extend_from_slice(&(groups.len() as u16 * 2).to_be_bytes()); - for g in groups { exts.extend_from_slice(&g.to_be_bytes()); } + for g in groups { + exts.extend_from_slice(&g.to_be_bytes()); + } // signature_algorithms let sig_algs: [u16; 4] = [0x0804, 0x0805, 0x0403, 0x0503]; // rsa_pss_rsae_sha256/384, ecdsa_secp256r1_sha256, rsa_pkcs1_sha256 exts.extend_from_slice(&0x000du16.to_be_bytes()); exts.extend_from_slice(&((2 + sig_algs.len() * 2) as u16).to_be_bytes()); exts.extend_from_slice(&(sig_algs.len() as u16 * 2).to_be_bytes()); - for a in sig_algs { exts.extend_from_slice(&a.to_be_bytes()); } + for a in sig_algs { + exts.extend_from_slice(&a.to_be_bytes()); + } // supported_versions (TLS1.3 + TLS1.2) let versions: [u16; 2] = [0x0304, 0x0303]; exts.extend_from_slice(&0x002bu16.to_be_bytes()); exts.extend_from_slice(&((1 + versions.len() * 2) as u16).to_be_bytes()); exts.push((versions.len() * 2) as u8); - for v in versions { exts.extend_from_slice(&v.to_be_bytes()); } + for v in versions { + exts.extend_from_slice(&v.to_be_bytes()); + } // key_share (x25519) let key = gen_key_share(rng); @@ -273,7 +275,10 @@ fn parse_server_hello(body: &[u8]) -> Option { pos += 4; let data = body.get(pos..pos + elen)?.to_vec(); pos += elen; - extensions.push(TlsExtension { ext_type: etype, data }); + extensions.push(TlsExtension { + ext_type: etype, + data, + }); } Some(ParsedServerHello { @@ -394,7 +399,7 @@ async fn connect_tcp_with_upstream( port: u16, connect_timeout: Duration, upstream: Option>, -) -> Result { +) -> Result { if let Some(manager) = upstream { if let Some(addr) = resolve_socket_addr(host, port) { match manager.connect(addr, None, None).await { @@ -408,23 +413,25 @@ async fn connect_tcp_with_upstream( ); } } - } else if let Ok(mut addrs) = tokio::net::lookup_host((host, port)).await { - if let Some(addr) = addrs.find(|a| a.is_ipv4()) { - match manager.connect(addr, None, None).await { - Ok(stream) => return Ok(stream), - Err(e) => { - warn!( - host = %host, - port = port, - error = %e, - "Upstream connect failed, using direct connect" - ); - } + } else if let Ok(mut addrs) = tokio::net::lookup_host((host, port)).await + && let Some(addr) = addrs.find(|a| a.is_ipv4()) + { + match manager.connect(addr, None, None).await { + Ok(stream) => return Ok(stream), + Err(e) => { + warn!( + host = %host, + port = port, + error = %e, + "Upstream connect failed, using direct connect" + ); } } } } - connect_with_dns_override(host, port, connect_timeout).await + Ok(UpstreamStream::Tcp( + connect_with_dns_override(host, port, connect_timeout).await?, + )) } fn encode_tls13_certificate_message(cert_chain_der: &[Vec]) -> Option> { @@ -443,9 +450,7 @@ fn encode_tls13_certificate_message(cert_chain_der: &[Vec]) -> Option { warn!( @@ -616,12 +622,13 @@ where .map(|slice| slice.to_vec()) .unwrap_or_default(); let cert_chain_der: Vec> = certs.iter().map(|c| c.as_ref().to_vec()).collect(); - let cert_payload = encode_tls13_certificate_message(&cert_chain_der).map(|certificate_message| { - TlsCertPayload { - cert_chain_der: cert_chain_der.clone(), - certificate_message, - } - }); + let cert_payload = + encode_tls13_certificate_message(&cert_chain_der).map(|certificate_message| { + TlsCertPayload { + cert_chain_der: cert_chain_der.clone(), + certificate_message, + } + }); let total_cert_len = cert_payload .as_ref() diff --git a/src/transport/middle_proxy/ping.rs b/src/transport/middle_proxy/ping.rs index 2c76592..4432282 100644 --- a/src/transport/middle_proxy/ping.rs +++ b/src/transport/middle_proxy/ping.rs @@ -7,6 +7,7 @@ use tokio::net::UdpSocket; use crate::config::{UpstreamConfig, UpstreamType}; use crate::crypto::SecureRandom; use crate::error::ProxyError; +use crate::transport::shadowsocks::sanitize_shadowsocks_url; use crate::transport::{UpstreamEgressInfo, UpstreamRouteKind}; use super::MePool; @@ -40,7 +41,11 @@ pub fn format_sample_line(sample: &MePingSample) -> String { let sign = if sample.dc >= 0 { "+" } else { "-" }; let addr = format!("{}:{}", sample.addr.ip(), sample.addr.port()); - match (sample.connect_ms, sample.handshake_ms.as_ref(), sample.error.as_ref()) { + match ( + sample.connect_ms, + sample.handshake_ms.as_ref(), + sample.error.as_ref(), + ) { (Some(conn), Some(hs), None) => format!( " {sign} {addr}\tPing: {:.0} ms / RPC: {:.0} ms / OK", conn, hs @@ -121,6 +126,7 @@ fn route_from_egress(egress: Option) -> Option { None => route, }) } + UpstreamRouteKind::Shadowsocks => Some("shadowsocks".to_string()), } } @@ -232,6 +238,9 @@ pub async fn format_me_route( } UpstreamType::Socks4 { address, .. } => format!("socks4://{address}"), UpstreamType::Socks5 { address, .. } => format!("socks5://{address}"), + UpstreamType::Shadowsocks { url, .. } => sanitize_shadowsocks_url(url) + .map(|address| format!("shadowsocks://{address}")) + .unwrap_or_else(|_| "shadowsocks://invalid".to_string()), }; } @@ -254,6 +263,12 @@ pub async fn format_me_route( if has_socks5 { kinds.push("socks5"); } + if enabled_upstreams + .iter() + .any(|u| matches!(u.upstream_type, UpstreamType::Shadowsocks { .. })) + { + kinds.push("shadowsocks"); + } format!("mixed upstreams ({})", kinds.join(", ")) } @@ -335,7 +350,10 @@ pub async fn run_me_ping(pool: &Arc, rng: &SecureRandom) -> Vec { connect_ms = Some(conn_rtt); route = route_from_egress(upstream_egress); - match pool.handshake_only(stream, addr, upstream_egress, rng).await { + match pool + .handshake_only(stream, addr, upstream_egress, rng) + .await + { Ok(hs) => { handshake_ms = Some(hs.handshake_ms); // drop halves to close diff --git a/src/transport/mod.rs b/src/transport/mod.rs index cba5465..fd40105 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -2,6 +2,7 @@ pub mod pool; pub mod proxy_protocol; +pub mod shadowsocks; pub mod socket; pub mod socks; pub mod upstream; @@ -14,5 +15,8 @@ pub use socket::*; #[allow(unused_imports)] pub use socks::*; #[allow(unused_imports)] -pub use upstream::{DcPingResult, StartupPingResult, UpstreamEgressInfo, UpstreamManager, UpstreamRouteKind}; +pub use upstream::{ + DcPingResult, StartupPingResult, UpstreamEgressInfo, UpstreamManager, UpstreamRouteKind, + UpstreamStream, +}; pub mod middle_proxy; diff --git a/src/transport/shadowsocks.rs b/src/transport/shadowsocks.rs new file mode 100644 index 0000000..5211b20 --- /dev/null +++ b/src/transport/shadowsocks.rs @@ -0,0 +1,60 @@ +use std::net::{IpAddr, SocketAddr}; +use std::time::Duration; + +use shadowsocks::{ + ProxyClientStream, + config::{ServerConfig, ServerType}, + context::Context, + net::ConnectOpts, +}; + +use crate::error::{ProxyError, Result}; + +pub(crate) type ShadowsocksStream = ProxyClientStream; + +fn parse_server_config(url: &str, connect_timeout: Duration) -> Result { + let mut config = ServerConfig::from_url(url) + .map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?; + + if config.plugin().is_some() { + return Err(ProxyError::Config( + "shadowsocks plugins are not supported".to_string(), + )); + } + + config.set_timeout(connect_timeout); + Ok(config) +} + +pub(crate) fn sanitize_shadowsocks_url(url: &str) -> Result { + Ok(parse_server_config(url, Duration::from_secs(1))? + .addr() + .to_string()) +} + +fn connect_opts_for_interface(interface: &Option) -> ConnectOpts { + let mut opts = ConnectOpts::default(); + if let Some(interface) = interface { + if let Ok(ip) = interface.parse::() { + opts.bind_local_addr = Some(SocketAddr::new(ip, 0)); + } else { + opts.bind_interface = Some(interface.clone()); + } + } + opts +} + +pub(crate) async fn connect_shadowsocks( + url: &str, + interface: &Option, + target: SocketAddr, + connect_timeout: Duration, +) -> Result { + let config = parse_server_config(url, connect_timeout)?; + let context = Context::new_shared(ServerType::Local); + let opts = connect_opts_for_interface(interface); + + ProxyClientStream::connect_with_opts(context, &config, target, &opts) + .await + .map_err(ProxyError::Io) +} diff --git a/src/transport/upstream.rs b/src/transport/upstream.rs index 8360e1e..b0d82b1 100644 --- a/src/transport/upstream.rs +++ b/src/transport/upstream.rs @@ -4,22 +4,28 @@ #![allow(deprecated)] +use rand::Rng; use std::collections::{BTreeSet, HashMap}; -use std::net::{SocketAddr, IpAddr}; +use std::net::{IpAddr, SocketAddr}; +use std::pin::Pin; use std::sync::Arc; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use std::task::{Context, Poll}; use std::time::Duration; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; use tokio::net::TcpStream; use tokio::sync::RwLock; use tokio::time::Instant; -use rand::Rng; -use tracing::{debug, warn, info, trace}; +use tracing::{debug, info, trace, warn}; use crate::config::{UpstreamConfig, UpstreamType}; -use crate::error::{Result, ProxyError}; +use crate::error::{ProxyError, Result}; use crate::network::dns_overrides::{resolve_socket_addr, split_host_port}; -use crate::protocol::constants::{TG_DATACENTERS_V4, TG_DATACENTERS_V6, TG_DATACENTER_PORT}; +use crate::protocol::constants::{TG_DATACENTER_PORT, TG_DATACENTERS_V4, TG_DATACENTERS_V6}; use crate::stats::Stats; +use crate::transport::shadowsocks::{ + ShadowsocksStream, connect_shadowsocks, sanitize_shadowsocks_url, +}; use crate::transport::socket::{create_outgoing_socket_bound, resolve_interface_ip}; use crate::transport::socks::{connect_socks4, connect_socks5}; @@ -47,7 +53,10 @@ struct LatencyEma { impl LatencyEma { const fn new(alpha: f64) -> Self { - Self { value_ms: None, alpha } + Self { + value_ms: None, + alpha, + } } fn update(&mut self, sample_ms: f64) { @@ -131,11 +140,17 @@ impl UpstreamState { return Some(ms); } - let (sum, count) = self.dc_latency.iter() + let (sum, count) = self + .dc_latency + .iter() .filter_map(|l| l.get()) .fold((0.0, 0u32), |(s, c), v| (s + v, c + 1)); - if count > 0 { Some(sum / count as f64) } else { None } + if count > 0 { + Some(sum / count as f64) + } else { + None + } } } @@ -158,11 +173,78 @@ pub struct StartupPingResult { pub both_available: bool, } +pub enum UpstreamStream { + Tcp(TcpStream), + Shadowsocks(Box), +} + +impl std::fmt::Debug for UpstreamStream { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Tcp(_) => f.write_str("UpstreamStream::Tcp(..)"), + Self::Shadowsocks(_) => f.write_str("UpstreamStream::Shadowsocks(..)"), + } + } +} + +impl UpstreamStream { + pub fn into_tcp(self) -> Result { + match self { + Self::Tcp(stream) => Ok(stream), + Self::Shadowsocks(_) => Err(ProxyError::Config( + "shadowsocks upstreams are not supported when general.use_middle_proxy = true" + .to_string(), + )), + } + } +} + +impl AsyncRead for UpstreamStream { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.get_mut() { + Self::Tcp(stream) => Pin::new(stream).poll_read(cx, buf), + Self::Shadowsocks(stream) => Pin::new(stream.as_mut()).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for UpstreamStream { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.get_mut() { + Self::Tcp(stream) => Pin::new(stream).poll_write(cx, buf), + Self::Shadowsocks(stream) => Pin::new(stream.as_mut()).poll_write(cx, buf), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + Self::Tcp(stream) => Pin::new(stream).poll_flush(cx), + Self::Shadowsocks(stream) => Pin::new(stream.as_mut()).poll_flush(cx), + } + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + Self::Tcp(stream) => Pin::new(stream).poll_shutdown(cx), + Self::Shadowsocks(stream) => Pin::new(stream.as_mut()).poll_shutdown(cx), + } + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UpstreamRouteKind { Direct, Socks4, Socks5, + Shadowsocks, } #[derive(Debug, Clone)] @@ -194,6 +276,7 @@ pub struct UpstreamApiSummarySnapshot { pub direct_total: usize, pub socks4_total: usize, pub socks5_total: usize, + pub shadowsocks_total: usize, } #[derive(Debug, Clone)] @@ -253,7 +336,8 @@ impl UpstreamManager { connect_failfast_hard_errors: bool, stats: Arc, ) -> Self { - let states = configs.into_iter() + let states = configs + .into_iter() .filter(|c| c.enabled) .map(UpstreamState::new) .collect(); @@ -311,20 +395,13 @@ impl UpstreamManager { summary.unhealthy_total += 1; } - let (route_kind, address) = match &upstream.config.upstream_type { - UpstreamType::Direct { .. } => { - summary.direct_total += 1; - (UpstreamRouteKind::Direct, "direct".to_string()) - } - UpstreamType::Socks4 { address, .. } => { - summary.socks4_total += 1; - (UpstreamRouteKind::Socks4, address.clone()) - } - UpstreamType::Socks5 { address, .. } => { - summary.socks5_total += 1; - (UpstreamRouteKind::Socks5, address.clone()) - } - }; + let (route_kind, address) = Self::describe_upstream(&upstream.config.upstream_type); + match route_kind { + UpstreamRouteKind::Direct => summary.direct_total += 1, + UpstreamRouteKind::Socks4 => summary.socks4_total += 1, + UpstreamRouteKind::Socks5 => summary.socks5_total += 1, + UpstreamRouteKind::Shadowsocks => summary.shadowsocks_total += 1, + } let mut dc = Vec::with_capacity(NUM_DCS); for dc_idx in 0..NUM_DCS { @@ -352,6 +429,18 @@ impl UpstreamManager { Some(UpstreamApiSnapshot { summary, upstreams }) } + fn describe_upstream(upstream_type: &UpstreamType) -> (UpstreamRouteKind, String) { + match upstream_type { + UpstreamType::Direct { .. } => (UpstreamRouteKind::Direct, "direct".to_string()), + UpstreamType::Socks4 { address, .. } => (UpstreamRouteKind::Socks4, address.clone()), + UpstreamType::Socks5 { address, .. } => (UpstreamRouteKind::Socks5, address.clone()), + UpstreamType::Shadowsocks { url, .. } => ( + UpstreamRouteKind::Shadowsocks, + sanitize_shadowsocks_url(url).unwrap_or_else(|_| "invalid".to_string()), + ), + } + } + pub fn api_policy_snapshot(&self) -> UpstreamApiPolicySnapshot { UpstreamApiPolicySnapshot { connect_retry_attempts: self.connect_retry_attempts, @@ -539,44 +628,44 @@ impl UpstreamManager { // Scope filter: // If scope is set: only scoped and matched items // If scope is not set: only unscoped items - let filtered_upstreams : Vec = upstreams.iter() + let filtered_upstreams: Vec = upstreams + .iter() .enumerate() .filter(|(_, u)| { - scope.map_or( - u.config.scopes.is_empty(), - |req_scope| { - u.config.scopes - .split(',') - .map(str::trim) - .any(|s| s == req_scope) - } - ) + scope.map_or(u.config.scopes.is_empty(), |req_scope| { + u.config + .scopes + .split(',') + .map(str::trim) + .any(|s| s == req_scope) + }) }) .map(|(i, _)| i) .collect(); // Healthy filter - let healthy: Vec = filtered_upstreams.iter() + let healthy: Vec = filtered_upstreams + .iter() .filter(|&&i| upstreams[i].healthy) .copied() .collect(); if filtered_upstreams.is_empty() { - if Self::should_emit_warn( - self.no_upstreams_warn_epoch_ms.as_ref(), - 5_000, - ) { - warn!(scope = scope, "No upstreams available! Using first (direct?)"); + if Self::should_emit_warn(self.no_upstreams_warn_epoch_ms.as_ref(), 5_000) { + warn!( + scope = scope, + "No upstreams available! Using first (direct?)" + ); } return None; } if healthy.is_empty() { - if Self::should_emit_warn( - self.no_healthy_warn_epoch_ms.as_ref(), - 5_000, - ) { - warn!(scope = scope, "No healthy upstreams available! Using random."); + if Self::should_emit_warn(self.no_healthy_warn_epoch_ms.as_ref(), 5_000) { + warn!( + scope = scope, + "No healthy upstreams available! Using random." + ); } return Some(filtered_upstreams[rand::rng().gen_range(0..filtered_upstreams.len())]); } @@ -585,14 +674,18 @@ impl UpstreamManager { return Some(healthy[0]); } - let weights: Vec<(usize, f64)> = healthy.iter().map(|&i| { - let base = upstreams[i].config.weight as f64; - let latency_factor = upstreams[i].effective_latency(dc_idx) - .map(|ms| if ms > 1.0 { 1000.0 / ms } else { 1000.0 }) - .unwrap_or(1.0); + let weights: Vec<(usize, f64)> = healthy + .iter() + .map(|&i| { + let base = upstreams[i].config.weight as f64; + let latency_factor = upstreams[i] + .effective_latency(dc_idx) + .map(|ms| if ms > 1.0 { 1000.0 / ms } else { 1000.0 }) + .unwrap_or(1.0); - (i, base * latency_factor) - }).collect(); + (i, base * latency_factor) + }) + .collect(); let total: f64 = weights.iter().map(|(_, w)| w).sum(); @@ -620,8 +713,34 @@ impl UpstreamManager { } /// Connect to target through a selected upstream. - pub async fn connect(&self, target: SocketAddr, dc_idx: Option, scope: Option<&str>) -> Result { - let (stream, _) = self.connect_with_details(target, dc_idx, scope).await?; + pub async fn connect( + &self, + target: SocketAddr, + dc_idx: Option, + scope: Option<&str>, + ) -> Result { + let idx = self + .select_upstream(dc_idx, scope) + .await + .ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?; + + let mut upstream = { + let guard = self.upstreams.read().await; + guard[idx].config.clone() + }; + + if let Some(s) = scope { + upstream.selected_scope = s.to_string(); + } + + let bind_rr = { + let guard = self.upstreams.read().await; + guard.get(idx).map(|u| u.bind_rr.clone()) + }; + + let (stream, _) = self + .connect_selected_upstream(idx, upstream, target, dc_idx, bind_rr) + .await?; Ok(stream) } @@ -632,7 +751,9 @@ impl UpstreamManager { dc_idx: Option, scope: Option<&str>, ) -> Result<(TcpStream, UpstreamEgressInfo)> { - let idx = self.select_upstream(dc_idx, scope).await + let idx = self + .select_upstream(dc_idx, scope) + .await .ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?; let mut upstream = { @@ -650,6 +771,20 @@ impl UpstreamManager { guard.get(idx).map(|u| u.bind_rr.clone()) }; + let (stream, egress) = self + .connect_selected_upstream(idx, upstream, target, dc_idx, bind_rr) + .await?; + Ok((stream.into_tcp()?, egress)) + } + + async fn connect_selected_upstream( + &self, + idx: usize, + upstream: UpstreamConfig, + target: SocketAddr, + dc_idx: Option, + bind_rr: Option>, + ) -> Result<(UpstreamStream, UpstreamEgressInfo)> { let connect_started_at = Instant::now(); let mut last_error: Option = None; let mut attempts_used = 0u32; @@ -662,8 +797,8 @@ impl UpstreamManager { break; } let remaining_budget = self.connect_budget.saturating_sub(elapsed); - let attempt_timeout = Duration::from_secs(DIRECT_CONNECT_TIMEOUT_SECS) - .min(remaining_budget); + let attempt_timeout = + Duration::from_secs(DIRECT_CONNECT_TIMEOUT_SECS).min(remaining_budget); if attempt_timeout.is_zero() { last_error = Some(ProxyError::ConnectionTimeout { addr: target.to_string(), @@ -786,9 +921,12 @@ impl UpstreamManager { target: SocketAddr, bind_rr: Option>, connect_timeout: Duration, - ) -> Result<(TcpStream, UpstreamEgressInfo)> { + ) -> Result<(UpstreamStream, UpstreamEgressInfo)> { match &config.upstream_type { - UpstreamType::Direct { interface, bind_addresses } => { + UpstreamType::Direct { + interface, + bind_addresses, + } => { let bind_ip = Self::resolve_bind_address( interface, bind_addresses, @@ -796,9 +934,7 @@ impl UpstreamManager { bind_rr.as_deref(), true, ); - if bind_ip.is_none() - && bind_addresses.as_ref().is_some_and(|v| !v.is_empty()) - { + if bind_ip.is_none() && bind_addresses.as_ref().is_some_and(|v| !v.is_empty()) { return Err(ProxyError::Config(format!( "No valid bind_addresses for target family {target}" ))); @@ -813,8 +949,10 @@ impl UpstreamManager { socket.set_nonblocking(true)?; match socket.connect(&target.into()) { - Ok(()) => {}, - Err(err) if err.raw_os_error() == Some(libc::EINPROGRESS) || err.kind() == std::io::ErrorKind::WouldBlock => {}, + Ok(()) => {} + Err(err) + if err.raw_os_error() == Some(libc::EINPROGRESS) + || err.kind() == std::io::ErrorKind::WouldBlock => {} Err(err) => return Err(ProxyError::Io(err)), } @@ -836,7 +974,7 @@ impl UpstreamManager { let local_addr = stream.local_addr().ok(); Ok(( - stream, + UpstreamStream::Tcp(stream), UpstreamEgressInfo { upstream_id, route_kind: UpstreamRouteKind::Direct, @@ -846,8 +984,12 @@ impl UpstreamManager { socks_proxy_addr: None, }, )) - }, - UpstreamType::Socks4 { address, interface, user_id } => { + } + UpstreamType::Socks4 { + address, + interface, + user_id, + } => { // Try to parse as SocketAddr first (IP:port), otherwise treat as hostname:port let mut stream = if let Ok(proxy_addr) = address.parse::() { // IP:port format - use socket with optional interface binding @@ -863,8 +1005,10 @@ impl UpstreamManager { socket.set_nonblocking(true)?; match socket.connect(&proxy_addr.into()) { - Ok(()) => {}, - Err(err) if err.raw_os_error() == Some(libc::EINPROGRESS) || err.kind() == std::io::ErrorKind::WouldBlock => {}, + Ok(()) => {} + Err(err) + if err.raw_os_error() == Some(libc::EINPROGRESS) + || err.kind() == std::io::ErrorKind::WouldBlock => {} Err(err) => return Err(ProxyError::Io(err)), } @@ -888,14 +1032,16 @@ impl UpstreamManager { // Hostname:port format - use tokio DNS resolution // Note: interface binding is not supported for hostnames if interface.is_some() { - warn!("SOCKS4 interface binding is not supported for hostname addresses, ignoring"); + warn!( + "SOCKS4 interface binding is not supported for hostname addresses, ignoring" + ); } Self::connect_hostname_with_dns_override(address, connect_timeout).await? }; // replace socks user_id with config.selected_scope, if set - let scope: Option<&str> = Some(config.selected_scope.as_str()) - .filter(|s| !s.is_empty()); + let scope: Option<&str> = + Some(config.selected_scope.as_str()).filter(|s| !s.is_empty()); let _user_id: Option<&str> = scope.or(user_id.as_deref()); let bound = match tokio::time::timeout( @@ -915,7 +1061,7 @@ impl UpstreamManager { let local_addr = stream.local_addr().ok(); let socks_proxy_addr = stream.peer_addr().ok(); Ok(( - stream, + UpstreamStream::Tcp(stream), UpstreamEgressInfo { upstream_id, route_kind: UpstreamRouteKind::Socks4, @@ -925,8 +1071,13 @@ impl UpstreamManager { socks_proxy_addr, }, )) - }, - UpstreamType::Socks5 { address, interface, username, password } => { + } + UpstreamType::Socks5 { + address, + interface, + username, + password, + } => { // Try to parse as SocketAddr first (IP:port), otherwise treat as hostname:port let mut stream = if let Ok(proxy_addr) = address.parse::() { // IP:port format - use socket with optional interface binding @@ -942,8 +1093,10 @@ impl UpstreamManager { socket.set_nonblocking(true)?; match socket.connect(&proxy_addr.into()) { - Ok(()) => {}, - Err(err) if err.raw_os_error() == Some(libc::EINPROGRESS) || err.kind() == std::io::ErrorKind::WouldBlock => {}, + Ok(()) => {} + Err(err) + if err.raw_os_error() == Some(libc::EINPROGRESS) + || err.kind() == std::io::ErrorKind::WouldBlock => {} Err(err) => return Err(ProxyError::Io(err)), } @@ -967,15 +1120,17 @@ impl UpstreamManager { // Hostname:port format - use tokio DNS resolution // Note: interface binding is not supported for hostnames if interface.is_some() { - warn!("SOCKS5 interface binding is not supported for hostname addresses, ignoring"); + warn!( + "SOCKS5 interface binding is not supported for hostname addresses, ignoring" + ); } Self::connect_hostname_with_dns_override(address, connect_timeout).await? }; debug!(config = ?config, "Socks5 connection"); // replace socks user:pass with config.selected_scope, if set - let scope: Option<&str> = Some(config.selected_scope.as_str()) - .filter(|s| !s.is_empty()); + let scope: Option<&str> = + Some(config.selected_scope.as_str()).filter(|s| !s.is_empty()); let _username: Option<&str> = scope.or(username.as_deref()); let _password: Option<&str> = scope.or(password.as_deref()); @@ -996,7 +1151,7 @@ impl UpstreamManager { let local_addr = stream.local_addr().ok(); let socks_proxy_addr = stream.peer_addr().ok(); Ok(( - stream, + UpstreamStream::Tcp(stream), UpstreamEgressInfo { upstream_id, route_kind: UpstreamRouteKind::Socks5, @@ -1006,7 +1161,22 @@ impl UpstreamManager { socks_proxy_addr, }, )) - }, + } + UpstreamType::Shadowsocks { url, interface } => { + let stream = connect_shadowsocks(url, interface, target, connect_timeout).await?; + let local_addr = stream.get_ref().local_addr().ok(); + Ok(( + UpstreamStream::Shadowsocks(Box::new(stream)), + UpstreamEgressInfo { + upstream_id, + route_kind: UpstreamRouteKind::Shadowsocks, + local_addr, + direct_bind_ip: None, + socks_bound_addr: None, + socks_proxy_addr: None, + }, + )) + } } } @@ -1023,7 +1193,9 @@ impl UpstreamManager { ) -> Vec { let upstreams: Vec<(usize, UpstreamConfig, Arc)> = { let guard = self.upstreams.read().await; - guard.iter().enumerate() + guard + .iter() + .enumerate() .map(|(i, u)| (i, u.config.clone(), u.bind_rr.clone())) .collect() }; @@ -1051,6 +1223,11 @@ impl UpstreamManager { } UpstreamType::Socks4 { address, .. } => format!("socks4://{}", address), UpstreamType::Socks5 { address, .. } => format!("socks5://{}", address), + UpstreamType::Shadowsocks { url, .. } => { + let address = + sanitize_shadowsocks_url(url).unwrap_or_else(|_| "invalid".to_string()); + format!("shadowsocks://{address}") + } }; let mut v6_results = Vec::with_capacity(NUM_DCS); @@ -1061,8 +1238,14 @@ impl UpstreamManager { let result = tokio::time::timeout( Duration::from_secs(DC_PING_TIMEOUT_SECS), - self.ping_single_dc(*upstream_idx, upstream_config, Some(bind_rr.clone()), addr_v6) - ).await; + self.ping_single_dc( + *upstream_idx, + upstream_config, + Some(bind_rr.clone()), + addr_v6, + ), + ) + .await; let ping_result = match result { Ok(Ok(rtt_ms)) => { @@ -1112,8 +1295,14 @@ impl UpstreamManager { let result = tokio::time::timeout( Duration::from_secs(DC_PING_TIMEOUT_SECS), - self.ping_single_dc(*upstream_idx, upstream_config, Some(bind_rr.clone()), addr_v4) - ).await; + self.ping_single_dc( + *upstream_idx, + upstream_config, + Some(bind_rr.clone()), + addr_v4, + ), + ) + .await; let ping_result = match result { Ok(Ok(rtt_ms)) => { @@ -1162,7 +1351,7 @@ impl UpstreamManager { Err(_) => { warn!(dc = %dc_key, "Invalid dc_overrides key, skipping"); continue; - }, + } _ => continue, }; let dc_idx = dc_num as usize; @@ -1175,8 +1364,14 @@ impl UpstreamManager { } let result = tokio::time::timeout( Duration::from_secs(DC_PING_TIMEOUT_SECS), - self.ping_single_dc(*upstream_idx, upstream_config, Some(bind_rr.clone()), addr) - ).await; + self.ping_single_dc( + *upstream_idx, + upstream_config, + Some(bind_rr.clone()), + addr, + ), + ) + .await; let ping_result = match result { Ok(Ok(rtt_ms)) => DcPingResult { @@ -1205,7 +1400,9 @@ impl UpstreamManager { v4_results.push(ping_result); } } - Err(_) => warn!(dc = %dc_idx, addr = %addr_str, "Invalid dc_overrides address, skipping"), + Err(_) => { + warn!(dc = %dc_idx, addr = %addr_str, "Invalid dc_overrides address, skipping") + } } } } @@ -1381,12 +1578,8 @@ impl UpstreamManager { ipv6_enabled: bool, dc_overrides: HashMap>, ) { - let groups = Self::build_health_check_groups( - prefer_ipv6, - ipv4_enabled, - ipv6_enabled, - &dc_overrides, - ); + let groups = + Self::build_health_check_groups(prefer_ipv6, ipv4_enabled, ipv6_enabled, &dc_overrides); let required_healthy_groups = Self::required_healthy_group_count(groups.len()); let mut endpoint_rotation: HashMap<(usize, i16, bool), usize> = HashMap::new(); @@ -1416,13 +1609,16 @@ impl UpstreamManager { let mut group_ok = false; let mut group_rtt_ms = None; - for (is_primary, endpoints) in [(true, &group.primary), (false, &group.fallback)] { + for (is_primary, endpoints) in + [(true, &group.primary), (false, &group.fallback)] + { if endpoints.is_empty() { continue; } let rotation_key = (i, group.dc_idx, is_primary); - let start_idx = *endpoint_rotation.entry(rotation_key).or_insert(0) % endpoints.len(); + let start_idx = + *endpoint_rotation.entry(rotation_key).or_insert(0) % endpoints.len(); let mut next_idx = (start_idx + 1) % endpoints.len(); for step in 0..endpoints.len() { @@ -1544,8 +1740,7 @@ impl UpstreamManager { return None; } - UpstreamState::dc_array_idx(dc_idx) - .map(|idx| guard[0].dc_ip_pref[idx]) + UpstreamState::dc_array_idx(dc_idx).map(|idx| guard[0].dc_ip_pref[idx]) } /// Get preferred DC address based on config preference @@ -1566,6 +1761,12 @@ impl UpstreamManager { #[cfg(test)] mod tests { use super::*; + use std::sync::Arc; + + use crate::stats::Stats; + + const TEST_SHADOWSOCKS_URL: &str = + "ss://2022-blake3-aes-256-gcm:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=@127.0.0.1:8388"; #[test] fn required_healthy_group_count_applies_three_group_threshold() { @@ -1596,15 +1797,18 @@ mod tests { assert!(dc2.primary.iter().all(|addr| addr.is_ipv6())); assert!(dc2.fallback.iter().all(|addr| addr.is_ipv4())); - assert!(dc2 - .primary - .contains(&"[2001:db8::10]:443".parse::().unwrap())); - assert!(dc2 - .fallback - .contains(&"203.0.113.10:443".parse::().unwrap())); - assert!(dc2 - .fallback - .contains(&"203.0.113.11:443".parse::().unwrap())); + assert!( + dc2.primary + .contains(&"[2001:db8::10]:443".parse::().unwrap()) + ); + assert!( + dc2.fallback + .contains(&"203.0.113.10:443".parse::().unwrap()) + ); + assert!( + dc2.fallback + .contains(&"203.0.113.11:443".parse::().unwrap()) + ); } #[test] @@ -1626,12 +1830,14 @@ mod tests { .expect("override-only dc group must be present"); assert_eq!(dc9.primary.len(), 2); - assert!(dc9 - .primary - .contains(&"198.51.100.1:443".parse::().unwrap())); - assert!(dc9 - .primary - .contains(&"198.51.100.2:443".parse::().unwrap())); + assert!( + dc9.primary + .contains(&"198.51.100.1:443".parse::().unwrap()) + ); + assert!( + dc9.primary + .contains(&"198.51.100.2:443".parse::().unwrap()) + ); assert!(dc9.fallback.is_empty()); } @@ -1678,4 +1884,36 @@ mod tests { assert_eq!(bind, None); } + + #[test] + fn api_snapshot_reports_shadowsocks_as_sanitized_route() { + let manager = UpstreamManager::new( + vec![UpstreamConfig { + upstream_type: UpstreamType::Shadowsocks { + url: TEST_SHADOWSOCKS_URL.to_string(), + interface: None, + }, + weight: 2, + enabled: true, + scopes: String::new(), + selected_scope: String::new(), + }], + 1, + 100, + 1000, + 1, + false, + Arc::new(Stats::new()), + ); + + let snapshot = manager.try_api_snapshot().expect("snapshot"); + assert_eq!(snapshot.summary.configured_total, 1); + assert_eq!(snapshot.summary.shadowsocks_total, 1); + assert_eq!(snapshot.upstreams.len(), 1); + assert_eq!( + snapshot.upstreams[0].route_kind, + UpstreamRouteKind::Shadowsocks + ); + assert_eq!(snapshot.upstreams[0].address, "127.0.0.1:8388"); + } }