Compare commits

..

26 Commits

Author SHA1 Message Date
Alexey
b720906fbc Merge pull request #813 from telemt/flow
Flow
2026-05-29 16:50:37 +03:00
Alexey
ac244962ed Merge branch 'main' into flow 2026-05-29 16:07:29 +03:00
Aleksei K
752a2f5012 Bump -> 3.4.13 2026-05-29 14:05:19 +03:00
Aleksei K
a77aedfd7a Atomically claim pressure eviction budget in MR 2026-05-29 13:17:47 +03:00
Alexey
8575d0ee5d Merge pull request #809 from Dimasssss/install.sh
Add interactive prompt for server port during installation
2026-05-29 10:38:00 +03:00
Alexey
213aba5dc9 Update README.md 2026-05-29 08:54:03 +03:00
Aleksei K
a79aaee166 Merge branch 'flow' of https://github.com/telemt/telemt into flow 2026-05-28 16:11:27 +03:00
Aleksei K
2a0fcd6e35 Align ServerHello cipher and opaque ALPN behavior in TLS-F 2026-05-28 16:11:25 +03:00
Dimasssss
54a53e9ff0 Update install.sh 2026-05-28 12:26:46 +03:00
Alexey
63bcd7b3d0 Merge pull request #780 from temandroid/main
fix(docker): mount config as directory to allow atomic API writes
2026-05-27 08:33:43 +03:00
Alexey
b68b10790c Merge pull request #799 from groozchique/main
[docs] more CONFIG_PARAMS.md fixes
2026-05-27 08:30:33 +03:00
Alexey
383d4318fe Add logging configuration to docker-compose: merge pull request #804 from Iv/main
Add logging configuration to docker-compose
2026-05-27 08:30:00 +03:00
Kravchenko Ivan
d293861351 Add logging configuration to docker-compose
There was a disk full problem.
2026-05-26 20:43:26 +03:00
Alexey
31da0a1356 Fixes for Disable Colors 2026-05-26 12:20:28 +03:00
Nick Parfyonov
34bc1d943a [docs] add Hot-Reload column back to [general]
Re-adding Hot-Reload column to the [general] which was removed by mistake in my previous changes
2026-05-25 10:19:01 +03:00
Nick Parfyonov
50dee40dd2 [docs] remove duplicated parameters in [censorship] 2026-05-25 10:16:11 +03:00
Alexey
d4adf0ef9a ME: Bound writer queue waits under backpressure 2026-05-25 00:28:29 +03:00
Alexey
dc8951eae8 Reduce MR + ME Routing hot-path contention 2026-05-22 20:19:09 +03:00
Alexey
77a7f89075 Reuse ME reader scratch buffer across read loop iterations 2026-05-22 19:56:38 +03:00
Alexey
31b9504464 Merge pull request #797 from groozchique/main
Remove duplicated config params in [general] + some clarifications in FAQ + fix grammar mistake in RU FAQ
2026-05-22 19:23:40 +03:00
Nick Parfyonov
54cb4d0f29 [docs] some clarification in client-to-DC section in FAQ
Made some clarification about MTProxy requiremnt to reach all DCs to work properly for every client
2026-05-22 18:20:37 +03:00
Nick Parfyonov
d449fc080c [docs] fix grammar mistake in FAQ.ru.md 2026-05-22 18:15:49 +03:00
Nick Parfyonov
3b8d16bee5 [docs] remove duplicated parameters in CONFIG_PARAMS.md 2026-05-22 18:14:10 +03:00
Alexey
9abaf9006c Prioritize Cancellation in MP select paths 2026-05-22 16:47:54 +03:00
TEMAndroid
855c5eef8b Merge branch 'main' into main 2026-05-18 10:41:24 +03:00
TEMAndroid
b175927324 fix(docker): mount config as directory to allow atomic API writes 2026-05-11 20:50:31 +00:00
34 changed files with 1305 additions and 1074 deletions

379
Cargo.lock generated
View File

@@ -111,9 +111,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]] [[package]]
name = "asn1-rs" name = "asn1-rs"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56624a96882bb8c26d61312ae18cb45868e5a9992ea73c58e45c3101e56a1e60" checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8"
dependencies = [ dependencies = [
"asn1-rs-derive", "asn1-rs-derive",
"asn1-rs-impl", "asn1-rs-impl",
@@ -121,7 +121,7 @@ dependencies = [
"nom", "nom",
"num-traits", "num-traits",
"rusticata-macros", "rusticata-macros",
"thiserror 2.0.18", "thiserror",
"time", "time",
] ]
@@ -167,15 +167,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
version = "1.5.0" version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]] [[package]]
name = "aws-lc-rs" name = "aws-lc-rs"
version = "1.16.3" version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
dependencies = [ dependencies = [
"aws-lc-sys", "aws-lc-sys",
"zeroize", "zeroize",
@@ -183,9 +183,9 @@ dependencies = [
[[package]] [[package]]
name = "aws-lc-sys" name = "aws-lc-sys"
version = "0.40.0" version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
dependencies = [ dependencies = [
"cc", "cc",
"cmake", "cmake",
@@ -220,12 +220,6 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "2.11.1" version = "2.11.1"
@@ -234,9 +228,9 @@ checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
[[package]] [[package]]
name = "blake3" name = "blake3"
version = "1.8.4" version = "1.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce"
dependencies = [ dependencies = [
"arrayref", "arrayref",
"arrayvec", "arrayvec",
@@ -266,9 +260,9 @@ dependencies = [
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.20.2" version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]] [[package]]
name = "byte_string" name = "byte_string"
@@ -299,9 +293,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.60" version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@@ -309,12 +303,6 @@ dependencies = [
"shlex", "shlex",
] ]
[[package]]
name = "cesu8"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@@ -660,9 +648,9 @@ dependencies = [
[[package]] [[package]]
name = "dashmap" name = "dashmap"
version = "6.1.0" version = "6.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" checksum = "e6361d5c062261c78a176addb82d4c821ae42bed6089de0e12603cd25de2059c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"crossbeam-utils", "crossbeam-utils",
@@ -674,9 +662,9 @@ dependencies = [
[[package]] [[package]]
name = "data-encoding" name = "data-encoding"
version = "2.10.0" version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
[[package]] [[package]]
name = "der" name = "der"
@@ -724,9 +712,9 @@ dependencies = [
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -771,9 +759,9 @@ dependencies = [
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
[[package]] [[package]]
name = "enum-as-inner" name = "enum-as-inner"
@@ -1014,9 +1002,9 @@ dependencies = [
[[package]] [[package]]
name = "h2" name = "h2"
version = "0.4.13" version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -1070,9 +1058,9 @@ dependencies = [
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.17.0" version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]] [[package]]
name = "heck" name = "heck"
@@ -1104,7 +1092,7 @@ dependencies = [
"once_cell", "once_cell",
"rand 0.9.4", "rand 0.9.4",
"ring", "ring",
"thiserror 2.0.18", "thiserror",
"tinyvec", "tinyvec",
"tokio", "tokio",
"tracing", "tracing",
@@ -1127,7 +1115,7 @@ dependencies = [
"rand 0.9.4", "rand 0.9.4",
"resolv-conf", "resolv-conf",
"smallvec", "smallvec",
"thiserror 2.0.18", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
] ]
@@ -1152,9 +1140,9 @@ dependencies = [
[[package]] [[package]]
name = "http" name = "http"
version = "1.4.0" version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
dependencies = [ dependencies = [
"bytes", "bytes",
"itoa", "itoa",
@@ -1197,9 +1185,9 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
[[package]] [[package]]
name = "hyper" name = "hyper"
version = "1.9.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc"
dependencies = [ dependencies = [
"atomic-waker", "atomic-waker",
"bytes", "bytes",
@@ -1380,9 +1368,9 @@ dependencies = [
[[package]] [[package]]
name = "idna_adapter" name = "idna_adapter"
version = "1.2.1" version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [ dependencies = [
"icu_normalizer", "icu_normalizer",
"icu_properties", "icu_properties",
@@ -1395,7 +1383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.17.0", "hashbrown 0.17.1",
"serde", "serde",
"serde_core", "serde_core",
] ]
@@ -1406,7 +1394,7 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199" checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"inotify-sys", "inotify-sys",
"libc", "libc",
] ]
@@ -1458,16 +1446,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "iri-string"
version = "0.7.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.13.0"
@@ -1485,27 +1463,32 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]] [[package]]
name = "jni" name = "jni"
version = "0.21.1" version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [ dependencies = [
"cesu8",
"cfg-if", "cfg-if",
"combine", "combine",
"jni-sys 0.3.1", "jni-macros",
"jni-sys",
"log", "log",
"thiserror 1.0.69", "simd_cesu8",
"thiserror",
"walkdir", "walkdir",
"windows-sys 0.45.0", "windows-link",
] ]
[[package]] [[package]]
name = "jni-sys" name = "jni-macros"
version = "0.3.1" version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [ dependencies = [
"jni-sys 0.4.1", "proc-macro2",
"quote",
"rustc_version",
"simd_cesu8",
"syn",
] ]
[[package]] [[package]]
@@ -1539,9 +1522,9 @@ dependencies = [
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.95" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"futures-util", "futures-util",
@@ -1561,11 +1544,11 @@ dependencies = [
[[package]] [[package]]
name = "kqueue-sys" name = "kqueue-sys"
version = "1.0.4" version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" checksum = "07293a4e297ac234359b510362495713f75ea345d5307140414f20c69ffeb087"
dependencies = [ dependencies = [
"bitflags 1.3.2", "bitflags",
"libc", "libc",
] ]
@@ -1583,9 +1566,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.185" version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
@@ -1610,9 +1593,9 @@ dependencies = [
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.29" version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]] [[package]]
name = "lru" name = "lru"
@@ -1656,9 +1639,9 @@ dependencies = [
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]] [[package]]
name = "memoffset" name = "memoffset"
@@ -1677,9 +1660,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
[[package]] [[package]]
name = "mio" name = "mio"
version = "1.2.0" version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -1706,11 +1689,11 @@ dependencies = [
[[package]] [[package]]
name = "nix" name = "nix"
version = "0.31.2" version = "0.31.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3" checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"cfg-if", "cfg-if",
"cfg_aliases", "cfg_aliases",
"libc", "libc",
@@ -1733,7 +1716,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3" checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"fsevent-sys", "fsevent-sys",
"inotify", "inotify",
"kqueue", "kqueue",
@@ -1751,7 +1734,7 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a" checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
] ]
[[package]] [[package]]
@@ -1775,9 +1758,9 @@ dependencies = [
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.1" version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]] [[package]]
name = "num-integer" name = "num-integer"
@@ -1875,18 +1858,18 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.11" version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.11" version = "1.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2017,7 +2000,7 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [ dependencies = [
"bit-set", "bit-set",
"bit-vec", "bit-vec",
"bitflags 2.11.1", "bitflags",
"num-traits", "num-traits",
"rand 0.9.4", "rand 0.9.4",
"rand_chacha", "rand_chacha",
@@ -2048,7 +2031,7 @@ dependencies = [
"rustc-hash", "rustc-hash",
"rustls", "rustls",
"socket2", "socket2",
"thiserror 2.0.18", "thiserror",
"tokio", "tokio",
"tracing", "tracing",
"web-time", "web-time",
@@ -2070,7 +2053,7 @@ dependencies = [
"rustls", "rustls",
"rustls-pki-types", "rustls-pki-types",
"slab", "slab",
"thiserror 2.0.18", "thiserror",
"tinyvec", "tinyvec",
"tracing", "tracing",
"web-time", "web-time",
@@ -2201,7 +2184,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
] ]
[[package]] [[package]]
@@ -2235,9 +2218,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.13.2" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [ dependencies = [
"base64", "base64",
"bytes", "bytes",
@@ -2331,7 +2314,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
@@ -2340,9 +2323,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.23.38" version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [ dependencies = [
"aws-lc-rs", "aws-lc-rs",
"once_cell", "once_cell",
@@ -2367,9 +2350,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-pki-types" name = "rustls-pki-types"
version = "1.14.0" version = "1.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
dependencies = [ dependencies = [
"web-time", "web-time",
"zeroize", "zeroize",
@@ -2377,9 +2360,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-platform-verifier" name = "rustls-platform-verifier"
version = "0.6.2" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [ dependencies = [
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
@@ -2479,7 +2462,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -2544,9 +2527,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.149" version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [ dependencies = [
"itoa", "itoa",
"memchr", "memchr",
@@ -2629,7 +2612,7 @@ dependencies = [
"shadowsocks-crypto", "shadowsocks-crypto",
"socket2", "socket2",
"spin", "spin",
"thiserror 2.0.18", "thiserror",
"tokio", "tokio",
"tokio-tfo", "tokio-tfo",
"trait-variant", "trait-variant",
@@ -2667,9 +2650,9 @@ dependencies = [
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.3.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
@@ -2687,6 +2670,22 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@@ -2701,9 +2700,9 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]] [[package]]
name = "socket2" name = "socket2"
version = "0.6.3" version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.61.2", "windows-sys 0.61.2",
@@ -2791,7 +2790,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]] [[package]]
name = "telemt" name = "telemt"
version = "3.4.12" version = "3.4.13"
dependencies = [ dependencies = [
"aes", "aes",
"anyhow", "anyhow",
@@ -2835,7 +2834,7 @@ dependencies = [
"socket2", "socket2",
"static_assertions", "static_assertions",
"subtle", "subtle",
"thiserror 2.0.18", "thiserror",
"tokio", "tokio",
"tokio-rustls", "tokio-rustls",
"tokio-test", "tokio-test",
@@ -2864,33 +2863,13 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "thiserror"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
dependencies = [
"thiserror-impl 1.0.69",
]
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.18" version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [ dependencies = [
"thiserror-impl 2.0.18", "thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [
"proc-macro2",
"quote",
"syn",
] ]
[[package]] [[package]]
@@ -2981,9 +2960,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.52.1" version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -3130,20 +3109,20 @@ dependencies = [
[[package]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.8" version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
"http-body", "http-body",
"iri-string",
"pin-project-lite", "pin-project-lite",
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"url",
] ]
[[package]] [[package]]
@@ -3177,7 +3156,7 @@ checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [ dependencies = [
"crossbeam-channel", "crossbeam-channel",
"symlink", "symlink",
"thiserror 2.0.18", "thiserror",
"time", "time",
"tracing-subscriber", "tracing-subscriber",
] ]
@@ -3384,9 +3363,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen" name = "wasm-bindgen"
version = "0.2.118" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"once_cell", "once_cell",
@@ -3397,9 +3376,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-futures" name = "wasm-bindgen-futures"
version = "0.4.68" version = "0.4.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -3407,9 +3386,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro" name = "wasm-bindgen-macro"
version = "0.2.118" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [ dependencies = [
"quote", "quote",
"wasm-bindgen-macro-support", "wasm-bindgen-macro-support",
@@ -3417,9 +3396,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-macro-support" name = "wasm-bindgen-macro-support"
version = "0.2.118" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
@@ -3430,9 +3409,9 @@ dependencies = [
[[package]] [[package]]
name = "wasm-bindgen-shared" name = "wasm-bindgen-shared"
version = "0.2.118" version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -3465,7 +3444,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags",
"hashbrown 0.15.5", "hashbrown 0.15.5",
"indexmap", "indexmap",
"semver", "semver",
@@ -3473,9 +3452,9 @@ dependencies = [
[[package]] [[package]]
name = "web-sys" name = "web-sys"
version = "0.3.95" version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
dependencies = [ dependencies = [
"js-sys", "js-sys",
"wasm-bindgen", "wasm-bindgen",
@@ -3616,15 +3595,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
"windows-targets 0.42.2",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"
@@ -3652,21 +3622,6 @@ dependencies = [
"windows-link", "windows-link",
] ]
[[package]]
name = "windows-targets"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
"windows_aarch64_gnullvm 0.42.2",
"windows_aarch64_msvc 0.42.2",
"windows_i686_gnu 0.42.2",
"windows_i686_msvc 0.42.2",
"windows_x86_64_gnu 0.42.2",
"windows_x86_64_gnullvm 0.42.2",
"windows_x86_64_msvc 0.42.2",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.52.6" version = "0.52.6"
@@ -3700,12 +3655,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1", "windows_x86_64_msvc 0.53.1",
] ]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]] [[package]]
name = "windows_aarch64_gnullvm" name = "windows_aarch64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -3718,12 +3667,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -3736,12 +3679,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.52.6" version = "0.52.6"
@@ -3766,12 +3703,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.52.6" version = "0.52.6"
@@ -3784,12 +3715,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.52.6" version = "0.52.6"
@@ -3802,12 +3727,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.52.6" version = "0.52.6"
@@ -3820,12 +3739,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.52.6" version = "0.52.6"
@@ -3840,9 +3753,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "1.0.1" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
[[package]] [[package]]
name = "wit-bindgen" name = "wit-bindgen"
@@ -3908,7 +3821,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags 2.11.1", "bitflags",
"indexmap", "indexmap",
"log", "log",
"serde", "serde",
@@ -3969,7 +3882,7 @@ dependencies = [
"nom", "nom",
"oid-registry", "oid-registry",
"rusticata-macros", "rusticata-macros",
"thiserror 2.0.18", "thiserror",
"time", "time",
] ]
@@ -3998,18 +3911,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.48" version = "0.8.49"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -4018,9 +3931,9 @@ dependencies = [
[[package]] [[package]]
name = "zerofrom" name = "zerofrom"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [ dependencies = [
"zerofrom-derive", "zerofrom-derive",
] ]

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "telemt" name = "telemt"
version = "3.4.12" version = "3.4.13"
edition = "2024" edition = "2024"
[features] [features]

View File

@@ -4,13 +4,11 @@
[🇷🇺 README на русском](https://github.com/telemt/telemt/blob/main/README.ru.md) [🇷🇺 README на русском](https://github.com/telemt/telemt/blob/main/README.ru.md)
***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist***
> [!NOTE] > [!NOTE]
> >
> Fixed TLS ClientHello is now available in official clients for Desktop / Android / iOS > Telegram Clients TLS ClientHello has been banned by JA3 Fingerprint: we are already looking for ways to solve this problem
> >
> To work with EE-MTProxy, please update your client! > You can try build your client with our Telegram Devlibrary - [tdlib-obf](https://github.com/telemt/tdlib-obf)
<p align="center"> <p align="center">
<a href="https://t.me/telemtrs"> <a href="https://t.me/telemtrs">

View File

@@ -10,12 +10,15 @@ services:
- "443:443" - "443:443"
- "127.0.0.1:9090:9090" - "127.0.0.1:9090:9090"
- "127.0.0.1:9091:9091" - "127.0.0.1:9091:9091"
# Allow caching 'proxy-secret' in read-only container # Working dir uses tmpfs for caching 'proxy-secret' at runtime.
working_dir: /etc/telemt # Config is mounted as a directory (not a single file) so the API can
# atomically update config.toml via write-temp → rename within the same FS.
working_dir: /run/telemt
command: ["/etc/telemt/config.toml"]
volumes: volumes:
- ./config.toml:/etc/telemt/config.toml:ro - ./config:/etc/telemt:rw
tmpfs: tmpfs:
- /etc/telemt:rw,mode=1777,size=4m - /run/telemt:rw,mode=1777,size=4m
environment: environment:
- RUST_LOG=info - RUST_LOG=info
healthcheck: healthcheck:
@@ -24,8 +27,6 @@ services:
timeout: 5s timeout: 5s
retries: 3 retries: 3
start_period: 20s start_period: 20s
# Uncomment this line if you want to use host network for IPv6, but bridge is default and usually better
# network_mode: host
cap_drop: cap_drop:
- ALL - ALL
cap_add: cap_add:
@@ -37,3 +38,8 @@ services:
nofile: nofile:
soft: 65536 soft: 65536
hard: 262144 hard: 262144
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"

View File

@@ -85,145 +85,7 @@ This document lists all configuration keys accepted by `config.toml`.
# [general] # [general]
| Key | Type | Default |
| --- | ---- | ------- |
| [`data_path`](#data_path) | `String` | — |
| [`prefer_ipv6`](#prefer_ipv6) | `bool` | `false` |
| [`fast_mode`](#fast_mode) | `bool` | `true` |
| [`use_middle_proxy`](#use_middle_proxy) | `bool` | `true` |
| [`proxy_secret_path`](#proxy_secret_path) | `String` | `"proxy-secret"` |
| [`proxy_config_v4_cache_path`](#proxy_config_v4_cache_path) | `String` | `"cache/proxy-config-v4.txt"` |
| [`proxy_config_v6_cache_path`](#proxy_config_v6_cache_path) | `String` | `"cache/proxy-config-v6.txt"` |
| [`ad_tag`](#ad_tag) | `String` | — |
| [`middle_proxy_nat_ip`](#middle_proxy_nat_ip) | `IpAddr` | — |
| [`middle_proxy_nat_probe`](#middle_proxy_nat_probe) | `bool` | `true` |
| [`middle_proxy_nat_stun`](#middle_proxy_nat_stun) | `String` | — |
| [`middle_proxy_nat_stun_servers`](#middle_proxy_nat_stun_servers) | `String[]` | `[]` |
| [`stun_nat_probe_concurrency`](#stun_nat_probe_concurrency) | `usize` | `8` |
| [`middle_proxy_pool_size`](#middle_proxy_pool_size) | `usize` | `8` |
| [`middle_proxy_warm_standby`](#middle_proxy_warm_standby) | `usize` | `16` |
| [`me_init_retry_attempts`](#me_init_retry_attempts) | `u32` | `0` |
| [`me2dc_fallback`](#me2dc_fallback) | `bool` | `true` |
| [`me2dc_fast`](#me2dc_fast) | `bool` | `true` |
| [`me_keepalive_enabled`](#me_keepalive_enabled) | `bool` | `true` |
| [`me_keepalive_interval_secs`](#me_keepalive_interval_secs) | `u64` | `8` |
| [`me_keepalive_jitter_secs`](#me_keepalive_jitter_secs) | `u64` | `2` |
| [`me_keepalive_payload_random`](#me_keepalive_payload_random) | `bool` | `true` |
| [`rpc_proxy_req_every`](#rpc_proxy_req_every) | `u64` | `0` |
| [`me_writer_cmd_channel_capacity`](#me_writer_cmd_channel_capacity) | `usize` | `4096` |
| [`me_route_channel_capacity`](#me_route_channel_capacity) | `usize` | `768` |
| [`me_c2me_channel_capacity`](#me_c2me_channel_capacity) | `usize` | `1024` |
| [`me_c2me_send_timeout_ms`](#me_c2me_send_timeout_ms) | `u64` | `4000` |
| [`me_reader_route_data_wait_ms`](#me_reader_route_data_wait_ms) | `u64` | `2` |
| [`me_d2c_flush_batch_max_frames`](#me_d2c_flush_batch_max_frames) | `usize` | `32` |
| [`me_d2c_flush_batch_max_bytes`](#me_d2c_flush_batch_max_bytes) | `usize` | `131072` |
| [`me_d2c_flush_batch_max_delay_us`](#me_d2c_flush_batch_max_delay_us) | `u64` | `500` |
| [`me_d2c_ack_flush_immediate`](#me_d2c_ack_flush_immediate) | `bool` | `true` |
| [`me_quota_soft_overshoot_bytes`](#me_quota_soft_overshoot_bytes) | `u64` | `65536` |
| [`me_d2c_frame_buf_shrink_threshold_bytes`](#me_d2c_frame_buf_shrink_threshold_bytes) | `usize` | `262144` |
| [`direct_relay_copy_buf_c2s_bytes`](#direct_relay_copy_buf_c2s_bytes) | `usize` | `65536` |
| [`direct_relay_copy_buf_s2c_bytes`](#direct_relay_copy_buf_s2c_bytes) | `usize` | `262144` |
| [`crypto_pending_buffer`](#crypto_pending_buffer) | `usize` | `262144` |
| [`max_client_frame`](#max_client_frame) | `usize` | `16777216` |
| [`desync_all_full`](#desync_all_full) | `bool` | `false` |
| [`beobachten`](#beobachten) | `bool` | `true` |
| [`beobachten_minutes`](#beobachten_minutes) | `u64` | `10` |
| [`beobachten_flush_secs`](#beobachten_flush_secs) | `u64` | `15` |
| [`beobachten_file`](#beobachten_file) | `String` | `"cache/beobachten.txt"` |
| [`hardswap`](#hardswap) | `bool` | `true` |
| [`me_warmup_stagger_enabled`](#me_warmup_stagger_enabled) | `bool` | `true` |
| [`me_warmup_step_delay_ms`](#me_warmup_step_delay_ms) | `u64` | `500` |
| [`me_warmup_step_jitter_ms`](#me_warmup_step_jitter_ms) | `u64` | `300` |
| [`me_reconnect_max_concurrent_per_dc`](#me_reconnect_max_concurrent_per_dc) | `u32` | `8` |
| [`me_reconnect_backoff_base_ms`](#me_reconnect_backoff_base_ms) | `u64` | `500` |
| [`me_reconnect_backoff_cap_ms`](#me_reconnect_backoff_cap_ms) | `u64` | `30000` |
| [`me_reconnect_fast_retry_count`](#me_reconnect_fast_retry_count) | `u32` | `16` |
| [`me_single_endpoint_shadow_writers`](#me_single_endpoint_shadow_writers) | `u8` | `2` |
| [`me_single_endpoint_outage_mode_enabled`](#me_single_endpoint_outage_mode_enabled) | `bool` | `true` |
| [`me_single_endpoint_outage_disable_quarantine`](#me_single_endpoint_outage_disable_quarantine) | `bool` | `true` |
| [`me_single_endpoint_outage_backoff_min_ms`](#me_single_endpoint_outage_backoff_min_ms) | `u64` | `250` |
| [`me_single_endpoint_outage_backoff_max_ms`](#me_single_endpoint_outage_backoff_max_ms) | `u64` | `3000` |
| [`me_single_endpoint_shadow_rotate_every_secs`](#me_single_endpoint_shadow_rotate_every_secs) | `u64` | `900` |
| [`me_floor_mode`](#me_floor_mode) | `"static"` or `"adaptive"` | `"adaptive"` |
| [`me_adaptive_floor_idle_secs`](#me_adaptive_floor_idle_secs) | `u64` | `90` |
| [`me_adaptive_floor_min_writers_single_endpoint`](#me_adaptive_floor_min_writers_single_endpoint) | `u8` | `1` |
| [`me_adaptive_floor_min_writers_multi_endpoint`](#me_adaptive_floor_min_writers_multi_endpoint) | `u8` | `1` |
| [`me_adaptive_floor_recover_grace_secs`](#me_adaptive_floor_recover_grace_secs) | `u64` | `180` |
| [`me_adaptive_floor_writers_per_core_total`](#me_adaptive_floor_writers_per_core_total) | `u16` | `48` |
| [`me_adaptive_floor_cpu_cores_override`](#me_adaptive_floor_cpu_cores_override) | `u16` | `0` |
| [`me_adaptive_floor_max_extra_writers_single_per_core`](#me_adaptive_floor_max_extra_writers_single_per_core) | `u16` | `1` |
| [`me_adaptive_floor_max_extra_writers_multi_per_core`](#me_adaptive_floor_max_extra_writers_multi_per_core) | `u16` | `2` |
| [`me_adaptive_floor_max_active_writers_per_core`](#me_adaptive_floor_max_active_writers_per_core) | `u16` | `64` |
| [`me_adaptive_floor_max_warm_writers_per_core`](#me_adaptive_floor_max_warm_writers_per_core) | `u16` | `64` |
| [`me_adaptive_floor_max_active_writers_global`](#me_adaptive_floor_max_active_writers_global) | `u32` | `256` |
| [`me_adaptive_floor_max_warm_writers_global`](#me_adaptive_floor_max_warm_writers_global) | `u32` | `256` |
| [`upstream_connect_retry_attempts`](#upstream_connect_retry_attempts) | `u32` | `2` |
| [`upstream_connect_retry_backoff_ms`](#upstream_connect_retry_backoff_ms) | `u64` | `100` |
| [`upstream_connect_budget_ms`](#upstream_connect_budget_ms) | `u64` | `3000` |
| [`upstream_unhealthy_fail_threshold`](#upstream_unhealthy_fail_threshold) | `u32` | `5` |
| [`upstream_connect_failfast_hard_errors`](#upstream_connect_failfast_hard_errors) | `bool` | `false` |
| [`stun_iface_mismatch_ignore`](#stun_iface_mismatch_ignore) | `bool` | `false` |
| [`unknown_dc_log_path`](#unknown_dc_log_path) | `String` | `"unknown-dc.txt"` |
| [`unknown_dc_file_log_enabled`](#unknown_dc_file_log_enabled) | `bool` | `false` |
| [`log_level`](#log_level) | `"debug"`, `"verbose"`, `"normal"`, or `"silent"` | `"normal"` |
| [`disable_colors`](#disable_colors) | `bool` | `false` |
| [`me_socks_kdf_policy`](#me_socks_kdf_policy) | `"strict"` or `"compat"` | `"strict"` |
| [`me_route_backpressure_enabled`](#me_route_backpressure_enabled) | `bool` | `false` |
| [`me_route_fairshare_enabled`](#me_route_fairshare_enabled) | `bool` | `false` |
| [`me_route_backpressure_base_timeout_ms`](#me_route_backpressure_base_timeout_ms) | `u64` | `25` |
| [`me_route_backpressure_high_timeout_ms`](#me_route_backpressure_high_timeout_ms) | `u64` | `120` |
| [`me_route_backpressure_high_watermark_pct`](#me_route_backpressure_high_watermark_pct) | `u8` | `80` |
| [`me_health_interval_ms_unhealthy`](#me_health_interval_ms_unhealthy) | `u64` | `1000` |
| [`me_health_interval_ms_healthy`](#me_health_interval_ms_healthy) | `u64` | `3000` |
| [`me_admission_poll_ms`](#me_admission_poll_ms) | `u64` | `1000` |
| [`me_warn_rate_limit_ms`](#me_warn_rate_limit_ms) | `u64` | `5000` |
| [`me_route_no_writer_mode`](#me_route_no_writer_mode) | `"async_recovery_failfast"`, `"inline_recovery_legacy"`, or `"hybrid_async_persistent"` | `"hybrid_async_persistent"` |
| [`me_route_no_writer_wait_ms`](#me_route_no_writer_wait_ms) | `u64` | `250` |
| [`me_route_hybrid_max_wait_ms`](#me_route_hybrid_max_wait_ms) | `u64` | `3000` |
| [`me_route_blocking_send_timeout_ms`](#me_route_blocking_send_timeout_ms) | `u64` | `250` |
| [`me_route_inline_recovery_attempts`](#me_route_inline_recovery_attempts) | `u32` | `3` |
| [`me_route_inline_recovery_wait_ms`](#me_route_inline_recovery_wait_ms) | `u64` | `3000` |
| [`fast_mode_min_tls_record`](#fast_mode_min_tls_record) | `usize` | `0` |
| [`update_every`](#update_every) | `u64` | `300` |
| [`me_reinit_every_secs`](#me_reinit_every_secs) | `u64` | `900` |
| [`me_hardswap_warmup_delay_min_ms`](#me_hardswap_warmup_delay_min_ms) | `u64` | `1000` |
| [`me_hardswap_warmup_delay_max_ms`](#me_hardswap_warmup_delay_max_ms) | `u64` | `2000` |
| [`me_hardswap_warmup_extra_passes`](#me_hardswap_warmup_extra_passes) | `u8` | `3` |
| [`me_hardswap_warmup_pass_backoff_base_ms`](#me_hardswap_warmup_pass_backoff_base_ms) | `u64` | `500` |
| [`me_config_stable_snapshots`](#me_config_stable_snapshots) | `u8` | `2` |
| [`me_config_apply_cooldown_secs`](#me_config_apply_cooldown_secs) | `u64` | `300` |
| [`me_snapshot_require_http_2xx`](#me_snapshot_require_http_2xx) | `bool` | `true` |
| [`me_snapshot_reject_empty_map`](#me_snapshot_reject_empty_map) | `bool` | `true` |
| [`me_snapshot_min_proxy_for_lines`](#me_snapshot_min_proxy_for_lines) | `u32` | `1` |
| [`proxy_secret_stable_snapshots`](#proxy_secret_stable_snapshots) | `u8` | `2` |
| [`proxy_secret_rotate_runtime`](#proxy_secret_rotate_runtime) | `bool` | `true` |
| [`me_secret_atomic_snapshot`](#me_secret_atomic_snapshot) | `bool` | `true` |
| [`proxy_secret_len_max`](#proxy_secret_len_max) | `usize` | `256` |
| [`me_pool_drain_ttl_secs`](#me_pool_drain_ttl_secs) | `u64` | `90` |
| [`me_instadrain`](#me_instadrain) | `bool` | `false` |
| [`me_pool_drain_threshold`](#me_pool_drain_threshold) | `u64` | `32` |
| [`me_pool_drain_soft_evict_enabled`](#me_pool_drain_soft_evict_enabled) | `bool` | `true` |
| [`me_pool_drain_soft_evict_grace_secs`](#me_pool_drain_soft_evict_grace_secs) | `u64` | `10` |
| [`me_pool_drain_soft_evict_per_writer`](#me_pool_drain_soft_evict_per_writer) | `u8` | `2` |
| [`me_pool_drain_soft_evict_budget_per_core`](#me_pool_drain_soft_evict_budget_per_core) | `u16` | `16` |
| [`me_pool_drain_soft_evict_cooldown_ms`](#me_pool_drain_soft_evict_cooldown_ms) | `u64` | `1000` |
| [`me_bind_stale_mode`](#me_bind_stale_mode) | `"never"`, `"ttl"`, or `"always"` | `"ttl"` |
| [`me_bind_stale_ttl_secs`](#me_bind_stale_ttl_secs) | `u64` | `90` |
| [`me_pool_min_fresh_ratio`](#me_pool_min_fresh_ratio) | `f32` | `0.8` |
| [`me_reinit_drain_timeout_secs`](#me_reinit_drain_timeout_secs) | `u64` | `90` |
| [`proxy_secret_auto_reload_secs`](#proxy_secret_auto_reload_secs) | `u64` | `3600` |
| [`proxy_config_auto_reload_secs`](#proxy_config_auto_reload_secs) | `u64` | `3600` |
| [`me_reinit_singleflight`](#me_reinit_singleflight) | `bool` | `true` |
| [`me_reinit_trigger_channel`](#me_reinit_trigger_channel) | `usize` | `64` |
| [`me_reinit_coalesce_window_ms`](#me_reinit_coalesce_window_ms) | `u64` | `200` |
| [`me_deterministic_writer_sort`](#me_deterministic_writer_sort) | `bool` | `true` |
| [`me_writer_pick_mode`](#me_writer_pick_mode) | `"sorted_rr"` or `"p2c"` | `"p2c"` |
| [`me_writer_pick_sample_size`](#me_writer_pick_sample_size) | `u8` | `3` |
| [`ntp_check`](#ntp_check) | `bool` | `true` |
| [`ntp_servers`](#ntp_servers) | `String[]` | `["pool.ntp.org"]` |
| [`auto_degradation_enabled`](#auto_degradation_enabled) | `bool` | `true` |
| [`degradation_min_unavailable_dc_groups`](#degradation_min_unavailable_dc_groups) | `u8` | `2` |
| [`rst_on_close`](#rst_on_close) | `"off"`, `"errors"`, or `"always"` | `"off"` |
| Key | Type | Default | Hot-Reload | | Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- | | --- | ---- | ------- | ---------- |
| [`data_path`](#data_path) | `String` | — | `` | | [`data_path`](#data_path) | `String` | — | `` |
@@ -2525,41 +2387,6 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
# [censorship] # [censorship]
| Key | Type | Default |
| --- | ---- | ------- |
| [`tls_domain`](#tls_domain) | `String` | `"petrovich.ru"` |
| [`tls_domains`](#tls_domains) | `String[]` | `[]` |
| [`unknown_sni_action`](#unknown_sni_action) | `"drop"`, `"mask"`, `"accept"`, `"reject_handshake"` | `"drop"` |
| [`tls_fetch_scope`](#tls_fetch_scope) | `String` | `""` |
| [`tls_fetch`](#tls_fetch) | `Table` | built-in defaults |
| [`mask`](#mask) | `bool` | `true` |
| [`mask_host`](#mask_host) | `String` | — |
| [`mask_port`](#mask_port) | `u16` | `443` |
| [`exclusive_mask`](#exclusive_mask) | `Map<String,String>` | `{}` |
| [`mask_unix_sock`](#mask_unix_sock) | `String` | — |
| [`fake_cert_len`](#fake_cert_len) | `usize` | `2048` |
| [`tls_emulation`](#tls_emulation) | `bool` | `true` |
| [`tls_front_dir`](#tls_front_dir) | `String` | `"tlsfront"` |
| [`server_hello_delay_min_ms`](#server_hello_delay_min_ms) | `u64` | `0` |
| [`server_hello_delay_max_ms`](#server_hello_delay_max_ms) | `u64` | `0` |
| [`tls_new_session_tickets`](#tls_new_session_tickets) | `u8` | `0` |
| [`tls_full_cert_ttl_secs`](#tls_full_cert_ttl_secs) | `u64` | `90` |
| [`serverhello_compact`](#serverhello_compact) | `bool` | `false` |
| [`alpn_enforce`](#alpn_enforce) | `bool` | `true` |
| [`mask_proxy_protocol`](#mask_proxy_protocol) | `u8` | `0` |
| [`mask_shape_hardening`](#mask_shape_hardening) | `bool` | `true` |
| [`mask_shape_hardening_aggressive_mode`](#mask_shape_hardening_aggressive_mode) | `bool` | `false` |
| [`mask_shape_bucket_floor_bytes`](#mask_shape_bucket_floor_bytes) | `usize` | `512` |
| [`mask_shape_bucket_cap_bytes`](#mask_shape_bucket_cap_bytes) | `usize` | `4096` |
| [`mask_shape_above_cap_blur`](#mask_shape_above_cap_blur) | `bool` | `false` |
| [`mask_shape_above_cap_blur_max_bytes`](#mask_shape_above_cap_blur_max_bytes) | `usize` | `512` |
| [`mask_relay_max_bytes`](#mask_relay_max_bytes) | `usize` | `5242880` |
| [`mask_relay_timeout_ms`](#mask_relay_timeout_ms) | `u64` | `60_000` |
| [`mask_relay_idle_timeout_ms`](#mask_relay_idle_timeout_ms) | `u64` | `5_000` |
| [`mask_classifier_prefetch_timeout_ms`](#mask_classifier_prefetch_timeout_ms) | `u64` | `5` |
| [`mask_timing_normalization_enabled`](#mask_timing_normalization_enabled) | `bool` | `false` |
| [`mask_timing_normalization_floor_ms`](#mask_timing_normalization_floor_ms) | `u64` | `0` |
| [`mask_timing_normalization_ceiling_ms`](#mask_timing_normalization_ceiling_ms) | `u64` | `0` |
| Key | Type | Default | Hot-Reload | | Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- | | --- | ---- | ------- | ---------- |
| [`tls_domain`](#tls_domain) | `String` | `"petrovich.ru"` | `` | | [`tls_domain`](#tls_domain) | `String` | `"petrovich.ru"` | `` |

View File

@@ -85,145 +85,7 @@
# [general] # [general]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`data_path`](#data_path) | `String` | — |
| [`prefer_ipv6`](#prefer_ipv6) | `bool` | `false` |
| [`fast_mode`](#fast_mode) | `bool` | `true` |
| [`use_middle_proxy`](#use_middle_proxy) | `bool` | `true` |
| [`proxy_secret_path`](#proxy_secret_path) | `String` | `"proxy-secret"` |
| [`proxy_config_v4_cache_path`](#proxy_config_v4_cache_path) | `String` | `"cache/proxy-config-v4.txt"` |
| [`proxy_config_v6_cache_path`](#proxy_config_v6_cache_path) | `String` | `"cache/proxy-config-v6.txt"` |
| [`ad_tag`](#ad_tag) | `String` | — |
| [`middle_proxy_nat_ip`](#middle_proxy_nat_ip) | `IpAddr` | — |
| [`middle_proxy_nat_probe`](#middle_proxy_nat_probe) | `bool` | `true` |
| [`middle_proxy_nat_stun`](#middle_proxy_nat_stun) | `String` | — |
| [`middle_proxy_nat_stun_servers`](#middle_proxy_nat_stun_servers) | `String[]` | `[]` |
| [`stun_nat_probe_concurrency`](#stun_nat_probe_concurrency) | `usize` | `8` |
| [`middle_proxy_pool_size`](#middle_proxy_pool_size) | `usize` | `8` |
| [`middle_proxy_warm_standby`](#middle_proxy_warm_standby) | `usize` | `16` |
| [`me_init_retry_attempts`](#me_init_retry_attempts) | `u32` | `0` |
| [`me2dc_fallback`](#me2dc_fallback) | `bool` | `true` |
| [`me2dc_fast`](#me2dc_fast) | `bool` | `true` |
| [`me_keepalive_enabled`](#me_keepalive_enabled) | `bool` | `true` |
| [`me_keepalive_interval_secs`](#me_keepalive_interval_secs) | `u64` | `8` |
| [`me_keepalive_jitter_secs`](#me_keepalive_jitter_secs) | `u64` | `2` |
| [`me_keepalive_payload_random`](#me_keepalive_payload_random) | `bool` | `true` |
| [`rpc_proxy_req_every`](#rpc_proxy_req_every) | `u64` | `0` |
| [`me_writer_cmd_channel_capacity`](#me_writer_cmd_channel_capacity) | `usize` | `4096` |
| [`me_route_channel_capacity`](#me_route_channel_capacity) | `usize` | `768` |
| [`me_c2me_channel_capacity`](#me_c2me_channel_capacity) | `usize` | `1024` |
| [`me_c2me_send_timeout_ms`](#me_c2me_send_timeout_ms) | `u64` | `4000` |
| [`me_reader_route_data_wait_ms`](#me_reader_route_data_wait_ms) | `u64` | `2` |
| [`me_d2c_flush_batch_max_frames`](#me_d2c_flush_batch_max_frames) | `usize` | `32` |
| [`me_d2c_flush_batch_max_bytes`](#me_d2c_flush_batch_max_bytes) | `usize` | `131072` |
| [`me_d2c_flush_batch_max_delay_us`](#me_d2c_flush_batch_max_delay_us) | `u64` | `500` |
| [`me_d2c_ack_flush_immediate`](#me_d2c_ack_flush_immediate) | `bool` | `true` |
| [`me_quota_soft_overshoot_bytes`](#me_quota_soft_overshoot_bytes) | `u64` | `65536` |
| [`me_d2c_frame_buf_shrink_threshold_bytes`](#me_d2c_frame_buf_shrink_threshold_bytes) | `usize` | `262144` |
| [`direct_relay_copy_buf_c2s_bytes`](#direct_relay_copy_buf_c2s_bytes) | `usize` | `65536` |
| [`direct_relay_copy_buf_s2c_bytes`](#direct_relay_copy_buf_s2c_bytes) | `usize` | `262144` |
| [`crypto_pending_buffer`](#crypto_pending_buffer) | `usize` | `262144` |
| [`max_client_frame`](#max_client_frame) | `usize` | `16777216` |
| [`desync_all_full`](#desync_all_full) | `bool` | `false` |
| [`beobachten`](#beobachten) | `bool` | `true` |
| [`beobachten_minutes`](#beobachten_minutes) | `u64` | `10` |
| [`beobachten_flush_secs`](#beobachten_flush_secs) | `u64` | `15` |
| [`beobachten_file`](#beobachten_file) | `String` | `"cache/beobachten.txt"` |
| [`hardswap`](#hardswap) | `bool` | `true` |
| [`me_warmup_stagger_enabled`](#me_warmup_stagger_enabled) | `bool` | `true` |
| [`me_warmup_step_delay_ms`](#me_warmup_step_delay_ms) | `u64` | `500` |
| [`me_warmup_step_jitter_ms`](#me_warmup_step_jitter_ms) | `u64` | `300` |
| [`me_reconnect_max_concurrent_per_dc`](#me_reconnect_max_concurrent_per_dc) | `u32` | `8` |
| [`me_reconnect_backoff_base_ms`](#me_reconnect_backoff_base_ms) | `u64` | `500` |
| [`me_reconnect_backoff_cap_ms`](#me_reconnect_backoff_cap_ms) | `u64` | `30000` |
| [`me_reconnect_fast_retry_count`](#me_reconnect_fast_retry_count) | `u32` | `16` |
| [`me_single_endpoint_shadow_writers`](#me_single_endpoint_shadow_writers) | `u8` | `2` |
| [`me_single_endpoint_outage_mode_enabled`](#me_single_endpoint_outage_mode_enabled) | `bool` | `true` |
| [`me_single_endpoint_outage_disable_quarantine`](#me_single_endpoint_outage_disable_quarantine) | `bool` | `true` |
| [`me_single_endpoint_outage_backoff_min_ms`](#me_single_endpoint_outage_backoff_min_ms) | `u64` | `250` |
| [`me_single_endpoint_outage_backoff_max_ms`](#me_single_endpoint_outage_backoff_max_ms) | `u64` | `3000` |
| [`me_single_endpoint_shadow_rotate_every_secs`](#me_single_endpoint_shadow_rotate_every_secs) | `u64` | `900` |
| [`me_floor_mode`](#me_floor_mode) | `"static"` or `"adaptive"` | `"adaptive"` |
| [`me_adaptive_floor_idle_secs`](#me_adaptive_floor_idle_secs) | `u64` | `90` |
| [`me_adaptive_floor_min_writers_single_endpoint`](#me_adaptive_floor_min_writers_single_endpoint) | `u8` | `1` |
| [`me_adaptive_floor_min_writers_multi_endpoint`](#me_adaptive_floor_min_writers_multi_endpoint) | `u8` | `1` |
| [`me_adaptive_floor_recover_grace_secs`](#me_adaptive_floor_recover_grace_secs) | `u64` | `180` |
| [`me_adaptive_floor_writers_per_core_total`](#me_adaptive_floor_writers_per_core_total) | `u16` | `48` |
| [`me_adaptive_floor_cpu_cores_override`](#me_adaptive_floor_cpu_cores_override) | `u16` | `0` |
| [`me_adaptive_floor_max_extra_writers_single_per_core`](#me_adaptive_floor_max_extra_writers_single_per_core) | `u16` | `1` |
| [`me_adaptive_floor_max_extra_writers_multi_per_core`](#me_adaptive_floor_max_extra_writers_multi_per_core) | `u16` | `2` |
| [`me_adaptive_floor_max_active_writers_per_core`](#me_adaptive_floor_max_active_writers_per_core) | `u16` | `64` |
| [`me_adaptive_floor_max_warm_writers_per_core`](#me_adaptive_floor_max_warm_writers_per_core) | `u16` | `64` |
| [`me_adaptive_floor_max_active_writers_global`](#me_adaptive_floor_max_active_writers_global) | `u32` | `256` |
| [`me_adaptive_floor_max_warm_writers_global`](#me_adaptive_floor_max_warm_writers_global) | `u32` | `256` |
| [`upstream_connect_retry_attempts`](#upstream_connect_retry_attempts) | `u32` | `2` |
| [`upstream_connect_retry_backoff_ms`](#upstream_connect_retry_backoff_ms) | `u64` | `100` |
| [`upstream_connect_budget_ms`](#upstream_connect_budget_ms) | `u64` | `3000` |
| [`upstream_unhealthy_fail_threshold`](#upstream_unhealthy_fail_threshold) | `u32` | `5` |
| [`upstream_connect_failfast_hard_errors`](#upstream_connect_failfast_hard_errors) | `bool` | `false` |
| [`stun_iface_mismatch_ignore`](#stun_iface_mismatch_ignore) | `bool` | `false` |
| [`unknown_dc_log_path`](#unknown_dc_log_path) | `String` | `"unknown-dc.txt"` |
| [`unknown_dc_file_log_enabled`](#unknown_dc_file_log_enabled) | `bool` | `false` |
| [`log_level`](#log_level) | `"debug"`, `"verbose"`, `"normal"`, or `"silent"` | `"normal"` |
| [`disable_colors`](#disable_colors) | `bool` | `false` |
| [`me_socks_kdf_policy`](#me_socks_kdf_policy) | `"strict"` or `"compat"` | `"strict"` |
| [`me_route_backpressure_enabled`](#me_route_backpressure_enabled) | `bool` | `false` |
| [`me_route_fairshare_enabled`](#me_route_fairshare_enabled) | `bool` | `false` |
| [`me_route_backpressure_base_timeout_ms`](#me_route_backpressure_base_timeout_ms) | `u64` | `25` |
| [`me_route_backpressure_high_timeout_ms`](#me_route_backpressure_high_timeout_ms) | `u64` | `120` |
| [`me_route_backpressure_high_watermark_pct`](#me_route_backpressure_high_watermark_pct) | `u8` | `80` |
| [`me_health_interval_ms_unhealthy`](#me_health_interval_ms_unhealthy) | `u64` | `1000` |
| [`me_health_interval_ms_healthy`](#me_health_interval_ms_healthy) | `u64` | `3000` |
| [`me_admission_poll_ms`](#me_admission_poll_ms) | `u64` | `1000` |
| [`me_warn_rate_limit_ms`](#me_warn_rate_limit_ms) | `u64` | `5000` |
| [`me_route_no_writer_mode`](#me_route_no_writer_mode) | `"async_recovery_failfast"`, `"inline_recovery_legacy"`, or `"hybrid_async_persistent"` | `"hybrid_async_persistent"` |
| [`me_route_no_writer_wait_ms`](#me_route_no_writer_wait_ms) | `u64` | `250` |
| [`me_route_hybrid_max_wait_ms`](#me_route_hybrid_max_wait_ms) | `u64` | `3000` |
| [`me_route_blocking_send_timeout_ms`](#me_route_blocking_send_timeout_ms) | `u64` | `250` |
| [`me_route_inline_recovery_attempts`](#me_route_inline_recovery_attempts) | `u32` | `3` |
| [`me_route_inline_recovery_wait_ms`](#me_route_inline_recovery_wait_ms) | `u64` | `3000` |
| [`fast_mode_min_tls_record`](#fast_mode_min_tls_record) | `usize` | `0` |
| [`update_every`](#update_every) | `u64` | `300` |
| [`me_reinit_every_secs`](#me_reinit_every_secs) | `u64` | `900` |
| [`me_hardswap_warmup_delay_min_ms`](#me_hardswap_warmup_delay_min_ms) | `u64` | `1000` |
| [`me_hardswap_warmup_delay_max_ms`](#me_hardswap_warmup_delay_max_ms) | `u64` | `2000` |
| [`me_hardswap_warmup_extra_passes`](#me_hardswap_warmup_extra_passes) | `u8` | `3` |
| [`me_hardswap_warmup_pass_backoff_base_ms`](#me_hardswap_warmup_pass_backoff_base_ms) | `u64` | `500` |
| [`me_config_stable_snapshots`](#me_config_stable_snapshots) | `u8` | `2` |
| [`me_config_apply_cooldown_secs`](#me_config_apply_cooldown_secs) | `u64` | `300` |
| [`me_snapshot_require_http_2xx`](#me_snapshot_require_http_2xx) | `bool` | `true` |
| [`me_snapshot_reject_empty_map`](#me_snapshot_reject_empty_map) | `bool` | `true` |
| [`me_snapshot_min_proxy_for_lines`](#me_snapshot_min_proxy_for_lines) | `u32` | `1` |
| [`proxy_secret_stable_snapshots`](#proxy_secret_stable_snapshots) | `u8` | `2` |
| [`proxy_secret_rotate_runtime`](#proxy_secret_rotate_runtime) | `bool` | `true` |
| [`me_secret_atomic_snapshot`](#me_secret_atomic_snapshot) | `bool` | `true` |
| [`proxy_secret_len_max`](#proxy_secret_len_max) | `usize` | `256` |
| [`me_pool_drain_ttl_secs`](#me_pool_drain_ttl_secs) | `u64` | `90` |
| [`me_instadrain`](#me_instadrain) | `bool` | `false` |
| [`me_pool_drain_threshold`](#me_pool_drain_threshold) | `u64` | `32` |
| [`me_pool_drain_soft_evict_enabled`](#me_pool_drain_soft_evict_enabled) | `bool` | `true` |
| [`me_pool_drain_soft_evict_grace_secs`](#me_pool_drain_soft_evict_grace_secs) | `u64` | `10` |
| [`me_pool_drain_soft_evict_per_writer`](#me_pool_drain_soft_evict_per_writer) | `u8` | `2` |
| [`me_pool_drain_soft_evict_budget_per_core`](#me_pool_drain_soft_evict_budget_per_core) | `u16` | `16` |
| [`me_pool_drain_soft_evict_cooldown_ms`](#me_pool_drain_soft_evict_cooldown_ms) | `u64` | `1000` |
| [`me_bind_stale_mode`](#me_bind_stale_mode) | `"never"`, `"ttl"`, or `"always"` | `"ttl"` |
| [`me_bind_stale_ttl_secs`](#me_bind_stale_ttl_secs) | `u64` | `90` |
| [`me_pool_min_fresh_ratio`](#me_pool_min_fresh_ratio) | `f32` | `0.8` |
| [`me_reinit_drain_timeout_secs`](#me_reinit_drain_timeout_secs) | `u64` | `90` |
| [`proxy_secret_auto_reload_secs`](#proxy_secret_auto_reload_secs) | `u64` | `3600` |
| [`proxy_config_auto_reload_secs`](#proxy_config_auto_reload_secs) | `u64` | `3600` |
| [`me_reinit_singleflight`](#me_reinit_singleflight) | `bool` | `true` |
| [`me_reinit_trigger_channel`](#me_reinit_trigger_channel) | `usize` | `64` |
| [`me_reinit_coalesce_window_ms`](#me_reinit_coalesce_window_ms) | `u64` | `200` |
| [`me_deterministic_writer_sort`](#me_deterministic_writer_sort) | `bool` | `true` |
| [`me_writer_pick_mode`](#me_writer_pick_mode) | `"sorted_rr"` or `"p2c"` | `"p2c"` |
| [`me_writer_pick_sample_size`](#me_writer_pick_sample_size) | `u8` | `3` |
| [`ntp_check`](#ntp_check) | `bool` | `true` |
| [`ntp_servers`](#ntp_servers) | `String[]` | `["pool.ntp.org"]` |
| [`auto_degradation_enabled`](#auto_degradation_enabled) | `bool` | `true` |
| [`degradation_min_unavailable_dc_groups`](#degradation_min_unavailable_dc_groups) | `u8` | `2` |
| [`rst_on_close`](#rst_on_close) | `"off"`, `"errors"` или `"always"` | `"off"` |
| Ключ | Тип | По умолчанию | Hot-Reload | | Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- | | --- | ---- | ------- | ---------- |
| [`data_path`](#data_path) | `String` | — | `` | | [`data_path`](#data_path) | `String` | — | `` |
@@ -2531,41 +2393,6 @@
# [censorship] # [censorship]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`tls_domain`](#tls_domain) | `String` | `"petrovich.ru"` |
| [`tls_domains`](#tls_domains) | `String[]` | `[]` |
| [`unknown_sni_action`](#unknown_sni_action) | `"drop"`, `"mask"`, `"accept"`, `"reject_handshake"` | `"drop"` |
| [`tls_fetch_scope`](#tls_fetch_scope) | `String` | `""` |
| [`tls_fetch`](#tls_fetch) | `Table` | built-in defaults |
| [`mask`](#mask) | `bool` | `true` |
| [`mask_host`](#mask_host) | `String` | — |
| [`mask_port`](#mask_port) | `u16` | `443` |
| [`exclusive_mask`](#exclusive_mask) | `Map<String,String>` | `{}` |
| [`mask_unix_sock`](#mask_unix_sock) | `String` | — |
| [`fake_cert_len`](#fake_cert_len) | `usize` | `2048` |
| [`tls_emulation`](#tls_emulation) | `bool` | `true` |
| [`tls_front_dir`](#tls_front_dir) | `String` | `"tlsfront"` |
| [`server_hello_delay_min_ms`](#server_hello_delay_min_ms) | `u64` | `0` |
| [`server_hello_delay_max_ms`](#server_hello_delay_max_ms) | `u64` | `0` |
| [`tls_new_session_tickets`](#tls_new_session_tickets) | `u8` | `0` |
| [`tls_full_cert_ttl_secs`](#tls_full_cert_ttl_secs) | `u64` | `90` |
| [`serverhello_compact`](#serverhello_compact) | `bool` | `false` |
| [`alpn_enforce`](#alpn_enforce) | `bool` | `true` |
| [`mask_proxy_protocol`](#mask_proxy_protocol) | `u8` | `0` |
| [`mask_shape_hardening`](#mask_shape_hardening) | `bool` | `true` |
| [`mask_shape_hardening_aggressive_mode`](#mask_shape_hardening_aggressive_mode) | `bool` | `false` |
| [`mask_shape_bucket_floor_bytes`](#mask_shape_bucket_floor_bytes) | `usize` | `512` |
| [`mask_shape_bucket_cap_bytes`](#mask_shape_bucket_cap_bytes) | `usize` | `4096` |
| [`mask_shape_above_cap_blur`](#mask_shape_above_cap_blur) | `bool` | `false` |
| [`mask_shape_above_cap_blur_max_bytes`](#mask_shape_above_cap_blur_max_bytes) | `usize` | `512` |
| [`mask_relay_max_bytes`](#mask_relay_max_bytes) | `usize` | `5242880` |
| [`mask_relay_timeout_ms`](mask_relay_timeout_ms) | `u64` | `60_000` |
| [`mask_relay_idle_timeout_ms`](mask_relay_idle_timeout_ms) | `u64` | `5_000` |
| [`mask_classifier_prefetch_timeout_ms`](#mask_classifier_prefetch_timeout_ms) | `u64` | `5` |
| [`mask_timing_normalization_enabled`](#mask_timing_normalization_enabled) | `bool` | `false` |
| [`mask_timing_normalization_floor_ms`](#mask_timing_normalization_floor_ms) | `u64` | `0` |
| [`mask_timing_normalization_ceiling_ms`](#mask_timing_normalization_ceiling_ms) | `u64` | `0` |
| Ключ | Тип | По умолчанию | Hot-Reload | | Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- | | --- | ---- | ------- | ---------- |
| [`tls_domain`](#tls_domain) | `String` | `"petrovich.ru"` | `` | | [`tls_domain`](#tls_domain) | `String` | `"petrovich.ru"` | `` |

View File

@@ -172,7 +172,7 @@ Those cross-DC requests are normal and happen constantly.
> If your home DC is DC2 and DC2 goes down, you **cannot** reach DC5 even though DC5 itself is perfectly healthy. > If your home DC is DC2 and DC2 goes down, you **cannot** reach DC5 even though DC5 itself is perfectly healthy.
> The client has no valid session to route the request through. > The client has no valid session to route the request through.
This is also why an MTProxy only needs to reach Telegram's DC infrastructure as a whole. This is also why it is required for MTProxy to reach Telegram's DC infrastructure as a whole.
The proxy itself doesn't care which DC your account lives on. The client negotiates the correct DC through the proxy after connecting. The proxy itself doesn't care which DC your account lives on. The client negotiates the correct DC through the proxy after connecting.
### How many people can use one link ### How many people can use one link

View File

@@ -157,7 +157,7 @@ https://github.com/telemt/telemt/discussions/167
## Как клиенты взаимодействуют с дата-центрами Telegram ## Как клиенты взаимодействуют с дата-центрами Telegram
При регистрации аккаунта Telegram он навсегда привязывается к одному из дата-центров (DC). При регистрации аккаунта Telegram он навсегда привязывается к одному из дата-центров (DC).
Telegram заранее определяет к какому DC привязать аккаунт исходя из региона, к которому относиться номер телефона. Telegram заранее определяет к какому DC привязать аккаунт исходя из региона, к которому относится номер телефона.
Этот DC становится вашим **домашним**: именно там хранится весь контент, который вы загружаете (фото, видео, файлы, сообщения). Этот DC становится вашим **домашним**: именно там хранится весь контент, который вы загружаете (фото, видео, файлы, сообщения).
И именно на нем клиент авторизуется при каждом подключении. И именно на нем клиент авторизуется при каждом подключении.
@@ -170,7 +170,7 @@ Telegram заранее определяет к какому DC привязат
> Если ваш домашний DC — DC2, и DC2 лежит, вы **не сможете** достучаться и до DC5, даже если сам DC5 полностью исправен. > Если ваш домашний DC — DC2, и DC2 лежит, вы **не сможете** достучаться и до DC5, даже если сам DC5 полностью исправен.
> У клиента просто нет валидной сессии, через которую можно было бы направить запрос. > У клиента просто нет валидной сессии, через которую можно было бы направить запрос.
По той же причине MTProxy достаточно иметь доступ к инфраструктуре Telegram в целом. По той же причине MTProxy необходимо иметь доступ к инфраструктуре Telegram целиком, а не частично.
Cамому MTProxy всё равно, на каком DC живёт ваш аккаунт. Клиент cам договаривается о нужном DC через прокси уже после подключения. Cамому MTProxy всё равно, на каком DC живёт ваш аккаунт. Клиент cам договаривается о нужном DC через прокси уже после подключения.
## Что такое dd и ee в контексте MTProxy? ## Что такое dd и ee в контексте MTProxy?

View File

@@ -235,7 +235,10 @@ curl -s http://127.0.0.1:9091/v1/users | jq -r '.data[] | "[\(.username)]", (.li
# Telemt через Docker Compose # Telemt через Docker Compose
**1. Отредактируйте `config.toml` в корневом каталоге репозитория (как минимум: порт, пользовательские секреты, tls_domain)** **1. Создайте директорию `config/` и поместите в неё отрдеактированный `config.toml` (указав как минимум: порт, пользовательские секреты, tls_domain):**
```bash
mkdir config && mv config.toml config/
```
**2. Запустите контейнер:** **2. Запустите контейнер:**
```bash ```bash
docker compose up -d --build docker compose up -d --build
@@ -249,7 +252,7 @@ docker compose logs -f telemt
docker compose down docker compose down
``` ```
> [!NOTE] > [!NOTE]
> - В `docker-compose.yml` файл `./config.toml` монтируется в `/app/config.toml` (доступно только для чтения) > - Директория `./config/` монтируется в `/etc/telemt/` (read-write), что позволяет API атомарно обновлять config.toml
> - По умолчанию публикуются порты 443:443, а контейнер запускается со сброшенными привилегиями (добавлена только `NET_BIND_SERVICE`) > - По умолчанию публикуются порты 443:443, а контейнер запускается со сброшенными привилегиями (добавлена только `NET_BIND_SERVICE`)
> - Если вам действительно нужна сеть хоста (обычно это требуется только для некоторых конфигураций IPv6), раскомментируйте `network_mode: host` > - Если вам действительно нужна сеть хоста (обычно это требуется только для некоторых конфигураций IPv6), раскомментируйте `network_mode: host`

View File

@@ -84,21 +84,22 @@ set_language() {
L_INFO_KEEP_CONF="Примечание: Конфигурация сохранена. Используйте 'purge' для очистки." L_INFO_KEEP_CONF="Примечание: Конфигурация сохранена. Используйте 'purge' для очистки."
L_INFO_I_START="Начинается установка" L_INFO_I_START="Начинается установка"
L_I_STAGE_1=">>> Этап 1: Проверка окружения и зависимостей" L_I_STAGE_1=">>> Этап 1: Проверка окружения и зависимостей"
L_I_STAGE_1_5=">>> Этап 1.5: Интерактивная настройка" L_I_STAGE_2=">>> Этап 2: Интерактивная настройка"
L_I_PROMPT_DOM="\nПожалуйста, укажите домен TLS\nНажмите Enter, чтобы оставить по умолчанию [%s]: " L_I_PROMPT_DOM="\nПожалуйста, укажите домен TLS\nНажмите Enter, чтобы оставить по умолчанию [%s]: "
L_I_PROMPT_PORT="\nПожалуйста, укажите порт сервера\nНажмите Enter, чтобы оставить по умолчанию [%s]: "
L_WARN_NO_TTY="Интерактивный режим недоступен (нет TTY). Используется:" L_WARN_NO_TTY="Интерактивный режим недоступен (нет TTY). Используется:"
L_I_STAGE_2=">>> Этап 2: Загрузка архива" L_I_STAGE_3=">>> Этап 3: Загрузка архива"
L_ERR_TMP_DIR="Не удалось создать временную директорию" L_ERR_TMP_DIR="Не удалось создать временную директорию"
L_ERR_TMP_INV="Временная директория недействительна" L_ERR_TMP_INV="Временная директория недействительна"
L_INFO_FALLBACK="Сборка x86_64-v3 не найдена, откат к стандартной x86_64..." L_INFO_FALLBACK="Сборка x86_64-v3 не найдена, откат к стандартной x86_64..."
L_ERR_DL_FAIL="Ошибка загрузки архива" L_ERR_DL_FAIL="Ошибка загрузки архива"
L_I_STAGE_3=">>> Этап 3: Распаковка архива" L_I_STAGE_4=">>> Этап 4: Распаковка архива"
L_ERR_EXTRACT="Ошибка распаковки архива." L_ERR_EXTRACT="Ошибка распаковки архива."
L_ERR_BIN_NOT_FOUND="Бинарный файл не найден в архиве" L_ERR_BIN_NOT_FOUND="Бинарный файл не найден в архиве"
L_I_STAGE_4=">>> Этап 4: Настройка окружения (Юзер, Группа, Папки)" L_I_STAGE_5=">>> Этап 5: Настройка окружения (Юзер, Группа, Папки)"
L_I_STAGE_5=">>> Этап 5: Установка бинарного файла" L_I_STAGE_6=">>> Этап 6: Установка бинарного файла"
L_I_STAGE_6=">>> Этап 6: Генерация/Обновление конфигурации" L_I_STAGE_7=">>> Этап 7: Генерация/Обновление конфигурации"
L_I_STAGE_7=">>> Этап 7: Установка и запуск службы" L_I_STAGE_8=">>> Этап 8: Установка и запуск службы"
L_OUT_WARN_H="УСТАНОВКА ЗАВЕРШЕНА С ПРЕДУПРЕЖДЕНИЯМИ" L_OUT_WARN_H="УСТАНОВКА ЗАВЕРШЕНА С ПРЕДУПРЕЖДЕНИЯМИ"
L_OUT_WARN_D="Служба установлена, но не запустилась.\nПожалуйста, проверьте логи.\n" L_OUT_WARN_D="Служба установлена, но не запустилась.\nПожалуйста, проверьте логи.\n"
L_OUT_SUCC_H="УСТАНОВКА УСПЕШНО ЗАВЕРШЕНА" L_OUT_SUCC_H="УСТАНОВКА УСПЕШНО ЗАВЕРШЕНА"
@@ -160,21 +161,22 @@ set_language() {
L_INFO_KEEP_CONF="Note: Configuration kept. Run with 'purge' to remove completely." L_INFO_KEEP_CONF="Note: Configuration kept. Run with 'purge' to remove completely."
L_INFO_I_START="Starting installation of" L_INFO_I_START="Starting installation of"
L_I_STAGE_1=">>> Stage 1: Verifying environment and dependencies" L_I_STAGE_1=">>> Stage 1: Verifying environment and dependencies"
L_I_STAGE_1_5=">>> Stage 1.5: Interactive Setup" L_I_STAGE_2=">>> Stage 2: Interactive Setup"
L_I_PROMPT_DOM="\nPlease specify the TLS Domain\nPress Enter to keep default [%s]: " L_I_PROMPT_DOM="\nPlease specify the TLS Domain\nPress Enter to keep default [%s]: "
L_I_PROMPT_PORT="\nPlease specify the Server Port\nPress Enter to keep default [%s]: "
L_WARN_NO_TTY="Interactive mode unavailable (no TTY). Using:" L_WARN_NO_TTY="Interactive mode unavailable (no TTY). Using:"
L_I_STAGE_2=">>> Stage 2: Downloading archive" L_I_STAGE_3=">>> Stage 3: Downloading archive"
L_ERR_TMP_DIR="Temp directory creation failed" L_ERR_TMP_DIR="Temp directory creation failed"
L_ERR_TMP_INV="Temp directory is invalid or was not created" L_ERR_TMP_INV="Temp directory is invalid or was not created"
L_INFO_FALLBACK="x86_64-v3 build not found, falling back to standard x86_64..." L_INFO_FALLBACK="x86_64-v3 build not found, falling back to standard x86_64..."
L_ERR_DL_FAIL="Download failed" L_ERR_DL_FAIL="Download failed"
L_I_STAGE_3=">>> Stage 3: Extracting archive" L_I_STAGE_4=">>> Stage 4: Extracting archive"
L_ERR_EXTRACT="Extraction failed." L_ERR_EXTRACT="Extraction failed."
L_ERR_BIN_NOT_FOUND="Binary not found in archive" L_ERR_BIN_NOT_FOUND="Binary not found in archive"
L_I_STAGE_4=">>> Stage 4: Setting up environment (User, Group, Directories)" L_I_STAGE_5=">>> Stage 5: Setting up environment (User, Group, Directories)"
L_I_STAGE_5=">>> Stage 5: Installing binary" L_I_STAGE_6=">>> Stage 6: Installing binary"
L_I_STAGE_6=">>> Stage 6: Generating/Updating configuration" L_I_STAGE_7=">>> Stage 7: Generating/Updating configuration"
L_I_STAGE_7=">>> Stage 7: Installing and starting service" L_I_STAGE_8=">>> Stage 8: Installing and starting service"
L_OUT_WARN_H="INSTALLATION COMPLETED WITH WARNINGS" L_OUT_WARN_H="INSTALLATION COMPLETED WITH WARNINGS"
L_OUT_WARN_D="The service was installed but failed to start.\nPlease check the logs to determine the issue.\n" L_OUT_WARN_D="The service was installed but failed to start.\nPlease check the logs to determine the issue.\n"
L_OUT_SUCC_H="INSTALLATION SUCCESS" L_OUT_SUCC_H="INSTALLATION SUCCESS"
@@ -269,7 +271,10 @@ say() {
if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
printf '\n' printf '\n'
else else
printf '[INFO] %s\n' "$*" case "$*" in
\[*\]*) printf '%s\n' "$*" ;;
*) printf '[INFO] %s\n' "$*" ;;
esac
fi fi
} }
die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; } die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
@@ -527,9 +532,9 @@ setup_dirs() {
stop_service() { stop_service() {
svc="$(get_svc_mgr)" svc="$(get_svc_mgr)"
if [ "$svc" = "systemd" ] && systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then if [ "$svc" = "systemd" ] && $SUDO systemctl is-active --quiet "$SERVICE_NAME" 2>/dev/null; then
$SUDO systemctl stop "$SERVICE_NAME" 2>/dev/null || true $SUDO systemctl stop "$SERVICE_NAME" 2>/dev/null || true
elif [ "$svc" = "openrc" ] && rc-service "$SERVICE_NAME" status >/dev/null 2>&1; then elif [ "$svc" = "openrc" ] && $SUDO rc-service "$SERVICE_NAME" status >/dev/null 2>&1; then
$SUDO rc-service "$SERVICE_NAME" stop 2>/dev/null || true $SUDO rc-service "$SERVICE_NAME" stop 2>/dev/null || true
fi fi
} }
@@ -832,10 +837,36 @@ case "$ACTION" in
fi fi
fi fi
check_port_availability if [ "$PORT_PROVIDED" -eq 0 ] || [ "$DOMAIN_PROVIDED" -eq 0 ]; then
say "$L_I_STAGE_2"
fi
if [ "$PORT_PROVIDED" -eq 0 ]; then
if [ -t 0 ] || [ -c /dev/tty ]; then
while true; do
printf "$L_I_PROMPT_PORT" "$SERVER_PORT"
read -r input_port </dev/tty || input_port=""
if [ -z "$input_port" ]; then
break
fi
case "$input_port" in
*[!0-9]*) printf '[ERROR] %s\n' "$L_ERR_PORT_NUM" >&2; continue ;;
esac
port_num="$(printf '%s\n' "$input_port" | sed 's/^0*//')"
[ -z "$port_num" ] && port_num="0"
if [ "${#port_num}" -gt 5 ] || [ "$port_num" -lt 1 ] || [ "$port_num" -gt 65535 ]; then
printf '[ERROR] %s\n' "$L_ERR_PORT_RANGE" >&2; continue
fi
SERVER_PORT="$port_num"
break
done
else
say "[WARNING] $L_WARN_NO_TTY $SERVER_PORT"
fi
PORT_PROVIDED=1
fi
if [ "$DOMAIN_PROVIDED" -eq 0 ]; then if [ "$DOMAIN_PROVIDED" -eq 0 ]; then
say "$L_I_STAGE_1_5"
if [ -t 0 ] || [ -c /dev/tty ]; then if [ -t 0 ] || [ -c /dev/tty ]; then
printf "$L_I_PROMPT_DOM" "$TLS_DOMAIN" printf "$L_I_PROMPT_DOM" "$TLS_DOMAIN"
read -r input_domain </dev/tty || input_domain="" read -r input_domain </dev/tty || input_domain=""
@@ -848,6 +879,8 @@ case "$ACTION" in
DOMAIN_PROVIDED=1 DOMAIN_PROVIDED=1
fi fi
check_port_availability
if [ "$TARGET_VERSION" != "latest" ]; then if [ "$TARGET_VERSION" != "latest" ]; then
TARGET_VERSION="${TARGET_VERSION#v}" TARGET_VERSION="${TARGET_VERSION#v}"
fi fi
@@ -861,7 +894,7 @@ case "$ACTION" in
DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}" DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}"
fi fi
say "$L_I_STAGE_2" say "$L_I_STAGE_3"
TEMP_DIR="$(mktemp -d)" || die "$L_ERR_TMP_DIR" TEMP_DIR="$(mktemp -d)" || die "$L_ERR_TMP_DIR"
if [ -z "$TEMP_DIR" ] || [ ! -d "$TEMP_DIR" ]; then if [ -z "$TEMP_DIR" ] || [ ! -d "$TEMP_DIR" ]; then
die "$L_ERR_TMP_INV" die "$L_ERR_TMP_INV"
@@ -883,7 +916,7 @@ case "$ACTION" in
fi fi
fi fi
say "$L_I_STAGE_3" say "$L_I_STAGE_4"
if ! gzip -dc "${TEMP_DIR}/${FILE_NAME}" | tar -xf - -C "$TEMP_DIR" 2>/dev/null; then if ! gzip -dc "${TEMP_DIR}/${FILE_NAME}" | tar -xf - -C "$TEMP_DIR" 2>/dev/null; then
die "$L_ERR_EXTRACT" die "$L_ERR_EXTRACT"
fi fi
@@ -891,16 +924,16 @@ case "$ACTION" in
EXTRACTED_BIN="$(find "$TEMP_DIR" -type f -name "$BIN_NAME" -print 2>/dev/null | head -n 1 || true)" EXTRACTED_BIN="$(find "$TEMP_DIR" -type f -name "$BIN_NAME" -print 2>/dev/null | head -n 1 || true)"
[ -n "$EXTRACTED_BIN" ] || die "$L_ERR_BIN_NOT_FOUND" [ -n "$EXTRACTED_BIN" ] || die "$L_ERR_BIN_NOT_FOUND"
say "$L_I_STAGE_4" say "$L_I_STAGE_5"
ensure_user_group; setup_dirs; stop_service ensure_user_group; setup_dirs; stop_service
say "$L_I_STAGE_5" say "$L_I_STAGE_6"
install_binary "$EXTRACTED_BIN" "${INSTALL_DIR}/${BIN_NAME}" install_binary "$EXTRACTED_BIN" "${INSTALL_DIR}/${BIN_NAME}"
say "$L_I_STAGE_6" say "$L_I_STAGE_7"
install_config install_config
say "$L_I_STAGE_7" say "$L_I_STAGE_8"
install_service install_service
if [ "${SERVICE_START_FAILED:-0}" -eq 1 ]; then if [ "${SERVICE_START_FAILED:-0}" -eq 1 ]; then

View File

@@ -1,6 +1,7 @@
use http_body_util::{BodyExt, Full}; use http_body_util::{BodyExt, Full};
use hyper::StatusCode; use hyper::StatusCode;
use hyper::body::{Bytes, Incoming}; use hyper::body::{Bytes, Incoming};
use hyper::header::ALLOW;
use serde::Serialize; use serde::Serialize;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
@@ -25,6 +26,8 @@ pub(super) fn success_response<T: Serialize>(
} }
pub(super) fn error_response(request_id: u64, failure: ApiFailure) -> hyper::Response<Full<Bytes>> { pub(super) fn error_response(request_id: u64, failure: ApiFailure) -> hyper::Response<Full<Bytes>> {
let status = failure.status;
let allow = failure.allow;
let payload = ErrorResponse { let payload = ErrorResponse {
ok: false, ok: false,
error: ErrorBody { error: ErrorBody {
@@ -40,11 +43,13 @@ pub(super) fn error_response(request_id: u64, failure: ApiFailure) -> hyper::Res
) )
.into_bytes() .into_bytes()
}); });
hyper::Response::builder() let mut builder = hyper::Response::builder()
.status(failure.status) .status(status)
.header("content-type", "application/json; charset=utf-8") .header("content-type", "application/json; charset=utf-8");
.body(Full::new(Bytes::from(body))) if let Some(allow) = allow {
.unwrap() builder = builder.header(ALLOW, allow);
}
builder.body(Full::new(Bytes::from(body))).unwrap()
} }
pub(super) async fn read_json<T: DeserializeOwned>( pub(super) async fn read_json<T: DeserializeOwned>(

View File

@@ -41,7 +41,9 @@ mod runtime_watch;
mod runtime_zero; mod runtime_zero;
mod users; mod users;
use config_store::{current_revision, load_config_from_disk, parse_if_match}; use config_store::{
current_revision, ensure_expected_revision, load_config_from_disk, parse_if_match,
};
use events::ApiEventStore; use events::ApiEventStore;
use http_utils::{error_response, read_json, read_optional_json, success_response}; use http_utils::{error_response, read_json, read_optional_json, success_response};
use model::{ use model::{
@@ -75,6 +77,10 @@ use users::{
const API_MAX_CONTROL_CONNECTIONS: usize = 1024; const API_MAX_CONTROL_CONNECTIONS: usize = 1024;
const API_HTTP_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15); const API_HTTP_CONNECTION_TIMEOUT: Duration = Duration::from_secs(15);
const ROUTE_USERNAME_ERROR: &str = "username must match [A-Za-z0-9_.-] and be 1..64 chars"; const ROUTE_USERNAME_ERROR: &str = "username must match [A-Za-z0-9_.-] and be 1..64 chars";
const ALLOW_GET: &str = "GET";
const ALLOW_POST: &str = "POST";
const ALLOW_GET_POST: &str = "GET, POST";
const ALLOW_GET_PATCH_DELETE: &str = "GET, PATCH, DELETE";
pub(super) struct ApiRuntimeState { pub(super) struct ApiRuntimeState {
pub(super) process_started_at_epoch_secs: u64, pub(super) process_started_at_epoch_secs: u64,
@@ -125,6 +131,57 @@ fn parse_route_username(user: &str) -> Result<&str, ApiFailure> {
} }
} }
fn user_action_route_matches(path: &str, suffix: &str) -> bool {
path.strip_prefix("/v1/users/")
.and_then(|path| path.strip_suffix(suffix))
.map(|user| !user.is_empty() && !user.contains('/'))
.unwrap_or(false)
}
fn allowed_methods_for_path(path: &str) -> Option<&'static str> {
match path {
"/v1/health"
| "/v1/health/ready"
| "/v1/system/info"
| "/v1/runtime/gates"
| "/v1/runtime/initialization"
| "/v1/limits/effective"
| "/v1/security/posture"
| "/v1/security/whitelist"
| "/v1/stats/summary"
| "/v1/stats/zero/all"
| "/v1/stats/upstreams"
| "/v1/stats/minimal/all"
| "/v1/stats/me-writers"
| "/v1/stats/dcs"
| "/v1/runtime/me-pool-state"
| "/v1/runtime/me_pool_state"
| "/v1/runtime/me-quality"
| "/v1/runtime/me_quality"
| "/v1/runtime/upstream-quality"
| "/v1/runtime/upstream_quality"
| "/v1/runtime/nat-stun"
| "/v1/runtime/nat_stun"
| "/v1/runtime/me-selftest"
| "/v1/runtime/connections/summary"
| "/v1/runtime/events/recent"
| "/v1/stats/users/active-ips"
| "/v1/stats/users/quota"
| "/v1/stats/users" => Some(ALLOW_GET),
"/v1/users" => Some(ALLOW_GET_POST),
_ if user_action_route_matches(path, "/reset-quota") => Some(ALLOW_POST),
_ if user_action_route_matches(path, "/rotate-secret") => Some(ALLOW_POST),
_ if path
.strip_prefix("/v1/users/")
.map(|user| !user.is_empty() && !user.contains('/'))
.unwrap_or(false) =>
{
Some(ALLOW_GET_PATCH_DELETE)
}
_ => None,
}
}
pub async fn serve( pub async fn serve(
listen: SocketAddr, listen: SocketAddr,
stats: Arc<Stats>, stats: Arc<Stats>,
@@ -435,22 +492,22 @@ async fn handle(
let data = build_dcs_data(shared.as_ref(), api_cfg).await; let data = build_dcs_data(shared.as_ref(), api_cfg).await;
Ok(success_response(StatusCode::OK, data, revision)) Ok(success_response(StatusCode::OK, data, revision))
} }
("GET", "/v1/runtime/me_pool_state") => { ("GET", "/v1/runtime/me-pool-state") | ("GET", "/v1/runtime/me_pool_state") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let data = build_runtime_me_pool_state_data(shared.as_ref()).await; let data = build_runtime_me_pool_state_data(shared.as_ref()).await;
Ok(success_response(StatusCode::OK, data, revision)) Ok(success_response(StatusCode::OK, data, revision))
} }
("GET", "/v1/runtime/me_quality") => { ("GET", "/v1/runtime/me-quality") | ("GET", "/v1/runtime/me_quality") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let data = build_runtime_me_quality_data(shared.as_ref()).await; let data = build_runtime_me_quality_data(shared.as_ref()).await;
Ok(success_response(StatusCode::OK, data, revision)) Ok(success_response(StatusCode::OK, data, revision))
} }
("GET", "/v1/runtime/upstream_quality") => { ("GET", "/v1/runtime/upstream-quality") | ("GET", "/v1/runtime/upstream_quality") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let data = build_runtime_upstream_quality_data(shared.as_ref()).await; let data = build_runtime_upstream_quality_data(shared.as_ref()).await;
Ok(success_response(StatusCode::OK, data, revision)) Ok(success_response(StatusCode::OK, data, revision))
} }
("GET", "/v1/runtime/nat_stun") => { ("GET", "/v1/runtime/nat-stun") | ("GET", "/v1/runtime/nat_stun") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let data = build_runtime_nat_stun_data(shared.as_ref()).await; let data = build_runtime_nat_stun_data(shared.as_ref()).await;
Ok(success_response(StatusCode::OK, data, revision)) Ok(success_response(StatusCode::OK, data, revision))
@@ -506,7 +563,7 @@ async fn handle(
.await; .await;
Ok(success_response(StatusCode::OK, users, revision)) Ok(success_response(StatusCode::OK, users, revision))
} }
("GET", "/v1/users/quota") => { ("GET", "/v1/stats/users/quota") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let disk_cfg = load_config_from_disk(&shared.config_path).await?; let disk_cfg = load_config_from_disk(&shared.config_path).await?;
let data = build_user_quota_list(&disk_cfg, shared.stats.as_ref()); let data = build_user_quota_list(&disk_cfg, shared.stats.as_ref());
@@ -567,6 +624,16 @@ async fn handle(
), ),
)); ));
} }
let expected_revision = parse_if_match(req.headers());
let disk_cfg = load_config_from_disk(&shared.config_path).await?;
ensure_expected_revision(&shared.config_path, expected_revision.as_deref())
.await?;
if !disk_cfg.access.users.contains_key(user) {
return Ok(error_response(
request_id,
ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "User not found"),
));
}
let snapshot = match crate::quota_state::reset_user_quota( let snapshot = match crate::quota_state::reset_user_quota(
&shared.quota_state_path, &shared.quota_state_path,
shared.stats.as_ref(), shared.stats.as_ref(),
@@ -761,16 +828,18 @@ async fn handle(
if method == Method::POST { if method == Method::POST {
return Ok(error_response( return Ok(error_response(
request_id, request_id,
ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"), ApiFailure::method_not_allowed(ALLOW_GET_PATCH_DELETE),
)); ));
} }
return Ok(error_response( return Ok(error_response(
request_id, request_id,
ApiFailure::new( ApiFailure::method_not_allowed(ALLOW_GET_PATCH_DELETE),
StatusCode::METHOD_NOT_ALLOWED, ));
"method_not_allowed", }
"Unsupported HTTP method for this route", if let Some(allow) = allowed_methods_for_path(normalized_path) {
), return Ok(error_response(
request_id,
ApiFailure::method_not_allowed(allow),
)); ));
} }
debug!( debug!(

View File

@@ -15,6 +15,7 @@ pub(super) struct ApiFailure {
pub(super) status: StatusCode, pub(super) status: StatusCode,
pub(super) code: &'static str, pub(super) code: &'static str,
pub(super) message: String, pub(super) message: String,
pub(super) allow: Option<&'static str>,
} }
impl ApiFailure { impl ApiFailure {
@@ -23,6 +24,7 @@ impl ApiFailure {
status, status,
code, code,
message: message.into(), message: message.into(),
allow: None,
} }
} }
@@ -33,6 +35,15 @@ impl ApiFailure {
pub(super) fn bad_request(message: impl Into<String>) -> Self { pub(super) fn bad_request(message: impl Into<String>) -> Self {
Self::new(StatusCode::BAD_REQUEST, "bad_request", message) Self::new(StatusCode::BAD_REQUEST, "bad_request", message)
} }
pub(super) fn method_not_allowed(allow: &'static str) -> Self {
Self {
status: StatusCode::METHOD_NOT_ALLOWED,
code: "method_not_allowed",
message: "Unsupported HTTP method for this route".to_string(),
allow: Some(allow),
}
}
} }
#[derive(Serialize)] #[derive(Serialize)]

View File

@@ -1,6 +1,7 @@
#![allow(clippy::items_after_test_module)] #![allow(clippy::items_after_test_module)]
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration; use std::time::Duration;
use tokio::sync::watch; use tokio::sync::watch;
@@ -18,8 +19,27 @@ use crate::transport::middle_proxy::{
const MAESTRO_COLOR: &str = "\x1b[92m"; const MAESTRO_COLOR: &str = "\x1b[92m";
const COLOR_RESET: &str = "\x1b[0m"; const COLOR_RESET: &str = "\x1b[0m";
static MAESTRO_COLORS_ENABLED: AtomicBool = AtomicBool::new(true);
/// Enables or disables ANSI color in direct MAESTRO status lines.
pub(crate) fn set_maestro_colors_enabled(enabled: bool) {
MAESTRO_COLORS_ENABLED.store(enabled, Ordering::Relaxed);
}
fn format_maestro_line(message: impl AsRef<str>, colors_enabled: bool) -> String {
if colors_enabled {
format!("{MAESTRO_COLOR}MAESTRO{COLOR_RESET}: {}", message.as_ref())
} else {
format!("MAESTRO: {}", message.as_ref())
}
}
/// Prints a direct MAESTRO status line outside the tracing subscriber.
pub(crate) fn print_maestro_line(message: impl AsRef<str>) { pub(crate) fn print_maestro_line(message: impl AsRef<str>) {
eprintln!("{MAESTRO_COLOR}MAESTRO{COLOR_RESET}: {}", message.as_ref()); eprintln!(
"{}",
format_maestro_line(message, MAESTRO_COLORS_ENABLED.load(Ordering::Relaxed))
);
} }
pub(crate) fn resolve_runtime_config_path( pub(crate) fn resolve_runtime_config_path(
@@ -274,11 +294,24 @@ mod tests {
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use super::{ use super::{
expected_handshake_close_description, is_expected_handshake_eof, peer_close_description, expected_handshake_close_description, format_maestro_line, is_expected_handshake_eof,
resolve_runtime_base_dir, resolve_runtime_config_path, peer_close_description, resolve_runtime_base_dir, resolve_runtime_config_path,
}; };
use crate::error::{ProxyError, StreamError}; use crate::error::{ProxyError, StreamError};
#[test]
fn maestro_line_formatter_respects_disabled_colors() {
let plain = format_maestro_line("boot", false);
assert_eq!(plain, "MAESTRO: boot");
assert!(!plain.contains('\x1b'));
}
#[test]
fn maestro_line_formatter_keeps_color_when_enabled() {
let colored = format_maestro_line("boot", true);
assert!(colored.contains("\x1b[92mMAESTRO\x1b[0m"));
}
#[test] #[test]
fn resolve_runtime_config_path_anchors_relative_to_startup_cwd() { fn resolve_runtime_config_path_anchors_relative_to_startup_cwd() {
let nonce = std::time::SystemTime::now() let nonce = std::time::SystemTime::now()

View File

@@ -49,6 +49,7 @@ use crate::transport::UpstreamManager;
use crate::transport::middle_proxy::MePool; use crate::transport::middle_proxy::MePool;
use helpers::{ use helpers::{
parse_cli, print_maestro_line, resolve_runtime_base_dir, resolve_runtime_config_path, parse_cli, print_maestro_line, resolve_runtime_base_dir, resolve_runtime_config_path,
set_maestro_colors_enabled,
}; };
#[cfg(unix)] #[cfg(unix)]
@@ -314,6 +315,7 @@ async fn run_telemt_core(
eprintln!("[telemt] Invalid network.dns_overrides: {}", e); eprintln!("[telemt] Invalid network.dns_overrides: {}", e);
std::process::exit(1); std::process::exit(1);
} }
set_maestro_colors_enabled(!config.general.disable_colors);
startup_tracker startup_tracker
.complete_component(COMPONENT_CONFIG_LOAD, Some("config is ready".to_string())) .complete_component(COMPONENT_CONFIG_LOAD, Some("config is ready".to_string()))
.await; .await;

View File

@@ -1385,6 +1385,7 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
false, false,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
Some(b"h2".to_vec()), Some(b"h2".to_vec()),
0, 0,
@@ -1509,12 +1510,22 @@ fn test_validate_tls_handshake_format() {
} }
fn build_client_hello_with_exts(exts: Vec<(u16, Vec<u8>)>, host: &str) -> Vec<u8> { fn build_client_hello_with_exts(exts: Vec<(u16, Vec<u8>)>, host: &str) -> Vec<u8> {
build_client_hello_with_ciphers_and_exts(&[[0x13, 0x01]], exts, host)
}
fn build_client_hello_with_ciphers_and_exts(
cipher_suites: &[[u8; 2]],
exts: Vec<(u16, Vec<u8>)>,
host: &str,
) -> Vec<u8> {
let mut body = Vec::new(); let mut body = Vec::new();
body.extend_from_slice(&TLS_VERSION); body.extend_from_slice(&TLS_VERSION);
body.extend_from_slice(&[0u8; 32]); body.extend_from_slice(&[0u8; 32]);
body.push(0); body.push(0);
body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
body.extend_from_slice(&[0x13, 0x01]); for suite in cipher_suites {
body.extend_from_slice(suite);
}
body.push(1); body.push(1);
body.push(0); body.push(0);
@@ -1654,6 +1665,52 @@ fn detect_client_hello_tls_version_rejects_malformed_supported_versions() {
assert!(detect_client_hello_tls_version(&ch).is_none()); assert!(detect_client_hello_tls_version(&ch).is_none());
} }
#[test]
fn select_server_hello_cipher_suite_keeps_profile_cipher_when_offered() {
let ch = build_client_hello_with_ciphers_and_exts(
&[[0x13, 0x01], [0x13, 0x03]],
Vec::new(),
"example.com",
);
assert_eq!(
select_server_hello_cipher_suite(&ch, [0x13, 0x03]),
[0x13, 0x03]
);
}
#[test]
fn select_server_hello_cipher_suite_ignores_profile_tls12_cipher() {
let ch = build_client_hello_with_ciphers_and_exts(
&[[0xc0, 0x2f], [0x13, 0x03]],
Vec::new(),
"example.com",
);
assert_eq!(
select_server_hello_cipher_suite(&ch, [0xc0, 0x2f]),
[0x13, 0x03]
);
}
#[test]
fn select_server_hello_cipher_suite_falls_back_to_offered_tls13_suite() {
let ch = build_client_hello_with_ciphers_and_exts(&[[0x13, 0x03]], Vec::new(), "example.com");
assert_eq!(
select_server_hello_cipher_suite(&ch, [0x13, 0x01]),
[0x13, 0x03]
);
}
#[test]
fn select_server_hello_cipher_suite_keeps_preferred_for_malformed_clienthello() {
let mut ch =
build_client_hello_with_ciphers_and_exts(&[[0x13, 0x03]], Vec::new(), "example.com");
ch.truncate(12);
assert_eq!(
select_server_hello_cipher_suite(&ch, [0x13, 0x01]),
[0x13, 0x01]
);
}
#[test] #[test]
fn extract_sni_rejects_zero_length_host_name() { fn extract_sni_rejects_zero_length_host_name() {
let mut sni_ext = Vec::new(); let mut sni_ext = Vec::new();
@@ -2179,7 +2236,7 @@ fn light_fuzz_boot_time_timestamp_matrix_with_short_replay_window_obeys_boot_cap
} }
#[test] #[test]
fn server_hello_application_data_contains_alpn_marker_when_selected() { fn server_hello_application_data_omits_alpn_marker_when_selected() {
let secret = b"alpn_marker_test"; let secret = b"alpn_marker_test";
let client_digest = [0x55u8; TLS_DIGEST_LEN]; let client_digest = [0x55u8; TLS_DIGEST_LEN];
let session_id = vec![0xAB; 32]; let session_id = vec![0xAB; 32];
@@ -2206,8 +2263,8 @@ fn server_hello_application_data_contains_alpn_marker_when_selected() {
assert!( assert!(
app_payload app_payload
.windows(expected.len()) .windows(expected.len())
.any(|window| window == expected), .all(|window| window != expected),
"first application payload must carry ALPN marker for selected protocol" "first application payload must not expose plaintext ALPN marker bytes"
); );
} }
@@ -2303,14 +2360,14 @@ fn server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
} }
#[test] #[test]
fn server_hello_embeds_full_alpn_marker_when_it_exactly_fits_fake_cert_len() { fn server_hello_omits_alpn_marker_even_when_it_would_fit_fake_cert_len() {
let secret = b"alpn_exact_fit_test"; let secret = b"alpn_exact_fit_test";
let client_digest = [0x58u8; TLS_DIGEST_LEN]; let client_digest = [0x58u8; TLS_DIGEST_LEN];
let session_id = vec![0xA5; 32]; let session_id = vec![0xA5; 32];
let rng = crate::crypto::SecureRandom::new(); let rng = crate::crypto::SecureRandom::new();
let proto = vec![b'z'; 57]; let proto = vec![b'z'; 57];
// marker_len = 4 + (2 + (1 + proto_len)) = 7 + proto_len = 64 // marker_len = 4 + (2 + (1 + proto_len)) = 7 + proto_len = 64.
let response = build_server_hello( let response = build_server_hello(
secret, secret,
&client_digest, &client_digest,
@@ -2336,7 +2393,7 @@ fn server_hello_embeds_full_alpn_marker_when_it_exactly_fits_fake_cert_len() {
expected_marker.extend_from_slice(&proto); expected_marker.extend_from_slice(&proto);
assert_eq!(app_payload.len(), expected_marker.len()); assert_eq!(app_payload.len(), expected_marker.len());
assert_eq!(app_payload, expected_marker.as_slice()); assert_ne!(app_payload, expected_marker.as_slice());
} }
#[test] #[test]

View File

@@ -105,6 +105,8 @@ mod extension_type {
/// TLS Cipher Suites /// TLS Cipher Suites
mod cipher_suite { mod cipher_suite {
pub const TLS_AES_128_GCM_SHA256: [u8; 2] = [0x13, 0x01]; pub const TLS_AES_128_GCM_SHA256: [u8; 2] = [0x13, 0x01];
pub const TLS_AES_256_GCM_SHA384: [u8; 2] = [0x13, 0x02];
pub const TLS_CHACHA20_POLY1305_SHA256: [u8; 2] = [0x13, 0x03];
} }
/// TLS Named Curves /// TLS Named Curves
@@ -241,6 +243,13 @@ impl ServerHelloBuilder {
self self
} }
fn with_cipher_suite(mut self, cipher_suite: [u8; 2]) -> Self {
if cipher_suite != [0, 0] {
self.cipher_suite = cipher_suite;
}
self
}
/// Build ServerHello message (without record header) /// Build ServerHello message (without record header)
fn build_message(&self) -> Vec<u8> { fn build_message(&self) -> Vec<u8> {
let Ok(session_id_len) = u8::try_from(self.session_id.len()) else { let Ok(session_id_len) = u8::try_from(self.session_id.len()) else {
@@ -520,6 +529,33 @@ pub fn build_server_hello(
rng: &SecureRandom, rng: &SecureRandom,
alpn: Option<Vec<u8>>, alpn: Option<Vec<u8>>,
new_session_tickets: u8, new_session_tickets: u8,
) -> Vec<u8> {
build_server_hello_with_cipher(
secret,
client_digest,
session_id,
fake_cert_len,
rng,
cipher_suite::TLS_AES_128_GCM_SHA256,
alpn,
new_session_tickets,
)
}
/// Build TLS ServerHello response with a caller-selected cipher suite.
///
/// The caller is responsible for selecting a suite that is compatible with the
/// already-authenticated ClientHello. Keeping the selection outside this
/// builder avoids extra ClientHello parsing in the response construction path.
pub(crate) fn build_server_hello_with_cipher(
secret: &[u8],
client_digest: &[u8; TLS_DIGEST_LEN],
session_id: &[u8],
fake_cert_len: usize,
rng: &SecureRandom,
selected_cipher_suite: [u8; 2],
alpn: Option<Vec<u8>>,
new_session_tickets: u8,
) -> Vec<u8> { ) -> Vec<u8> {
const MIN_APP_DATA: usize = 64; const MIN_APP_DATA: usize = 64;
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE; const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
@@ -528,6 +564,7 @@ pub fn build_server_hello(
// Build ServerHello // Build ServerHello
let server_hello = ServerHelloBuilder::new(session_id.to_vec()) let server_hello = ServerHelloBuilder::new(session_id.to_vec())
.with_cipher_suite(selected_cipher_suite)
.with_x25519_key(&x25519_key) .with_x25519_key(&x25519_key)
.with_tls13_version() .with_tls13_version()
.build_record(); .build_record();
@@ -538,28 +575,14 @@ pub fn build_server_hello(
TLS_VERSION[0], TLS_VERSION[0],
TLS_VERSION[1], TLS_VERSION[1],
0x00, 0x00,
0x01, // length = 1 0x01,
0x01, // CCS byte 0x01,
]; ];
// Build first encrypted flight mimic as opaque ApplicationData bytes. // Build first encrypted flight mimic as opaque ApplicationData bytes.
// Embed a compact EncryptedExtensions-like ALPN block when selected. // ALPN belongs inside encrypted EncryptedExtensions in real TLS 1.3.
let mut fake_cert = Vec::with_capacity(fake_cert_len); let mut fake_cert = Vec::with_capacity(fake_cert_len);
if let Some(proto) = alpn let _ = alpn;
.as_ref()
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize)
{
let proto_list_len = 1usize + proto.len();
let ext_data_len = 2usize + proto_list_len;
let marker_len = 4usize + ext_data_len;
if marker_len <= fake_cert_len {
fake_cert.extend_from_slice(&0x0010u16.to_be_bytes());
fake_cert.extend_from_slice(&(ext_data_len as u16).to_be_bytes());
fake_cert.extend_from_slice(&(proto_list_len as u16).to_be_bytes());
fake_cert.push(proto.len() as u8);
fake_cert.extend_from_slice(proto);
}
}
if fake_cert.len() < fake_cert_len { if fake_cert.len() < fake_cert_len {
fake_cert.extend_from_slice(&rng.bytes(fake_cert_len - fake_cert.len())); fake_cert.extend_from_slice(&rng.bytes(fake_cert_len - fake_cert.len()));
} else if fake_cert.len() > fake_cert_len { } else if fake_cert.len() > fake_cert_len {
@@ -580,7 +603,7 @@ pub fn build_server_hello(
let ticket_count = new_session_tickets.min(4); let ticket_count = new_session_tickets.min(4);
if ticket_count > 0 { if ticket_count > 0 {
for _ in 0..ticket_count { for _ in 0..ticket_count {
let ticket_len: usize = rng.range(48) + 48; // 48-95 bytes let ticket_len: usize = rng.range(48) + 48;
let mut record = Vec::with_capacity(5 + ticket_len); let mut record = Vec::with_capacity(5 + ticket_len);
record.push(TLS_RECORD_APPLICATION); record.push(TLS_RECORD_APPLICATION);
record.extend_from_slice(&TLS_VERSION); record.extend_from_slice(&TLS_VERSION);
@@ -927,6 +950,112 @@ pub fn detect_client_hello_tls_version(handshake: &[u8]) -> Option<ClientHelloTl
} }
} }
fn client_hello_cipher_suites_range(handshake: &[u8]) -> Option<(usize, usize)> {
if handshake.len() < 5 || handshake[0] != TLS_RECORD_HANDSHAKE {
return None;
}
let record_len = u16::from_be_bytes([handshake[3], handshake[4]]) as usize;
let record_end = 5usize.checked_add(record_len)?;
if record_end > handshake.len() {
return None;
}
let mut pos = 5;
if handshake.get(pos) != Some(&0x01) {
return None;
}
pos += 1;
if pos + 3 > record_end {
return None;
}
let handshake_len = ((handshake[pos] as usize) << 16)
| ((handshake[pos + 1] as usize) << 8)
| handshake[pos + 2] as usize;
pos += 3;
let handshake_end = pos.checked_add(handshake_len)?;
if handshake_end > record_end {
return None;
}
if pos + 2 + 32 > handshake_end {
return None;
}
pos += 2 + 32;
let session_id_len = *handshake.get(pos)? as usize;
pos = pos.checked_add(1)?.checked_add(session_id_len)?;
if pos + 2 > handshake_end {
return None;
}
let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
if cipher_len == 0 || cipher_len % 2 != 0 {
return None;
}
pos += 2;
let cipher_end = pos.checked_add(cipher_len)?;
if cipher_end > handshake_end {
return None;
}
Some((pos, cipher_end))
}
fn client_hello_offers_cipher_suite(
handshake: &[u8],
range: (usize, usize),
suite: [u8; 2],
) -> bool {
let mut pos = range.0;
while pos + 1 < range.1 {
if handshake[pos] == suite[0] && handshake[pos + 1] == suite[1] {
return true;
}
pos += 2;
}
false
}
fn is_tls13_cipher_suite(suite: [u8; 2]) -> bool {
suite == cipher_suite::TLS_AES_128_GCM_SHA256
|| suite == cipher_suite::TLS_AES_256_GCM_SHA384
|| suite == cipher_suite::TLS_CHACHA20_POLY1305_SHA256
}
/// Select the ServerHello cipher suite from the already-received ClientHello.
///
/// This is intentionally a borrowed, zero-allocation scan. It runs only for an
/// authenticated success response and keeps malformed or unexpected ClientHello
/// shapes on the previous fallback behavior.
pub(crate) fn select_server_hello_cipher_suite(handshake: &[u8], preferred: [u8; 2]) -> [u8; 2] {
let preferred = if is_tls13_cipher_suite(preferred) {
preferred
} else {
cipher_suite::TLS_AES_128_GCM_SHA256
};
let Some(range) = client_hello_cipher_suites_range(handshake) else {
return preferred;
};
if client_hello_offers_cipher_suite(handshake, range, preferred) {
return preferred;
}
for fallback in [
cipher_suite::TLS_AES_128_GCM_SHA256,
cipher_suite::TLS_CHACHA20_POLY1305_SHA256,
cipher_suite::TLS_AES_256_GCM_SHA384,
] {
if client_hello_offers_cipher_suite(handshake, range, fallback) {
return fallback;
}
}
preferred
}
/// Check if bytes look like a TLS ClientHello /// Check if bytes look like a TLS ClientHello
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool { pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
if first_bytes.len() < 3 { if first_bytes.len() < 3 {

View File

@@ -1504,6 +1504,13 @@ where
let validation_session_id_slice = &validation_session_id[..validation_session_id_len]; let validation_session_id_slice = &validation_session_id[..validation_session_id_len];
let response = if let Some((cached_entry, use_full_cert_payload)) = cached { let response = if let Some((cached_entry, use_full_cert_payload)) = cached {
let preferred_cipher_suite = if cached_entry.server_hello_template.cipher_suite == [0, 0] {
[0x13, 0x01]
} else {
cached_entry.server_hello_template.cipher_suite
};
let selected_cipher_suite =
tls::select_server_hello_cipher_suite(handshake, preferred_cipher_suite);
emulator::build_emulated_server_hello( emulator::build_emulated_server_hello(
&validated_secret, &validated_secret,
&validation_digest, &validation_digest,
@@ -1512,17 +1519,20 @@ where
use_full_cert_payload, use_full_cert_payload,
config.censorship.serverhello_compact, config.censorship.serverhello_compact,
client_tls_version, client_tls_version,
selected_cipher_suite,
rng, rng,
selected_alpn.clone(), selected_alpn.clone(),
config.censorship.tls_new_session_tickets, config.censorship.tls_new_session_tickets,
) )
} else { } else {
tls::build_server_hello( let selected_cipher_suite = tls::select_server_hello_cipher_suite(handshake, [0x13, 0x01]);
tls::build_server_hello_with_cipher(
&validated_secret, &validated_secret,
&validation_digest, &validation_digest,
validation_session_id_slice, validation_session_id_slice,
config.censorship.fake_cert_len, config.censorship.fake_cert_len,
rng, rng,
selected_cipher_suite,
selected_alpn.clone(), selected_alpn.clone(),
config.censorship.tls_new_session_tickets, config.censorship.tls_new_session_tickets,
) )

View File

@@ -1,6 +1,6 @@
use std::collections::BTreeSet;
#[cfg(test)] #[cfg(test)]
use std::collections::hash_map::DefaultHasher; use std::collections::hash_map::DefaultHasher;
use std::collections::{BTreeSet, HashMap};
#[cfg(test)] #[cfg(test)]
use std::future::Future; use std::future::Future;
#[cfg(test)] #[cfg(test)]

View File

@@ -1,4 +1,5 @@
use super::*; use super::*;
use dashmap::DashMap;
mod read; mod read;
@@ -10,10 +11,10 @@ pub(crate) use self::read::{
#[derive(Default)] #[derive(Default)]
pub(crate) struct RelayIdleCandidateRegistry { pub(crate) struct RelayIdleCandidateRegistry {
pub(in crate::proxy::middle_relay) by_conn_id: HashMap<u64, RelayIdleCandidateMeta>, pub(in crate::proxy::middle_relay) by_conn_id: DashMap<u64, RelayIdleCandidateMeta>,
pub(in crate::proxy::middle_relay) ordered: BTreeSet<(u64, u64)>, pub(in crate::proxy::middle_relay) ordered: parking_lot::Mutex<BTreeSet<(u64, u64)>>,
pressure_event_seq: u64, pressure_event_seq: AtomicU64,
pressure_consumed_seq: u64, pressure_consumed_seq: AtomicU64,
} }
/// Queue metadata used to preserve FIFO ordering for idle relay eviction. /// Queue metadata used to preserve FIFO ordering for idle relay eviction.
@@ -23,25 +24,10 @@ pub(in crate::proxy::middle_relay) struct RelayIdleCandidateMeta {
pub(in crate::proxy::middle_relay) mark_pressure_seq: u64, pub(in crate::proxy::middle_relay) mark_pressure_seq: u64,
} }
pub(super) fn relay_idle_candidate_registry_lock_in(
shared: &ProxySharedState,
) -> std::sync::MutexGuard<'_, RelayIdleCandidateRegistry> {
let registry = &shared.middle_relay.relay_idle_registry;
match registry.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let mut guard = poisoned.into_inner();
*guard = RelayIdleCandidateRegistry::default();
registry.clear_poison();
guard
}
}
}
pub(super) fn mark_relay_idle_candidate_in(shared: &ProxySharedState, conn_id: u64) -> bool { pub(super) fn mark_relay_idle_candidate_in(shared: &ProxySharedState, conn_id: u64) -> bool {
let mut guard = relay_idle_candidate_registry_lock_in(shared); let registry = &shared.middle_relay.relay_idle_registry;
if guard.by_conn_id.contains_key(&conn_id) { if registry.by_conn_id.contains_key(&conn_id) {
return false; return false;
} }
@@ -52,24 +38,38 @@ pub(super) fn mark_relay_idle_candidate_in(shared: &ProxySharedState, conn_id: u
.saturating_add(1); .saturating_add(1);
let meta = RelayIdleCandidateMeta { let meta = RelayIdleCandidateMeta {
mark_order_seq, mark_order_seq,
mark_pressure_seq: guard.pressure_event_seq, mark_pressure_seq: registry.pressure_event_seq.load(Ordering::Relaxed),
}; };
guard.by_conn_id.insert(conn_id, meta); match registry.by_conn_id.entry(conn_id) {
guard.ordered.insert((meta.mark_order_seq, conn_id)); dashmap::mapref::entry::Entry::Occupied(_) => false,
true dashmap::mapref::entry::Entry::Vacant(entry) => {
entry.insert(meta);
registry
.ordered
.lock()
.insert((meta.mark_order_seq, conn_id));
true
}
}
} }
pub(super) fn clear_relay_idle_candidate_in(shared: &ProxySharedState, conn_id: u64) { pub(super) fn clear_relay_idle_candidate_in(shared: &ProxySharedState, conn_id: u64) {
let mut guard = relay_idle_candidate_registry_lock_in(shared); let registry = &shared.middle_relay.relay_idle_registry;
if let Some(meta) = guard.by_conn_id.remove(&conn_id) { if let Some((_, meta)) = registry.by_conn_id.remove(&conn_id) {
guard.ordered.remove(&(meta.mark_order_seq, conn_id)); registry
.ordered
.lock()
.remove(&(meta.mark_order_seq, conn_id));
} }
} }
pub(super) fn note_relay_pressure_event_in(shared: &ProxySharedState) { pub(super) fn note_relay_pressure_event_in(shared: &ProxySharedState) {
let mut guard = relay_idle_candidate_registry_lock_in(shared); shared
guard.pressure_event_seq = guard.pressure_event_seq.wrapping_add(1); .middle_relay
.relay_idle_registry
.pressure_event_seq
.fetch_add(1, Ordering::Relaxed);
} }
pub(crate) fn note_global_relay_pressure(shared: &ProxySharedState) { pub(crate) fn note_global_relay_pressure(shared: &ProxySharedState) {
@@ -77,8 +77,11 @@ pub(crate) fn note_global_relay_pressure(shared: &ProxySharedState) {
} }
pub(super) fn relay_pressure_event_seq_in(shared: &ProxySharedState) -> u64 { pub(super) fn relay_pressure_event_seq_in(shared: &ProxySharedState) -> u64 {
let guard = relay_idle_candidate_registry_lock_in(shared); shared
guard.pressure_event_seq .middle_relay
.relay_idle_registry
.pressure_event_seq
.load(Ordering::Relaxed)
} }
pub(super) fn maybe_evict_idle_candidate_on_pressure_in( pub(super) fn maybe_evict_idle_candidate_on_pressure_in(
@@ -87,33 +90,52 @@ pub(super) fn maybe_evict_idle_candidate_on_pressure_in(
seen_pressure_seq: &mut u64, seen_pressure_seq: &mut u64,
stats: &Stats, stats: &Stats,
) -> bool { ) -> bool {
let mut guard = relay_idle_candidate_registry_lock_in(shared); let registry = &shared.middle_relay.relay_idle_registry;
let latest_pressure_seq = guard.pressure_event_seq; let latest_pressure_seq = registry.pressure_event_seq.load(Ordering::Relaxed);
if latest_pressure_seq == *seen_pressure_seq { if latest_pressure_seq == *seen_pressure_seq {
return false; return false;
} }
*seen_pressure_seq = latest_pressure_seq; *seen_pressure_seq = latest_pressure_seq;
if latest_pressure_seq == guard.pressure_consumed_seq { let consumed_pressure_seq = registry.pressure_consumed_seq.load(Ordering::Relaxed);
if latest_pressure_seq == consumed_pressure_seq {
return false; return false;
} }
if guard.ordered.is_empty() { let oldest = {
guard.pressure_consumed_seq = latest_pressure_seq; let mut ordered = registry.ordered.lock();
return false; loop {
} let Some((mark_order_seq, candidate_conn_id)) = ordered.iter().next().copied() else {
// Empty queues consume the event so later candidates cannot replay stale pressure.
let oldest = guard let _ = registry.pressure_consumed_seq.compare_exchange(
.ordered consumed_pressure_seq,
.iter() latest_pressure_seq,
.next() Ordering::Relaxed,
.map(|(_, candidate_conn_id)| *candidate_conn_id); Ordering::Relaxed,
);
return false;
};
let Some(candidate_meta) = registry.by_conn_id.get(&candidate_conn_id) else {
ordered.remove(&(mark_order_seq, candidate_conn_id));
continue;
};
if candidate_meta.mark_order_seq != mark_order_seq {
ordered.remove(&(mark_order_seq, candidate_conn_id));
continue;
}
break Some(candidate_conn_id);
}
};
if oldest != Some(conn_id) { if oldest != Some(conn_id) {
return false; return false;
} }
let Some(candidate_meta) = guard.by_conn_id.get(&conn_id).copied() else { let Some(candidate_meta) = registry
.by_conn_id
.get(&conn_id)
.map(|entry| *entry.value())
else {
return false; return false;
}; };
@@ -121,10 +143,27 @@ pub(super) fn maybe_evict_idle_candidate_on_pressure_in(
return false; return false;
} }
if let Some(meta) = guard.by_conn_id.remove(&conn_id) { // Claim the global pressure budget before removal; otherwise racing sessions
guard.ordered.remove(&(meta.mark_order_seq, conn_id)); // can observe the next FIFO item and spend the same event more than once.
if registry
.pressure_consumed_seq
.compare_exchange(
consumed_pressure_seq,
latest_pressure_seq,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_err()
{
return false;
}
if let Some((_, meta)) = registry.by_conn_id.remove(&conn_id) {
registry
.ordered
.lock()
.remove(&(meta.mark_order_seq, conn_id));
} }
guard.pressure_consumed_seq = latest_pressure_seq;
stats.increment_relay_pressure_evict_total(); stats.increment_relay_pressure_evict_total();
true true
} }
@@ -220,72 +259,32 @@ pub(crate) fn mark_relay_idle_candidate_for_testing(
shared: &ProxySharedState, shared: &ProxySharedState,
conn_id: u64, conn_id: u64,
) -> bool { ) -> bool {
let registry = &shared.middle_relay.relay_idle_registry; mark_relay_idle_candidate_in(shared, conn_id)
let mut guard = match registry.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let mut guard = poisoned.into_inner();
*guard = RelayIdleCandidateRegistry::default();
registry.clear_poison();
guard
}
};
if guard.by_conn_id.contains_key(&conn_id) {
return false;
}
let mark_order_seq = shared
.middle_relay
.relay_idle_mark_seq
.fetch_add(1, Ordering::Relaxed);
let mark_pressure_seq = guard.pressure_event_seq;
let meta = RelayIdleCandidateMeta {
mark_order_seq,
mark_pressure_seq,
};
guard.by_conn_id.insert(conn_id, meta);
guard.ordered.insert((mark_order_seq, conn_id));
true
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn oldest_relay_idle_candidate_for_testing(shared: &ProxySharedState) -> Option<u64> { pub(crate) fn oldest_relay_idle_candidate_for_testing(shared: &ProxySharedState) -> Option<u64> {
let registry = &shared.middle_relay.relay_idle_registry; let registry = &shared.middle_relay.relay_idle_registry;
let guard = match registry.lock() { registry
Ok(guard) => guard, .ordered
Err(poisoned) => { .lock()
let mut guard = poisoned.into_inner(); .iter()
*guard = RelayIdleCandidateRegistry::default(); .next()
registry.clear_poison(); .map(|(_, conn_id)| *conn_id)
guard
}
};
guard.ordered.iter().next().map(|(_, conn_id)| *conn_id)
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn clear_relay_idle_candidate_for_testing(shared: &ProxySharedState, conn_id: u64) { pub(crate) fn clear_relay_idle_candidate_for_testing(shared: &ProxySharedState, conn_id: u64) {
let registry = &shared.middle_relay.relay_idle_registry; clear_relay_idle_candidate_in(shared, conn_id);
let mut guard = match registry.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let mut guard = poisoned.into_inner();
*guard = RelayIdleCandidateRegistry::default();
registry.clear_poison();
guard
}
};
if let Some(meta) = guard.by_conn_id.remove(&conn_id) {
guard.ordered.remove(&(meta.mark_order_seq, conn_id));
}
} }
#[cfg(test)] #[cfg(test)]
pub(crate) fn clear_relay_idle_pressure_state_for_testing_in_shared(shared: &ProxySharedState) { pub(crate) fn clear_relay_idle_pressure_state_for_testing_in_shared(shared: &ProxySharedState) {
if let Ok(mut guard) = shared.middle_relay.relay_idle_registry.lock() { let registry = &shared.middle_relay.relay_idle_registry;
*guard = RelayIdleCandidateRegistry::default(); registry.by_conn_id.clear();
} registry.ordered.lock().clear();
registry.pressure_event_seq.store(0, Ordering::Relaxed);
registry.pressure_consumed_seq.store(0, Ordering::Relaxed);
shared shared
.middle_relay .middle_relay
.relay_idle_mark_seq .relay_idle_mark_seq
@@ -327,15 +326,10 @@ pub(crate) fn set_relay_pressure_state_for_testing(
pressure_consumed_seq: u64, pressure_consumed_seq: u64,
) { ) {
let registry = &shared.middle_relay.relay_idle_registry; let registry = &shared.middle_relay.relay_idle_registry;
let mut guard = match registry.lock() { registry
Ok(guard) => guard, .pressure_event_seq
Err(poisoned) => { .store(pressure_event_seq, Ordering::Relaxed);
let mut guard = poisoned.into_inner(); registry
*guard = RelayIdleCandidateRegistry::default(); .pressure_consumed_seq
registry.clear_poison(); .store(pressure_consumed_seq, Ordering::Relaxed);
guard
}
};
guard.pressure_event_seq = pressure_event_seq;
guard.pressure_consumed_seq = pressure_consumed_seq;
} }

View File

@@ -41,11 +41,12 @@ pub(super) async fn reserve_user_quota_with_yield(
return Err(MiddleQuotaReserveError::DeadlineExceeded); return Err(MiddleQuotaReserveError::DeadlineExceeded);
} }
tokio::select! { tokio::select! {
_ = tokio::time::sleep(Duration::from_millis(backoff_ms)) => {} biased;
_ = cancel.cancelled() => { _ = cancel.cancelled() => {
stats.increment_quota_acquire_cancelled_total(); stats.increment_quota_acquire_cancelled_total();
return Err(MiddleQuotaReserveError::Cancelled); return Err(MiddleQuotaReserveError::Cancelled);
} }
_ = tokio::time::sleep(Duration::from_millis(backoff_ms)) => {}
} }
backoff_rounds = backoff_rounds.saturating_add(1); backoff_rounds = backoff_rounds.saturating_add(1);
if backoff_rounds >= QUOTA_RESERVE_MAX_BACKOFF_ROUNDS { if backoff_rounds >= QUOTA_RESERVE_MAX_BACKOFF_ROUNDS {
@@ -128,11 +129,12 @@ pub(super) async fn wait_for_traffic_budget_or_cancel(
return Err(ProxyError::TrafficBudgetWaitDeadlineExceeded); return Err(ProxyError::TrafficBudgetWaitDeadlineExceeded);
} }
tokio::select! { tokio::select! {
_ = tokio::time::sleep(next_refill_delay()) => {} biased;
_ = cancel.cancelled() => { _ = cancel.cancelled() => {
stats.increment_flow_wait_middle_rate_limit_cancelled_total(); stats.increment_flow_wait_middle_rate_limit_cancelled_total();
return Err(ProxyError::TrafficBudgetWaitCancelled); return Err(ProxyError::TrafficBudgetWaitCancelled);
} }
_ = tokio::time::sleep(next_refill_delay()) => {}
} }
let wait_ms = wait_started_at let wait_ms = wait_started_at
.elapsed() .elapsed()

View File

@@ -59,7 +59,7 @@ pub(crate) struct MiddleRelaySharedState {
pub(crate) desync_hasher: RandomState, pub(crate) desync_hasher: RandomState,
pub(crate) desync_full_cache_last_emit_at: Mutex<Option<Instant>>, pub(crate) desync_full_cache_last_emit_at: Mutex<Option<Instant>>,
pub(crate) desync_dedup_rotation_state: Mutex<DesyncDedupRotationState>, pub(crate) desync_dedup_rotation_state: Mutex<DesyncDedupRotationState>,
pub(crate) relay_idle_registry: Mutex<RelayIdleCandidateRegistry>, pub(crate) relay_idle_registry: RelayIdleCandidateRegistry,
pub(crate) relay_idle_mark_seq: AtomicU64, pub(crate) relay_idle_mark_seq: AtomicU64,
} }
@@ -97,7 +97,7 @@ impl ProxySharedState {
desync_hasher: RandomState::new(), desync_hasher: RandomState::new(),
desync_full_cache_last_emit_at: Mutex::new(None), desync_full_cache_last_emit_at: Mutex::new(None),
desync_dedup_rotation_state: Mutex::new(DesyncDedupRotationState::default()), desync_dedup_rotation_state: Mutex::new(DesyncDedupRotationState::default()),
relay_idle_registry: Mutex::new(RelayIdleCandidateRegistry::default()), relay_idle_registry: RelayIdleCandidateRegistry::default(),
relay_idle_mark_seq: AtomicU64::new(0), relay_idle_mark_seq: AtomicU64::new(0),
}, },
traffic_limiter: TrafficLimiter::new(), traffic_limiter: TrafficLimiter::new(),

View File

@@ -1,33 +1,21 @@
use super::*; use super::*;
use std::panic::{AssertUnwindSafe, catch_unwind};
#[test] #[test]
fn blackhat_registry_poison_recovers_with_fail_closed_reset_and_pressure_accounting() { fn blackhat_registry_stale_order_entry_is_skipped_and_pressure_accounting_continues() {
let shared = ProxySharedState::new(); let shared = ProxySharedState::new();
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref()); clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
let _ = catch_unwind(AssertUnwindSafe(|| { shared
let mut guard = shared .middle_relay
.middle_relay .relay_idle_registry
.relay_idle_registry .ordered
.lock() .lock()
.expect("registry lock must be acquired before poison"); .insert((0, 999));
guard.by_conn_id.insert(
999,
RelayIdleCandidateMeta {
mark_order_seq: 1,
mark_pressure_seq: 0,
},
);
guard.ordered.insert((1, 999));
panic!("intentional poison for idle-registry recovery");
}));
// Helper lock must recover from poison, reset stale state, and continue.
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 42)); assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 42));
assert_eq!( assert_eq!(
oldest_relay_idle_candidate_for_testing(shared.as_ref()), oldest_relay_idle_candidate_for_testing(shared.as_ref()),
Some(42) Some(999)
); );
let before = relay_pressure_event_seq_for_testing(shared.as_ref()); let before = relay_pressure_event_seq_for_testing(shared.as_ref());
@@ -35,25 +23,43 @@ fn blackhat_registry_poison_recovers_with_fail_closed_reset_and_pressure_account
let after = relay_pressure_event_seq_for_testing(shared.as_ref()); let after = relay_pressure_event_seq_for_testing(shared.as_ref());
assert!( assert!(
after > before, after > before,
"pressure accounting must still advance after poison" "pressure accounting must still advance with stale ordered entries"
);
let mut seen_pressure_seq = before;
assert!(maybe_evict_idle_candidate_on_pressure_for_testing(
shared.as_ref(),
42,
&mut seen_pressure_seq,
&Stats::new()
));
assert_eq!(
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
None
); );
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref()); clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
} }
#[test] #[test]
fn clear_state_helper_must_reset_poisoned_registry_for_deterministic_fifo_tests() { fn clear_state_helper_must_reset_split_registry_for_deterministic_fifo_tests() {
let shared = ProxySharedState::new(); let shared = ProxySharedState::new();
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref()); clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
let _ = catch_unwind(AssertUnwindSafe(|| { shared.middle_relay.relay_idle_registry.by_conn_id.insert(
let _guard = shared 999,
.middle_relay RelayIdleCandidateMeta {
.relay_idle_registry mark_order_seq: 1,
.lock() mark_pressure_seq: 0,
.expect("registry lock must be acquired before poison"); },
panic!("intentional poison while lock held"); );
})); shared
.middle_relay
.relay_idle_registry
.ordered
.lock()
.insert((1, 999));
set_relay_pressure_state_for_testing(shared.as_ref(), 7, 6);
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref()); clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());

View File

@@ -8,12 +8,17 @@ use crate::protocol::constants::{
use crate::protocol::tls::{ use crate::protocol::tls::{
ClientHelloTlsVersion, TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key, ClientHelloTlsVersion, TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key,
}; };
use crate::tls_front::types::{CachedTlsData, ParsedCertificateInfo, TlsProfileSource}; use crate::tls_front::types::{
CachedTlsData, ParsedCertificateInfo, TlsExtension, TlsProfileSource,
};
use crc32fast::Hasher; use crc32fast::Hasher;
const MIN_APP_DATA: usize = 64; const MIN_APP_DATA: usize = 64;
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE; const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
const MAX_TICKET_RECORDS: usize = 4; const MAX_TICKET_RECORDS: usize = 4;
const EXT_SUPPORTED_VERSIONS: u16 = 0x002b;
const EXT_KEY_SHARE: u16 = 0x0033;
const EXT_ALPN: u16 = 0x0010;
fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> { fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> {
sizes sizes
@@ -185,6 +190,74 @@ fn hash_compact_cert_info_payload(cert_payload: Vec<u8>) -> Option<Vec<u8>> {
Some(hashed) Some(hashed)
} }
fn push_supported_versions_extension(extensions: &mut Vec<u8>) {
extensions.extend_from_slice(&EXT_SUPPORTED_VERSIONS.to_be_bytes());
extensions.extend_from_slice(&(2u16).to_be_bytes());
extensions.extend_from_slice(&0x0304u16.to_be_bytes());
}
fn push_key_share_extension(extensions: &mut Vec<u8>, rng: &SecureRandom) {
let key = gen_fake_x25519_key(rng);
extensions.extend_from_slice(&EXT_KEY_SHARE.to_be_bytes());
extensions.extend_from_slice(&(2 + 2 + 32u16).to_be_bytes());
extensions.extend_from_slice(&0x001du16.to_be_bytes());
extensions.extend_from_slice(&(32u16).to_be_bytes());
extensions.extend_from_slice(&key);
}
fn replay_profiled_server_hello_extension(
ext: &TlsExtension,
extensions: &mut Vec<u8>,
rng: &SecureRandom,
saw_supported_versions: &mut bool,
saw_key_share: &mut bool,
) {
match ext.ext_type {
EXT_SUPPORTED_VERSIONS if !*saw_supported_versions => {
push_supported_versions_extension(extensions);
*saw_supported_versions = true;
}
EXT_KEY_SHARE if !*saw_key_share => {
push_key_share_extension(extensions, rng);
*saw_key_share = true;
}
EXT_ALPN => {}
_ => {}
}
}
fn build_profiled_server_hello_extensions(cached: &CachedTlsData, rng: &SecureRandom) -> Vec<u8> {
let capacity = cached
.server_hello_template
.extensions
.iter()
.map(|ext| 4 + ext.data.len())
.sum::<usize>()
.max(44);
let mut extensions = Vec::with_capacity(capacity);
let mut saw_supported_versions = false;
let mut saw_key_share = false;
for ext in &cached.server_hello_template.extensions {
replay_profiled_server_hello_extension(
ext,
&mut extensions,
rng,
&mut saw_supported_versions,
&mut saw_key_share,
);
}
if !saw_key_share {
push_key_share_extension(&mut extensions, rng);
}
if !saw_supported_versions {
push_supported_versions_extension(&mut extensions);
}
extensions
}
/// Build a ServerHello + CCS + ApplicationData sequence using cached TLS metadata. /// Build a ServerHello + CCS + ApplicationData sequence using cached TLS metadata.
pub fn build_emulated_server_hello( pub fn build_emulated_server_hello(
secret: &[u8], secret: &[u8],
@@ -194,39 +267,28 @@ pub fn build_emulated_server_hello(
use_full_cert_payload: bool, use_full_cert_payload: bool,
serverhello_compact: bool, serverhello_compact: bool,
client_tls_version: ClientHelloTlsVersion, client_tls_version: ClientHelloTlsVersion,
selected_cipher_suite: [u8; 2],
rng: &SecureRandom, rng: &SecureRandom,
alpn: Option<Vec<u8>>, alpn: Option<Vec<u8>>,
new_session_tickets: u8, new_session_tickets: u8,
) -> Vec<u8> { ) -> Vec<u8> {
// --- ServerHello --- // --- ServerHello ---
let mut extensions = Vec::new(); let extensions = build_profiled_server_hello_extensions(cached, rng);
let key = gen_fake_x25519_key(rng);
extensions.extend_from_slice(&0x0033u16.to_be_bytes());
extensions.extend_from_slice(&(2 + 2 + 32u16).to_be_bytes());
extensions.extend_from_slice(&0x001du16.to_be_bytes());
extensions.extend_from_slice(&(32u16).to_be_bytes());
extensions.extend_from_slice(&key);
extensions.extend_from_slice(&0x002bu16.to_be_bytes());
extensions.extend_from_slice(&(2u16).to_be_bytes());
extensions.extend_from_slice(&0x0304u16.to_be_bytes());
let extensions_len = extensions.len() as u16; let extensions_len = extensions.len() as u16;
let body_len = 2 + // version let body_len = 2 + 32 + 1 + session_id.len() + 2 + 1 + 2 + extensions.len();
32 + // random
1 + session_id.len() + // session id
2 + // cipher
1 + // compression
2 + extensions.len(); // extensions
let mut message = Vec::with_capacity(4 + body_len); let mut message = Vec::with_capacity(4 + body_len);
message.push(0x02); // ServerHello message.push(0x02);
let len_bytes = (body_len as u32).to_be_bytes(); let len_bytes = (body_len as u32).to_be_bytes();
message.extend_from_slice(&len_bytes[1..4]); message.extend_from_slice(&len_bytes[1..4]);
message.extend_from_slice(&cached.server_hello_template.version); // 0x0303 message.extend_from_slice(&cached.server_hello_template.version);
message.extend_from_slice(&[0u8; 32]); // random placeholder message.extend_from_slice(&[0u8; 32]);
message.push(session_id.len() as u8); message.push(session_id.len() as u8);
message.extend_from_slice(session_id); message.extend_from_slice(session_id);
let cipher = if cached.server_hello_template.cipher_suite == [0, 0] { let cipher = if selected_cipher_suite != [0, 0] {
selected_cipher_suite
} else if cached.server_hello_template.cipher_suite == [0, 0] {
[0x13, 0x01] [0x13, 0x01]
} else { } else {
cached.server_hello_template.cipher_suite cached.server_hello_template.cipher_suite
@@ -303,21 +365,10 @@ pub fn build_emulated_server_hello(
} }
let mut app_data = Vec::new(); let mut app_data = Vec::new();
let alpn_marker = alpn // ALPN selection is encrypted inside EncryptedExtensions in real TLS 1.3.
.as_ref() // Keeping the FakeTLS record body opaque avoids a stable plaintext marker.
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize) let _ = alpn;
.map(|proto| { for size in sizes {
let proto_list_len = 1usize + proto.len();
let ext_data_len = 2usize + proto_list_len;
let mut marker = Vec::with_capacity(4 + ext_data_len);
marker.extend_from_slice(&0x0010u16.to_be_bytes());
marker.extend_from_slice(&(ext_data_len as u16).to_be_bytes());
marker.extend_from_slice(&(proto_list_len as u16).to_be_bytes());
marker.push(proto.len() as u8);
marker.extend_from_slice(proto);
marker
});
for (idx, size) in sizes.into_iter().enumerate() {
let mut rec = Vec::with_capacity(5 + size); let mut rec = Vec::with_capacity(5 + size);
rec.push(TLS_RECORD_APPLICATION); rec.push(TLS_RECORD_APPLICATION);
rec.extend_from_slice(&TLS_VERSION); rec.extend_from_slice(&TLS_VERSION);
@@ -334,31 +385,18 @@ pub fn build_emulated_server_hello(
if body_len > copy_len { if body_len > copy_len {
rec.extend_from_slice(&rng.bytes(body_len - copy_len)); rec.extend_from_slice(&rng.bytes(body_len - copy_len));
} }
rec.push(0x16); // inner content type marker (handshake) rec.push(0x16);
rec.extend_from_slice(&rng.bytes(16)); // AEAD-like tag rec.extend_from_slice(&rng.bytes(16));
} else { } else {
rec.extend_from_slice(&rng.bytes(size)); rec.extend_from_slice(&rng.bytes(size));
} }
} else if size > 17 { } else if size > 17 {
let body_len = size - 17; let body_len = size - 17;
let mut body = Vec::with_capacity(body_len); let mut body = Vec::with_capacity(body_len);
if idx == 0 body.extend_from_slice(&rng.bytes(body_len));
&& let Some(marker) = &alpn_marker
{
if marker.len() <= body_len {
body.extend_from_slice(marker);
if body_len > marker.len() {
body.extend_from_slice(&rng.bytes(body_len - marker.len()));
}
} else {
body.extend_from_slice(&rng.bytes(body_len));
}
} else {
body.extend_from_slice(&rng.bytes(body_len));
}
rec.extend_from_slice(&body); rec.extend_from_slice(&body);
rec.push(0x16); // inner content type marker (handshake) rec.push(0x16);
rec.extend_from_slice(&rng.bytes(16)); // AEAD-like tag rec.extend_from_slice(&rng.bytes(16));
} else { } else {
rec.extend_from_slice(&rng.bytes(size)); rec.extend_from_slice(&rng.bytes(size));
} }
@@ -408,7 +446,8 @@ mod tests {
use std::time::SystemTime; use std::time::SystemTime;
use crate::tls_front::types::{ use crate::tls_front::types::{
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsProfileSource, CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsExtension,
TlsProfileSource,
}; };
use super::{ use super::{
@@ -432,6 +471,38 @@ mod tests {
&response[app_start + 5..app_start + 5 + app_len] &response[app_start + 5..app_start + 5 + app_len]
} }
fn server_hello_cipher_suite(response: &[u8]) -> [u8; 2] {
let mut pos = 5 + 4 + 2 + 32;
let session_id_len = response[pos] as usize;
pos += 1 + session_id_len;
[response[pos], response[pos + 1]]
}
fn server_hello_extension_types(response: &[u8]) -> Vec<u16> {
let record_len = u16::from_be_bytes([response[3], response[4]]) as usize;
let handshake_end = 5 + record_len;
let mut pos = 5 + 4 + 2 + 32;
let session_id_len = response[pos] as usize;
pos += 1 + session_id_len + 2 + 1;
let extensions_len = u16::from_be_bytes([response[pos], response[pos + 1]]) as usize;
pos += 2;
let extensions_end = (pos + extensions_len).min(handshake_end);
let mut out = Vec::new();
while pos + 4 <= extensions_end {
let ext_type = u16::from_be_bytes([response[pos], response[pos + 1]]);
let ext_len = u16::from_be_bytes([response[pos + 2], response[pos + 3]]) as usize;
pos += 4;
if pos + ext_len > extensions_end {
break;
}
out.push(ext_type);
pos += ext_len;
}
out
}
fn make_cached(cert_payload: Option<TlsCertPayload>) -> CachedTlsData { fn make_cached(cert_payload: Option<TlsCertPayload>) -> CachedTlsData {
CachedTlsData { CachedTlsData {
server_hello_template: ParsedServerHello { server_hello_template: ParsedServerHello {
@@ -468,6 +539,7 @@ mod tests {
true, true,
true, true,
ClientHelloTlsVersion::Tls12, ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng, &rng,
None, None,
0, 0,
@@ -484,6 +556,65 @@ mod tests {
assert!(payload.starts_with(&cert_msg)); assert!(payload.starts_with(&cert_msg));
} }
#[test]
fn test_build_emulated_server_hello_uses_selected_cipher_suite() {
let cached = make_cached(None);
let rng = SecureRandom::new();
let response = build_emulated_server_hello(
b"secret",
&[0x10; 32],
&[0x20; 16],
&cached,
false,
true,
ClientHelloTlsVersion::Tls13,
[0x13, 0x03],
&rng,
None,
0,
);
assert_eq!(server_hello_cipher_suite(&response), [0x13, 0x03]);
}
#[test]
fn test_build_emulated_server_hello_replays_profiled_safe_extension_order() {
let mut cached = make_cached(None);
cached.server_hello_template.extensions = vec![
TlsExtension {
ext_type: 0x002b,
data: vec![0x03, 0x04],
},
TlsExtension {
ext_type: 0x0010,
data: vec![0x00, 0x03, 0x02, b'h', b'2'],
},
TlsExtension {
ext_type: 0x0033,
data: vec![0; 36],
},
];
let rng = SecureRandom::new();
let response = build_emulated_server_hello(
b"secret",
&[0x21; 32],
&[0x22; 16],
&cached,
false,
true,
ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng,
Some(b"h2".to_vec()),
0,
);
assert_eq!(
server_hello_extension_types(&response),
vec![0x002b, 0x0033]
);
}
#[test] #[test]
fn test_build_emulated_server_hello_random_fallback_when_no_cert_payload() { fn test_build_emulated_server_hello_random_fallback_when_no_cert_payload() {
let cached = make_cached(None); let cached = make_cached(None);
@@ -496,6 +627,7 @@ mod tests {
true, true,
true, true,
ClientHelloTlsVersion::Tls12, ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng, &rng,
None, None,
0, 0,
@@ -530,6 +662,7 @@ mod tests {
false, false,
true, true,
ClientHelloTlsVersion::Tls12, ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng, &rng,
None, None,
0, 0,
@@ -570,6 +703,7 @@ mod tests {
true, true,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
None, None,
0, 0,
@@ -583,7 +717,7 @@ mod tests {
} }
#[test] #[test]
fn test_build_emulated_server_hello_compact_disabled_skips_compact_payload() { fn test_build_emulated_server_hello_keeps_alpn_marker_out_of_random_payload() {
let mut cached = make_cached(None); let mut cached = make_cached(None);
cached.cert_info = Some(crate::tls_front::types::ParsedCertificateInfo { cached.cert_info = Some(crate::tls_front::types::ParsedCertificateInfo {
not_after_unix: Some(1_900_000_000), not_after_unix: Some(1_900_000_000),
@@ -602,6 +736,7 @@ mod tests {
false, false,
false, false,
ClientHelloTlsVersion::Tls12, ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng, &rng,
Some(b"h2".to_vec()), Some(b"h2".to_vec()),
0, 0,
@@ -610,8 +745,8 @@ mod tests {
let payload = first_app_data_payload(&response); let payload = first_app_data_payload(&response);
let expected_alpn_marker = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2']; let expected_alpn_marker = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
assert!( assert!(
payload.starts_with(&expected_alpn_marker), !payload.starts_with(&expected_alpn_marker),
"when compact mode is disabled and no full cert payload exists, the random/alpn path must be used" "random fallback payload must not expose plaintext ALPN marker bytes"
); );
} }
@@ -633,6 +768,7 @@ mod tests {
false, false,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
None, None,
0, 0,

View File

@@ -65,6 +65,7 @@ fn emulated_server_hello_keeps_single_change_cipher_spec_for_client_compatibilit
false, false,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
None, None,
0, 0,
@@ -89,6 +90,7 @@ fn emulated_server_hello_does_not_emit_profile_ticket_tail_when_disabled() {
false, false,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
None, None,
0, 0,
@@ -111,6 +113,7 @@ fn emulated_server_hello_uses_profile_ticket_lengths_when_enabled() {
false, false,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
None, None,
2, 2,

View File

@@ -58,6 +58,7 @@ fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
true, true,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
Some(oversized_alpn), Some(oversized_alpn),
0, 0,
@@ -84,7 +85,7 @@ fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
} }
#[test] #[test]
fn emulated_server_hello_embeds_full_alpn_marker_when_body_can_fit() { fn emulated_server_hello_keeps_alpn_marker_out_of_appdata() {
let cached = make_cached(None); let cached = make_cached(None);
let rng = SecureRandom::new(); let rng = SecureRandom::new();
@@ -96,6 +97,7 @@ fn emulated_server_hello_embeds_full_alpn_marker_when_body_can_fit() {
true, true,
true, true,
ClientHelloTlsVersion::Tls13, ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng, &rng,
Some(b"h2".to_vec()), Some(b"h2".to_vec()),
0, 0,
@@ -104,8 +106,8 @@ fn emulated_server_hello_embeds_full_alpn_marker_when_body_can_fit() {
let payload = first_app_data_payload(&response); let payload = first_app_data_payload(&response);
let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2']; let expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
assert!( assert!(
payload.starts_with(&expected), !payload.starts_with(&expected),
"when body has enough capacity, emulated first application record must include full ALPN marker" "emulated ApplicationData must not expose plaintext ALPN marker bytes"
); );
} }
@@ -126,6 +128,7 @@ fn emulated_server_hello_prefers_cert_payload_over_alpn_marker() {
true, true,
true, true,
ClientHelloTlsVersion::Tls12, ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng, &rng,
Some(b"h2".to_vec()), Some(b"h2".to_vec()),
0, 0,

View File

@@ -8,6 +8,7 @@ use std::time::{Duration, Instant};
use bytes::BytesMut; use bytes::BytesMut;
use rand::RngExt; use rand::RngExt;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
@@ -26,6 +27,7 @@ const ME_ACTIVE_PING_JITTER_SECS: i64 = 5;
const ME_IDLE_KEEPALIVE_MAX_SECS: u64 = 5; const ME_IDLE_KEEPALIVE_MAX_SECS: u64 = 5;
const ME_RPC_PROXY_REQ_RESPONSE_WAIT_MS: u64 = 700; const ME_RPC_PROXY_REQ_RESPONSE_WAIT_MS: u64 = 700;
const ME_PING_TRACKER_CLEANUP_EVERY: u32 = 32; const ME_PING_TRACKER_CLEANUP_EVERY: u32 = 32;
const ME_SERVICE_SIGNAL_SEND_TIMEOUT_MS: u64 = 50;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum WriterTeardownMode { enum WriterTeardownMode {
@@ -45,6 +47,11 @@ enum WriterLifecycleExit {
Cancelled, Cancelled,
} }
enum ServiceWriterCommandSendError {
Closed,
TimedOut,
}
async fn writer_command_loop( async fn writer_command_loop(
mut rx: mpsc::Receiver<WriterCommand>, mut rx: mpsc::Receiver<WriterCommand>,
mut rpc_writer: RpcWriter, mut rpc_writer: RpcWriter,
@@ -52,6 +59,8 @@ async fn writer_command_loop(
) -> Result<()> { ) -> Result<()> {
loop { loop {
tokio::select! { tokio::select! {
biased;
_ = cancel.cancelled() => return Ok(()),
cmd = rx.recv() => { cmd = rx.recv() => {
match cmd { match cmd {
Some(WriterCommand::Data(payload)) => { Some(WriterCommand::Data(payload)) => {
@@ -69,7 +78,27 @@ async fn writer_command_loop(
Some(WriterCommand::Close) | None => return Ok(()), Some(WriterCommand::Close) | None => return Ok(()),
} }
} }
_ = cancel.cancelled() => return Ok(()), }
}
}
async fn send_service_writer_command(
tx: &mpsc::Sender<WriterCommand>,
cmd: WriterCommand,
) -> std::result::Result<(), ServiceWriterCommandSendError> {
match tx.try_send(cmd) {
Ok(()) => Ok(()),
Err(TrySendError::Closed(_)) => Err(ServiceWriterCommandSendError::Closed),
Err(TrySendError::Full(cmd)) => {
let wait = Duration::from_millis(ME_SERVICE_SIGNAL_SEND_TIMEOUT_MS);
match tokio::time::timeout(wait, tx.reserve()).await {
Ok(Ok(permit)) => {
permit.send(cmd);
Ok(())
}
Ok(Err(_)) => Err(ServiceWriterCommandSendError::Closed),
Err(_) => Err(ServiceWriterCommandSendError::TimedOut),
}
} }
} }
} }
@@ -108,6 +137,7 @@ async fn ping_loop(
Duration::from_secs(wait) Duration::from_secs(wait)
}; };
tokio::select! { tokio::select! {
biased;
_ = cancel_ping_token.cancelled() => return, _ = cancel_ping_token.cancelled() => return,
_ = tokio::time::sleep(startup_jitter) => {} _ = tokio::time::sleep(startup_jitter) => {}
} }
@@ -131,6 +161,7 @@ async fn ping_loop(
Duration::from_secs(secs) Duration::from_secs(secs)
}; };
tokio::select! { tokio::select! {
biased;
_ = cancel_ping_token.cancelled() => return, _ = cancel_ping_token.cancelled() => return,
_ = tokio::time::sleep(wait) => {} _ = tokio::time::sleep(wait) => {}
} }
@@ -151,14 +182,24 @@ async fn ping_loop(
} }
ping_id = ping_id.wrapping_add(1); ping_id = ping_id.wrapping_add(1);
stats_ping.increment_me_keepalive_sent(); stats_ping.increment_me_keepalive_sent();
if tx_ping if let Err(error) =
.send(WriterCommand::ControlAndFlush(payload)) send_service_writer_command(&tx_ping, WriterCommand::ControlAndFlush(payload)).await
.await
.is_err()
{ {
{
let mut tracker = ping_tracker_ping.lock().await;
tracker.remove(&sent_id);
}
stats_ping.increment_me_keepalive_failed(); stats_ping.increment_me_keepalive_failed();
debug!("ME ping failed, removing dead writer"); match error {
return; ServiceWriterCommandSendError::Closed => {
debug!("ME ping failed, removing dead writer");
return;
}
ServiceWriterCommandSendError::TimedOut => {
debug!("ME ping skipped: writer command channel is full");
continue;
}
}
} }
} }
} }
@@ -191,6 +232,7 @@ async fn rpc_proxy_req_signal_loop(
}; };
tokio::select! { tokio::select! {
biased;
_ = cancel_signal.cancelled() => return, _ = cancel_signal.cancelled() => return,
_ = tokio::time::sleep(Duration::from_millis(startup_jitter_ms)) => {} _ = tokio::time::sleep(Duration::from_millis(startup_jitter_ms)) => {}
} }
@@ -207,6 +249,7 @@ async fn rpc_proxy_req_signal_loop(
}; };
tokio::select! { tokio::select! {
biased;
_ = cancel_signal.cancelled() => return, _ = cancel_signal.cancelled() => return,
_ = tokio::time::sleep(wait) => {} _ = tokio::time::sleep(wait) => {}
} }
@@ -233,14 +276,15 @@ async fn rpc_proxy_req_signal_loop(
meta.proto_flags, meta.proto_flags,
); );
if tx_signal if let Err(error) =
.send(WriterCommand::DataAndFlush(payload)) send_service_writer_command(&tx_signal, WriterCommand::DataAndFlush(payload)).await
.await
.is_err()
{ {
stats_signal.increment_me_rpc_proxy_req_signal_failed_total(); stats_signal.increment_me_rpc_proxy_req_signal_failed_total();
let _ = pool.registry.unregister(conn_id).await; let _ = pool.registry.unregister(conn_id).await;
return; match error {
ServiceWriterCommandSendError::Closed => return,
ServiceWriterCommandSendError::TimedOut => continue,
}
} }
stats_signal.increment_me_rpc_proxy_req_signal_sent_total(); stats_signal.increment_me_rpc_proxy_req_signal_sent_total();
@@ -258,14 +302,16 @@ async fn rpc_proxy_req_signal_loop(
let close_payload = build_control_payload(RPC_CLOSE_EXT_U32, conn_id); let close_payload = build_control_payload(RPC_CLOSE_EXT_U32, conn_id);
if tx_signal if let Err(error) =
.send(WriterCommand::ControlAndFlush(close_payload)) send_service_writer_command(&tx_signal, WriterCommand::ControlAndFlush(close_payload))
.await .await
.is_err()
{ {
stats_signal.increment_me_rpc_proxy_req_signal_failed_total(); stats_signal.increment_me_rpc_proxy_req_signal_failed_total();
let _ = pool.registry.unregister(conn_id).await; let _ = pool.registry.unregister(conn_id).await;
return; match error {
ServiceWriterCommandSendError::Closed => return,
ServiceWriterCommandSendError::TimedOut => continue,
}
} }
stats_signal.increment_me_rpc_proxy_req_signal_close_sent_total(); stats_signal.increment_me_rpc_proxy_req_signal_close_sent_total();

View File

@@ -242,6 +242,7 @@ pub(crate) async fn reader_loop(
let mut raw = enc_leftover; let mut raw = enc_leftover;
let mut expected_seq: i32 = 0; let mut expected_seq: i32 = 0;
let mut data_route_queue_full_streak = HashMap::<u64, u8>::new(); let mut data_route_queue_full_streak = HashMap::<u64, u8>::new();
let mut tmp = [0u8; 65_536];
let mut fairness = WorkerFairnessState::new( let mut fairness = WorkerFairnessState::new(
WorkerFairnessConfig { WorkerFairnessConfig {
worker_id: (writer_id as u16).saturating_add(1), worker_id: (writer_id as u16).saturating_add(1),
@@ -263,18 +264,18 @@ pub(crate) async fn reader_loop(
let fairshare_enabled = route_fairshare_enabled.load(Ordering::Relaxed); let fairshare_enabled = route_fairshare_enabled.load(Ordering::Relaxed);
fairness.set_backpressure_enabled(backpressure_enabled); fairness.set_backpressure_enabled(backpressure_enabled);
let fairness_has_backlog = should_schedule_fairness_retry(&fairness_snapshot); let fairness_has_backlog = should_schedule_fairness_retry(&fairness_snapshot);
let mut tmp = [0u8; 65_536];
let backlog_retry_enabled = fairness_has_backlog; let backlog_retry_enabled = fairness_has_backlog;
let backlog_retry_delay = let backlog_retry_delay =
fairness_retry_delay(reader_route_data_wait_ms.load(Ordering::Relaxed)); fairness_retry_delay(reader_route_data_wait_ms.load(Ordering::Relaxed));
let mut retry_only = false; let mut retry_only = false;
let n = tokio::select! { let n = tokio::select! {
biased;
_ = cancel.cancelled() => return Ok(()),
res = rd.read(&mut tmp) => res.map_err(ProxyError::Io)?, res = rd.read(&mut tmp) => res.map_err(ProxyError::Io)?,
_ = tokio::time::sleep(backlog_retry_delay), if backlog_retry_enabled => { _ = tokio::time::sleep(backlog_retry_delay), if backlog_retry_enabled => {
retry_only = true; retry_only = true;
0usize 0usize
}, },
_ = cancel.cancelled() => return Ok(()),
}; };
if retry_only { if retry_only {
let route_wait_ms = reader_route_data_wait_ms.load(Ordering::Relaxed); let route_wait_ms = reader_route_data_wait_ms.load(Ordering::Relaxed);

View File

@@ -77,26 +77,24 @@ struct HotBindingTable {
struct BindingState { struct BindingState {
inner: Mutex<BindingInner>, inner: Mutex<BindingInner>,
writer_idle_since_epoch_secs: DashMap<u64, u64>,
bound_clients_by_writer: DashMap<u64, usize>,
active_sessions_by_target_dc: DashMap<i16, usize>,
last_meta_for_writer: DashMap<u64, ConnMeta>,
} }
struct BindingInner { struct BindingInner {
writers: HashMap<u64, mpsc::Sender<WriterCommand>>,
writer_for_conn: HashMap<u64, u64>, writer_for_conn: HashMap<u64, u64>,
conns_for_writer: HashMap<u64, HashSet<u64>>, conns_for_writer: HashMap<u64, HashSet<u64>>,
meta: HashMap<u64, ConnMeta>, meta: HashMap<u64, ConnMeta>,
last_meta_for_writer: HashMap<u64, ConnMeta>,
writer_idle_since_epoch_secs: HashMap<u64, u64>,
} }
impl BindingInner { impl BindingInner {
fn new() -> Self { fn new() -> Self {
Self { Self {
writers: HashMap::new(),
writer_for_conn: HashMap::new(), writer_for_conn: HashMap::new(),
conns_for_writer: HashMap::new(), conns_for_writer: HashMap::new(),
meta: HashMap::new(), meta: HashMap::new(),
last_meta_for_writer: HashMap::new(),
writer_idle_since_epoch_secs: HashMap::new(),
} }
} }
} }
@@ -149,6 +147,10 @@ impl ConnRegistry {
}, },
binding: BindingState { binding: BindingState {
inner: Mutex::new(BindingInner::new()), inner: Mutex::new(BindingInner::new()),
writer_idle_since_epoch_secs: DashMap::new(),
bound_clients_by_writer: DashMap::new(),
active_sessions_by_target_dc: DashMap::new(),
last_meta_for_writer: DashMap::new(),
}, },
next_id: AtomicU64::new(start), next_id: AtomicU64::new(start),
route_channel_capacity, route_channel_capacity,

View File

@@ -13,13 +13,63 @@ use super::{
}; };
impl ConnRegistry { impl ConnRegistry {
fn set_writer_bound_count(&self, writer_id: u64, count: usize) {
self.binding
.bound_clients_by_writer
.insert(writer_id, count);
if count == 0 {
self.binding
.writer_idle_since_epoch_secs
.entry(writer_id)
.or_insert_with(Self::now_epoch_secs);
} else {
self.binding.writer_idle_since_epoch_secs.remove(&writer_id);
}
}
fn adjust_active_target_dc(&self, target_dc: i16, delta: isize) {
if target_dc == 0 || delta == 0 {
return;
}
if delta > 0 {
self.binding
.active_sessions_by_target_dc
.entry(target_dc)
.and_modify(|count| *count = count.saturating_add(delta as usize))
.or_insert(delta as usize);
return;
}
let remove = if let Some(mut count) = self
.binding
.active_sessions_by_target_dc
.get_mut(&target_dc)
{
let decrement = delta.unsigned_abs();
*count = count.saturating_sub(decrement);
*count == 0
} else {
false
};
if remove {
self.binding.active_sessions_by_target_dc.remove(&target_dc);
}
}
pub async fn register_writer(&self, writer_id: u64, tx: mpsc::Sender<WriterCommand>) { pub async fn register_writer(&self, writer_id: u64, tx: mpsc::Sender<WriterCommand>) {
let mut binding = self.binding.inner.lock().await; let mut binding = self.binding.inner.lock().await;
binding.writers.insert(writer_id, tx.clone());
binding binding
.conns_for_writer .conns_for_writer
.entry(writer_id) .entry(writer_id)
.or_insert_with(HashSet::new); .or_insert_with(HashSet::new);
self.binding
.bound_clients_by_writer
.entry(writer_id)
.or_insert(0);
self.binding
.writer_idle_since_epoch_secs
.entry(writer_id)
.or_insert_with(Self::now_epoch_secs);
self.writers.map.insert(writer_id, tx); self.writers.map.insert(writer_id, tx);
} }
@@ -29,19 +79,18 @@ impl ConnRegistry {
self.routing.byte_budget.remove(&id); self.routing.byte_budget.remove(&id);
self.hot_binding.map.remove(&id); self.hot_binding.map.remove(&id);
let mut binding = self.binding.inner.lock().await; let mut binding = self.binding.inner.lock().await;
binding.meta.remove(&id); let previous_meta = binding.meta.remove(&id);
if let Some(meta) = previous_meta.as_ref() {
self.adjust_active_target_dc(meta.target_dc, -1);
}
if let Some(writer_id) = binding.writer_for_conn.remove(&id) { if let Some(writer_id) = binding.writer_for_conn.remove(&id) {
let became_empty = if let Some(set) = binding.conns_for_writer.get_mut(&writer_id) { let next_count = if let Some(set) = binding.conns_for_writer.get_mut(&writer_id) {
set.remove(&id); set.remove(&id);
set.is_empty() set.len()
} else { } else {
false 0
}; };
if became_empty { self.set_writer_bound_count(writer_id, next_count);
binding
.writer_idle_since_epoch_secs
.insert(writer_id, Self::now_epoch_secs());
}
return Some(writer_id); return Some(writer_id);
} }
None None
@@ -248,7 +297,7 @@ impl ConnRegistry {
if !self.routing.map.contains_key(&conn_id) { if !self.routing.map.contains_key(&conn_id) {
return false; return false;
} }
if !binding.writers.contains_key(&writer_id) { if !self.writers.map.contains_key(&writer_id) {
return false; return false;
} }
@@ -256,28 +305,32 @@ impl ConnRegistry {
if let Some(previous_writer_id) = previous_writer_id if let Some(previous_writer_id) = previous_writer_id
&& previous_writer_id != writer_id && previous_writer_id != writer_id
{ {
let became_empty = let next_count =
if let Some(set) = binding.conns_for_writer.get_mut(&previous_writer_id) { if let Some(set) = binding.conns_for_writer.get_mut(&previous_writer_id) {
set.remove(&conn_id); set.remove(&conn_id);
set.is_empty() set.len()
} else { } else {
false 0
}; };
if became_empty { self.set_writer_bound_count(previous_writer_id, next_count);
binding
.writer_idle_since_epoch_secs
.insert(previous_writer_id, Self::now_epoch_secs());
}
} }
binding.meta.insert(conn_id, meta.clone()); if let Some(previous_meta) = binding.meta.insert(conn_id, meta.clone()) {
binding.last_meta_for_writer.insert(writer_id, meta.clone()); self.adjust_active_target_dc(previous_meta.target_dc, -1);
binding.writer_idle_since_epoch_secs.remove(&writer_id); }
binding self.adjust_active_target_dc(meta.target_dc, 1);
.conns_for_writer self.binding
.entry(writer_id) .last_meta_for_writer
.or_insert_with(HashSet::new) .insert(writer_id, meta.clone());
.insert(conn_id); let next_count = {
let set = binding
.conns_for_writer
.entry(writer_id)
.or_insert_with(HashSet::new);
set.insert(conn_id);
set.len()
};
self.set_writer_bound_count(writer_id, next_count);
self.hot_binding self.hot_binding
.map .map
.insert(conn_id, HotConnBinding { writer_id, meta }); .insert(conn_id, HotConnBinding { writer_id, meta });
@@ -290,27 +343,38 @@ impl ConnRegistry {
.conns_for_writer .conns_for_writer
.entry(writer_id) .entry(writer_id)
.or_insert_with(HashSet::new); .or_insert_with(HashSet::new);
binding let count = binding
.writer_idle_since_epoch_secs .conns_for_writer
.entry(writer_id) .get(&writer_id)
.or_insert(Self::now_epoch_secs()); .map(|set| set.len())
.unwrap_or(0);
self.set_writer_bound_count(writer_id, count);
} }
pub async fn get_last_writer_meta(&self, writer_id: u64) -> Option<ConnMeta> { pub async fn get_last_writer_meta(&self, writer_id: u64) -> Option<ConnMeta> {
let binding = self.binding.inner.lock().await; self.binding
binding.last_meta_for_writer.get(&writer_id).cloned() .last_meta_for_writer
.get(&writer_id)
.map(|entry| entry.value().clone())
} }
pub async fn writer_idle_since_snapshot(&self) -> HashMap<u64, u64> { pub async fn writer_idle_since_snapshot(&self) -> HashMap<u64, u64> {
let binding = self.binding.inner.lock().await; self.binding
binding.writer_idle_since_epoch_secs.clone() .writer_idle_since_epoch_secs
.iter()
.map(|entry| (*entry.key(), *entry.value()))
.collect()
} }
pub async fn writer_idle_since_for_writer_ids(&self, writer_ids: &[u64]) -> HashMap<u64, u64> { pub async fn writer_idle_since_for_writer_ids(&self, writer_ids: &[u64]) -> HashMap<u64, u64> {
let binding = self.binding.inner.lock().await;
let mut out = HashMap::<u64, u64>::with_capacity(writer_ids.len()); let mut out = HashMap::<u64, u64>::with_capacity(writer_ids.len());
for writer_id in writer_ids { for writer_id in writer_ids {
if let Some(idle_since) = binding.writer_idle_since_epoch_secs.get(writer_id).copied() { if let Some(idle_since) = self
.binding
.writer_idle_since_epoch_secs
.get(writer_id)
.map(|entry| *entry.value())
{
out.insert(*writer_id, idle_since); out.insert(*writer_id, idle_since);
} }
} }
@@ -320,25 +384,19 @@ impl ConnRegistry {
pub(in crate::transport::middle_proxy) async fn writer_activity_snapshot( pub(in crate::transport::middle_proxy) async fn writer_activity_snapshot(
&self, &self,
) -> WriterActivitySnapshot { ) -> WriterActivitySnapshot {
let binding = self.binding.inner.lock().await;
let mut bound_clients_by_writer = HashMap::<u64, usize>::new();
let mut active_sessions_by_target_dc = HashMap::<i16, usize>::new();
for (writer_id, conn_ids) in &binding.conns_for_writer {
bound_clients_by_writer.insert(*writer_id, conn_ids.len());
}
for conn_meta in binding.meta.values() {
if conn_meta.target_dc == 0 {
continue;
}
*active_sessions_by_target_dc
.entry(conn_meta.target_dc)
.or_insert(0) += 1;
}
WriterActivitySnapshot { WriterActivitySnapshot {
bound_clients_by_writer, bound_clients_by_writer: self
active_sessions_by_target_dc, .binding
.bound_clients_by_writer
.iter()
.map(|entry| (*entry.key(), *entry.value()))
.collect(),
active_sessions_by_target_dc: self
.binding
.active_sessions_by_target_dc
.iter()
.map(|entry| (*entry.key(), *entry.value()))
.collect(),
} }
} }
@@ -393,10 +451,10 @@ impl ConnRegistry {
pub async fn writer_lost(&self, writer_id: u64) -> Vec<BoundConn> { pub async fn writer_lost(&self, writer_id: u64) -> Vec<BoundConn> {
let mut binding = self.binding.inner.lock().await; let mut binding = self.binding.inner.lock().await;
binding.writers.remove(&writer_id);
self.writers.map.remove(&writer_id); self.writers.map.remove(&writer_id);
binding.last_meta_for_writer.remove(&writer_id); self.binding.last_meta_for_writer.remove(&writer_id);
binding.writer_idle_since_epoch_secs.remove(&writer_id); self.binding.writer_idle_since_epoch_secs.remove(&writer_id);
self.binding.bound_clients_by_writer.remove(&writer_id);
let conns = binding let conns = binding
.conns_for_writer .conns_for_writer
.remove(&writer_id) .remove(&writer_id)
@@ -410,6 +468,10 @@ impl ConnRegistry {
continue; continue;
} }
binding.writer_for_conn.remove(&conn_id); binding.writer_for_conn.remove(&conn_id);
let meta = binding.meta.remove(&conn_id);
if let Some(meta) = meta.as_ref() {
self.adjust_active_target_dc(meta.target_dc, -1);
}
let remove_hot = self let remove_hot = self
.hot_binding .hot_binding
.map .map
@@ -419,11 +481,8 @@ impl ConnRegistry {
if remove_hot { if remove_hot {
self.hot_binding.map.remove(&conn_id); self.hot_binding.map.remove(&conn_id);
} }
if let Some(m) = binding.meta.get(&conn_id) { if let Some(m) = meta {
out.push(BoundConn { out.push(BoundConn { conn_id, meta: m });
conn_id,
meta: m.clone(),
});
} }
} }
out out
@@ -438,11 +497,10 @@ impl ConnRegistry {
} }
pub async fn is_writer_empty(&self, writer_id: u64) -> bool { pub async fn is_writer_empty(&self, writer_id: u64) -> bool {
let binding = self.binding.inner.lock().await; self.binding
binding .bound_clients_by_writer
.conns_for_writer
.get(&writer_id) .get(&writer_id)
.map(|s| s.is_empty()) .map(|count| *count.value() == 0)
.unwrap_or(true) .unwrap_or(true)
} }
@@ -457,21 +515,20 @@ impl ConnRegistry {
return false; return false;
} }
binding.writers.remove(&writer_id);
self.writers.map.remove(&writer_id); self.writers.map.remove(&writer_id);
binding.last_meta_for_writer.remove(&writer_id); self.binding.last_meta_for_writer.remove(&writer_id);
binding.writer_idle_since_epoch_secs.remove(&writer_id); self.binding.writer_idle_since_epoch_secs.remove(&writer_id);
self.binding.bound_clients_by_writer.remove(&writer_id);
binding.conns_for_writer.remove(&writer_id); binding.conns_for_writer.remove(&writer_id);
true true
} }
#[allow(dead_code)] #[allow(dead_code)]
pub(super) async fn non_empty_writer_ids(&self, writer_ids: &[u64]) -> HashSet<u64> { pub(super) async fn non_empty_writer_ids(&self, writer_ids: &[u64]) -> HashSet<u64> {
let binding = self.binding.inner.lock().await;
let mut out = HashSet::<u64>::with_capacity(writer_ids.len()); let mut out = HashSet::<u64>::with_capacity(writer_ids.len());
for writer_id in writer_ids { for writer_id in writer_ids {
if let Some(conns) = binding.conns_for_writer.get(writer_id) if let Some(count) = self.binding.bound_clients_by_writer.get(writer_id)
&& !conns.is_empty() && *count.value() > 0
{ {
out.insert(*writer_id); out.insert(*writer_id);
} }

View File

@@ -6,6 +6,7 @@ use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError; use tokio::sync::mpsc::error::TrySendError;
use tracing::{debug, warn}; use tracing::{debug, warn};
@@ -15,7 +16,6 @@ use super::registry::ConnMeta;
use super::wire::build_proxy_req_payload; use super::wire::build_proxy_req_payload;
use crate::config::{MeRouteNoWriterMode, MeWriterPickMode}; use crate::config::{MeRouteNoWriterMode, MeWriterPickMode};
use crate::error::{ProxyError, Result}; use crate::error::{ProxyError, Result};
use crate::network::IpFamily;
use crate::stream::PooledBuffer; use crate::stream::PooledBuffer;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
@@ -34,6 +34,11 @@ mod close;
mod recovery; mod recovery;
mod selection; mod selection;
enum WriterCommandReserveError {
Closed,
TimedOut,
}
fn proxy_tag_array(tag: Option<&[u8]>) -> Option<[u8; 16]> { fn proxy_tag_array(tag: Option<&[u8]>) -> Option<[u8; 16]> {
tag.and_then(|tag| <[u8; 16]>::try_from(tag).ok()) tag.and_then(|tag| <[u8; 16]>::try_from(tag).ok())
} }
@@ -45,6 +50,21 @@ fn proxy_req_payload_from_command(cmd: WriterCommand) -> Option<PooledBuffer> {
} }
} }
async fn reserve_writer_command_slot(
tx: &mpsc::Sender<WriterCommand>,
wait: Option<Duration>,
) -> std::result::Result<mpsc::OwnedPermit<WriterCommand>, WriterCommandReserveError> {
let reserve = tx.clone().reserve_owned();
match wait {
Some(wait) => match tokio::time::timeout(wait, reserve).await {
Ok(Ok(permit)) => Ok(permit),
Ok(Err(_)) => Err(WriterCommandReserveError::Closed),
Err(_) => Err(WriterCommandReserveError::TimedOut),
},
None => reserve.await.map_err(|_| WriterCommandReserveError::Closed),
}
}
impl MePool { impl MePool {
/// Send RPC_PROXY_REQ. `tag_override`: per-user ad_tag (from access.user_ad_tags); if None, uses pool default. /// Send RPC_PROXY_REQ. `tag_override`: per-user ad_tag (from access.user_ad_tags); if None, uses pool default.
pub async fn send_proxy_req( pub async fn send_proxy_req(
@@ -105,9 +125,25 @@ impl MePool {
return Ok(()); return Ok(());
} }
Err(TrySendError::Full(cmd)) => { Err(TrySendError::Full(cmd)) => {
if current.tx.send(cmd).await.is_ok() { match reserve_writer_command_slot(
self.note_hybrid_route_success(); &current.tx,
return Ok(()); self.route_runtime.me_route_blocking_send_timeout,
)
.await
{
Ok(permit) => {
permit.send(cmd);
self.note_hybrid_route_success();
return Ok(());
}
Err(WriterCommandReserveError::TimedOut) => {
self.stats
.increment_me_writer_pick_full_total(self.writer_pick_mode());
return Err(ProxyError::Proxy(
"ME writer channel full within blocking send timeout".into(),
));
}
Err(WriterCommandReserveError::Closed) => {}
} }
warn!(writer_id = current.writer_id, "ME writer channel closed"); warn!(writer_id = current.writer_id, "ME writer channel closed");
self.remove_writer_and_close_clients(current.writer_id) self.remove_writer_and_close_clients(current.writer_id)
@@ -124,9 +160,8 @@ impl MePool {
} }
let mut writers_snapshot = { let mut writers_snapshot = {
let ws = self.writers.read().await; let ws = self.writers.snapshot();
if ws.is_empty() { if ws.is_empty() {
drop(ws);
match no_writer_mode { match no_writer_mode {
MeRouteNoWriterMode::AsyncRecoveryFailfast => { MeRouteNoWriterMode::AsyncRecoveryFailfast => {
let deadline = *no_writer_deadline.get_or_insert_with(|| { let deadline = *no_writer_deadline.get_or_insert_with(|| {
@@ -154,38 +189,28 @@ impl MePool {
for _ in for _ in
0..self.route_runtime.me_route_inline_recovery_attempts.max(1) 0..self.route_runtime.me_route_inline_recovery_attempts.max(1)
{ {
for family in self.family_order() { let preferred = self.preferred_endpoints_by_dc.load_full();
let map = match family { for (dc, addrs) in preferred.iter() {
IpFamily::V4 => self.proxy_map_v4.read().await.clone(), for addr in addrs {
IpFamily::V6 => self.proxy_map_v6.read().await.clone(), let _ = self
}; .connect_one_for_dc(*addr, *dc, self.rng.as_ref())
for (dc, addrs) in &map { .await;
for (ip, port) in addrs {
let addr = SocketAddr::new(*ip, *port);
let _ = self
.connect_one_for_dc(
addr,
*dc,
self.rng.as_ref(),
)
.await;
}
} }
} }
if !self.writers.read().await.is_empty() { if !self.writers.snapshot().is_empty() {
break; break;
} }
} }
} }
if !self.writers.read().await.is_empty() { if !self.writers.snapshot().is_empty() {
continue; continue;
} }
let deadline = *no_writer_deadline.get_or_insert_with(|| { let deadline = *no_writer_deadline.get_or_insert_with(|| {
Instant::now() + self.route_runtime.me_route_inline_recovery_wait Instant::now() + self.route_runtime.me_route_inline_recovery_wait
}); });
if !self.wait_for_writer_until(deadline).await { if !self.wait_for_writer_until(deadline).await {
if !self.writers.read().await.is_empty() { if !self.writers.snapshot().is_empty() {
continue; continue;
} }
self.stats.increment_me_no_writer_failfast_total(); self.stats.increment_me_no_writer_failfast_total();
@@ -222,7 +247,7 @@ impl MePool {
} }
} }
} }
ws.clone() ws
}; };
let mut candidate_indices = self let mut candidate_indices = self
@@ -285,7 +310,12 @@ impl MePool {
)); ));
} }
emergency_attempts += 1; emergency_attempts += 1;
let mut endpoints = self.endpoint_candidates_for_target_dc(routed_dc).await; let mut endpoints = self
.preferred_endpoints_by_dc
.load()
.get(&routed_dc)
.cloned()
.unwrap_or_default();
endpoints.shuffle(&mut rand::rng()); endpoints.shuffle(&mut rand::rng());
for addr in endpoints { for addr in endpoints {
if self if self
@@ -298,9 +328,7 @@ impl MePool {
} }
tokio::time::sleep(Duration::from_millis(100 * emergency_attempts as u64)) tokio::time::sleep(Duration::from_millis(100 * emergency_attempts as u64))
.await; .await;
let ws2 = self.writers.read().await; writers_snapshot = self.writers.snapshot();
writers_snapshot = ws2.clone();
drop(ws2);
candidate_indices = self candidate_indices = self
.candidate_indices_for_dc(&writers_snapshot, routed_dc, false) .candidate_indices_for_dc(&writers_snapshot, routed_dc, false)
.await; .await;
@@ -563,33 +591,48 @@ impl MePool {
self.note_hybrid_route_success(); self.note_hybrid_route_success();
return Ok(()); return Ok(());
} }
Err(TrySendError::Full(cmd)) => match current.tx.send(cmd).await { Err(TrySendError::Full(cmd)) => {
Ok(()) => { match reserve_writer_command_slot(
self.note_hybrid_route_success(); &current.tx,
return Ok(()); self.route_runtime.me_route_blocking_send_timeout,
} )
Err(send_err) => { .await
let Some(payload) = proxy_req_payload_from_command(send_err.0) else { {
Ok(permit) => {
permit.send(cmd);
self.note_hybrid_route_success();
return Ok(());
}
Err(WriterCommandReserveError::TimedOut) => {
self.stats
.increment_me_writer_pick_full_total(self.writer_pick_mode());
return Err(ProxyError::Proxy( return Err(ProxyError::Proxy(
"ME writer rejected unexpected command type".into(), "ME writer channel full within blocking send timeout".into(),
)); ));
}; }
warn!(writer_id = current.writer_id, "ME writer channel closed"); Err(WriterCommandReserveError::Closed) => {
self.remove_writer_and_close_clients(current.writer_id) let Some(payload) = proxy_req_payload_from_command(cmd) else {
.await; return Err(ProxyError::Proxy(
return self "ME writer rejected unexpected command type".into(),
.send_proxy_req( ));
conn_id, };
target_dc, warn!(writer_id = current.writer_id, "ME writer channel closed");
client_addr, self.remove_writer_and_close_clients(current.writer_id)
our_addr, .await;
payload.as_ref(), return self
proto_flags, .send_proxy_req(
tag.as_ref().map(|tag| tag.as_slice()), conn_id,
) target_dc,
.await; client_addr,
our_addr,
payload.as_ref(),
proto_flags,
tag.as_ref().map(|tag| tag.as_slice()),
)
.await;
}
} }
}, }
Err(TrySendError::Closed(cmd)) => { Err(TrySendError::Closed(cmd)) => {
let Some(payload) = proxy_req_payload_from_command(cmd) else { let Some(payload) = proxy_req_payload_from_command(cmd) else {
return Err(ProxyError::Proxy( return Err(ProxyError::Proxy(

View File

@@ -10,18 +10,43 @@ use crate::protocol::constants::{RPC_CLOSE_CONN_U32, RPC_CLOSE_EXT_U32};
use super::super::MePool; use super::super::MePool;
use super::super::codec::{WriterCommand, build_control_payload}; use super::super::codec::{WriterCommand, build_control_payload};
use super::{WriterCommandReserveError, reserve_writer_command_slot};
const ME_CLOSE_SIGNAL_SEND_TIMEOUT: Duration = Duration::from_millis(50);
impl MePool { impl MePool {
/// Sends an extended close signal for a client-bound ME connection.
pub async fn send_close(self: &Arc<Self>, conn_id: u64) -> Result<()> { pub async fn send_close(self: &Arc<Self>, conn_id: u64) -> Result<()> {
if let Some(w) = self.registry.get_writer(conn_id).await { if let Some(w) = self.registry.get_writer(conn_id).await {
let payload = build_control_payload(RPC_CLOSE_EXT_U32, conn_id); let payload = build_control_payload(RPC_CLOSE_EXT_U32, conn_id);
if w.tx match w.tx.try_send(WriterCommand::ControlAndFlush(payload)) {
.send(WriterCommand::ControlAndFlush(payload)) Ok(()) => {}
.await Err(TrySendError::Full(cmd)) => {
.is_err() match reserve_writer_command_slot(&w.tx, Some(ME_CLOSE_SIGNAL_SEND_TIMEOUT))
{ .await
debug!("ME close write failed"); {
self.remove_writer_and_close_clients(w.writer_id).await; Ok(permit) => {
permit.send(cmd);
}
Err(WriterCommandReserveError::TimedOut) => {
debug!(conn_id, "ME close skipped: writer command channel is full");
}
Err(WriterCommandReserveError::Closed) => {
debug!(
conn_id,
"ME close skipped: writer command channel is closed"
);
self.remove_writer_and_close_clients(w.writer_id).await;
}
}
}
Err(TrySendError::Closed(_)) => {
debug!(
conn_id,
"ME close skipped: writer command channel is closed"
);
self.remove_writer_and_close_clients(w.writer_id).await;
}
} }
} else { } else {
debug!(conn_id, "ME close skipped (writer missing)"); debug!(conn_id, "ME close skipped (writer missing)");
@@ -31,13 +56,16 @@ impl MePool {
Ok(()) Ok(())
} }
/// Sends the compact close signal used by ME-side forced connection teardown.
pub async fn send_close_conn(self: &Arc<Self>, conn_id: u64) -> Result<()> { pub async fn send_close_conn(self: &Arc<Self>, conn_id: u64) -> Result<()> {
if let Some(w) = self.registry.get_writer(conn_id).await { if let Some(w) = self.registry.get_writer(conn_id).await {
let payload = build_control_payload(RPC_CLOSE_CONN_U32, conn_id); let payload = build_control_payload(RPC_CLOSE_CONN_U32, conn_id);
match w.tx.try_send(WriterCommand::ControlAndFlush(payload)) { match w.tx.try_send(WriterCommand::ControlAndFlush(payload)) {
Ok(()) => {} Ok(()) => {}
Err(TrySendError::Full(cmd)) => { Err(TrySendError::Full(cmd)) => {
let _ = tokio::time::timeout(Duration::from_millis(50), w.tx.send(cmd)).await; let _ = reserve_writer_command_slot(&w.tx, Some(ME_CLOSE_SIGNAL_SEND_TIMEOUT))
.await
.map(|permit| permit.send(cmd));
} }
Err(TrySendError::Closed(_)) => { Err(TrySendError::Closed(_)) => {
debug!(conn_id, "ME close_conn skipped: writer channel closed"); debug!(conn_id, "ME close_conn skipped: writer channel closed");
@@ -51,6 +79,7 @@ impl MePool {
Ok(()) Ok(())
} }
/// Sends close signals for all currently registered ME-bound connections during shutdown.
pub async fn shutdown_send_close_conn_all(self: &Arc<Self>) -> usize { pub async fn shutdown_send_close_conn_all(self: &Arc<Self>) -> usize {
let conn_ids = self.registry.active_conn_ids().await; let conn_ids = self.registry.active_conn_ids().await;
let total = conn_ids.len(); let total = conn_ids.len();
@@ -60,6 +89,7 @@ impl MePool {
total total
} }
/// Returns the current number of active ME writers tracked by the pool.
pub fn connection_count(&self) -> usize { pub fn connection_count(&self) -> usize {
self.conn_count.load(Ordering::Relaxed) self.conn_count.load(Ordering::Relaxed)
} }

View File

@@ -1,13 +1,9 @@
use std::collections::HashSet;
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tracing::warn; use tracing::warn;
use crate::network::IpFamily;
use super::super::MePool; use super::super::MePool;
use super::{ use super::{
HYBRID_GLOBAL_BURST_PERIOD_ROUNDS, HYBRID_RECENT_SUCCESS_WINDOW_MS, HYBRID_GLOBAL_BURST_PERIOD_ROUNDS, HYBRID_RECENT_SUCCESS_WINDOW_MS,
@@ -17,18 +13,18 @@ use super::{
impl MePool { impl MePool {
pub(super) async fn wait_for_writer_until(&self, deadline: Instant) -> bool { pub(super) async fn wait_for_writer_until(&self, deadline: Instant) -> bool {
let mut rx = self.writer_epoch.subscribe(); let mut rx = self.writer_epoch.subscribe();
if !self.writers.read().await.is_empty() { if !self.writers.snapshot().is_empty() {
return true; return true;
} }
let now = Instant::now(); let now = Instant::now();
if now >= deadline { if now >= deadline {
return !self.writers.read().await.is_empty(); return !self.writers.snapshot().is_empty();
} }
let timeout = deadline.saturating_duration_since(now); let timeout = deadline.saturating_duration_since(now);
if tokio::time::timeout(timeout, rx.changed()).await.is_ok() { if tokio::time::timeout(timeout, rx.changed()).await.is_ok() {
return !self.writers.read().await.is_empty(); return !self.writers.snapshot().is_empty();
} }
!self.writers.read().await.is_empty() !self.writers.snapshot().is_empty()
} }
pub(super) async fn wait_for_candidate_until(&self, routed_dc: i32, deadline: Instant) -> bool { pub(super) async fn wait_for_candidate_until(&self, routed_dc: i32, deadline: Instant) -> bool {
@@ -58,11 +54,11 @@ impl MePool {
pub(super) async fn has_candidate_for_target_dc(&self, routed_dc: i32) -> bool { pub(super) async fn has_candidate_for_target_dc(&self, routed_dc: i32) -> bool {
let writers_snapshot = { let writers_snapshot = {
let ws = self.writers.read().await; let ws = self.writers.snapshot();
if ws.is_empty() { if ws.is_empty() {
return false; return false;
} }
ws.clone() ws
}; };
let mut candidate_indices = self let mut candidate_indices = self
.candidate_indices_for_dc(&writers_snapshot, routed_dc, false) .candidate_indices_for_dc(&writers_snapshot, routed_dc, false)
@@ -79,7 +75,7 @@ impl MePool {
self: &Arc<Self>, self: &Arc<Self>,
routed_dc: i32, routed_dc: i32,
) -> bool { ) -> bool {
let endpoints = self.endpoint_candidates_for_target_dc(routed_dc).await; let endpoints = self.preferred_endpoints_for_dc(routed_dc).await;
if endpoints.is_empty() { if endpoints.is_empty() {
return false; return false;
} }
@@ -92,33 +88,19 @@ impl MePool {
pub(super) async fn trigger_async_recovery_global(self: &Arc<Self>) { pub(super) async fn trigger_async_recovery_global(self: &Arc<Self>) {
self.stats.increment_me_async_recovery_trigger_total(); self.stats.increment_me_async_recovery_trigger_total();
let mut seen = HashSet::<(i32, SocketAddr)>::new(); let preferred = self.preferred_endpoints_by_dc.load();
for family in self.family_order() { let mut triggered = 0usize;
let map_guard = match family { for (dc, addrs) in preferred.iter() {
IpFamily::V4 => self.proxy_map_v4.read().await, for addr in addrs {
IpFamily::V6 => self.proxy_map_v6.read().await, self.trigger_immediate_refill_for_dc(*addr, *dc);
}; triggered = triggered.saturating_add(1);
for (dc, addrs) in map_guard.iter() { if triggered >= 8 {
for (ip, port) in addrs { return;
let addr = SocketAddr::new(*ip, *port);
if seen.insert((*dc, addr)) {
self.trigger_immediate_refill_for_dc(addr, *dc);
}
if seen.len() >= 8 {
return;
}
} }
} }
} }
} }
pub(super) async fn endpoint_candidates_for_target_dc(
&self,
routed_dc: i32,
) -> Vec<SocketAddr> {
self.preferred_endpoints_for_dc(routed_dc).await
}
pub(super) async fn maybe_trigger_hybrid_recovery( pub(super) async fn maybe_trigger_hybrid_recovery(
self: &Arc<Self>, self: &Arc<Self>,
routed_dc: i32, routed_dc: i32,

View File

@@ -15,7 +15,10 @@ impl MePool {
routed_dc: i32, routed_dc: i32,
include_warm: bool, include_warm: bool,
) -> Vec<usize> { ) -> Vec<usize> {
let preferred = self.preferred_endpoints_for_dc(routed_dc).await; let preferred_snapshot = self.preferred_endpoints_by_dc.load();
let Some(preferred) = preferred_snapshot.get(&routed_dc) else {
return Vec::new();
};
if preferred.is_empty() { if preferred.is_empty() {
return Vec::new(); return Vec::new();
} }
@@ -25,7 +28,7 @@ impl MePool {
if !self.writer_eligible_for_selection(w, include_warm) { if !self.writer_eligible_for_selection(w, include_warm) {
continue; continue;
} }
if w.writer_dc == routed_dc && preferred.contains(&w.addr) { if w.writer_dc == routed_dc && preferred.binary_search(&w.addr).is_ok() {
out.push(idx); out.push(idx);
} }
} }