Compare commits

..

43 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
Alexey
231f04a810 Bump 2026-05-22 11:00:41 +03:00
Alexey
b32daf79bc Update Cargo.lock 2026-05-22 11:00:18 +03:00
Alexey
f668759c05 API Fixes + Exclusive Mask + Startup Speed-up + IDN + Decomposing hot-path modules: merge pull request #796 from telemt/flow
API Fixes + Exclusive Mask + Startup Speed-up + IDN + Decomposing hot-path modules
2026-05-22 10:55:26 +03:00
Alexey
4d9e835fa2 Merge branch 'main' into flow 2026-05-21 23:45:54 +03:00
Alexey
885258b85e Prioritize Relay Flow Cancellation over Buffered Writes 2026-05-21 20:35:25 +03:00
Alexey
b4c33eff39 Update CONTRIBUTING.md 2026-05-19 11:10:00 +03:00
TEMAndroid
855c5eef8b Merge branch 'main' into main 2026-05-18 10:41:24 +03:00
Alexey
01b0c5c6ce Merge pull request #786 from Dimasssss/patch-1
Update install.sh
2026-05-16 14:12:14 +03:00
Dimasssss
ad1bb5cc1a Update install.sh 2026-05-15 01:32:37 +03:00
Dimasssss
08cde1a255 Update install.sh 2026-05-15 01:29:13 +03:00
Dimasssss
faf1f28f9d Update install.sh 2026-05-15 01:23:45 +03:00
Dimasssss
32613c8e68 Update install.sh 2026-05-15 01:12:47 +03:00
TEMAndroid
b175927324 fix(docker): mount config as directory to allow atomic API writes 2026-05-11 20:50:31 +00:00
Alexey
1fe621f743 Update CONFIG_PARAMS.en.md 2026-05-10 17:37:41 +03:00
Alexey
3b0ebf3c9e Update CONFIG_PARAMS.ru.md 2026-05-10 17:37:31 +03:00
Alexey
b41f6bc21e Update CONFIG_PARAMS.en.md 2026-05-10 17:37:15 +03:00
Alexey
0a9f599611 Update CONFIG_PARAMS.en.md 2026-05-10 17:37:03 +03:00
Alexey
cdb021fc71 Update CONFIG_PARAMS.ru.md 2026-05-10 17:22:39 +03:00
Alexey
6b61183b9d Update CONFIG_PARAMS.en.md 2026-05-10 17:22:21 +03:00
36 changed files with 2173 additions and 1388 deletions

View File

@@ -52,6 +52,10 @@ By submitting a PR, you confirm that:
AI-generated code is treated as **draft** and must be validated like any other external contribution.
The problem isnt AI as a tool, but the dilution of responsibility. If the commit history says "Claude/GPT authored this", then who is accountable for the bug? Claude? GPT? Anthropic? OpenAI? Samuel Altman?
The user who didnt read the diff? No one? But, in a sensitive system, *"no one"* is an unacceptable maintainer model.
PRs that look like unverified AI dumps WILL be closed
---
@@ -79,4 +83,4 @@ This includes (but is not limited to):
- unverified or low-effort changes
- inability to explain the change
These actions follow the Code of Conduct and are intended to preserve signal, quality, and Telemt's integrity
These actions follow the Code of Conduct and are intended to preserve signal, quality, and Telemt's integrity

379
Cargo.lock generated
View File

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

View File

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

View File

@@ -4,13 +4,11 @@
[🇷🇺 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]
>
> 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">
<a href="https://t.me/telemtrs">

View File

@@ -10,12 +10,15 @@ services:
- "443:443"
- "127.0.0.1:9090:9090"
- "127.0.0.1:9091:9091"
# Allow caching 'proxy-secret' in read-only container
working_dir: /etc/telemt
# Working dir uses tmpfs for caching 'proxy-secret' at runtime.
# 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:
- ./config.toml:/etc/telemt/config.toml:ro
- ./config:/etc/telemt:rw
tmpfs:
- /etc/telemt:rw,mode=1777,size=4m
- /run/telemt:rw,mode=1777,size=4m
environment:
- RUST_LOG=info
healthcheck:
@@ -24,8 +27,6 @@ services:
timeout: 5s
retries: 3
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:
- ALL
cap_add:
@@ -37,3 +38,8 @@ services:
nofile:
soft: 65536
hard: 262144
logging:
driver: json-file
options:
max-size: "50m"
max-file: "5"

View File

@@ -10,6 +10,8 @@ This document lists all configuration keys accepted by `config.toml`.
>
> The configuration parameters detailed in this document are intended for advanced users and fine-tuning purposes. Modifying these settings without a clear understanding of their function may lead to application instability or other unexpected behavior. Please proceed with caution and at your own risk.
> `Hot-Reload` marks whether a changed value is applied by the config watcher without restarting the process; `✘` means restart is required for runtime effect.
# Table of contents
- [Top-level keys](#top-level-keys)
- [general](#general)
@@ -29,12 +31,16 @@ This document lists all configuration keys accepted by `config.toml`.
# Top-level keys
| Key | Type | Default |
| --- | ---- | ------- |
| [`include`](#include) | `String` (special directive) | — |
| [`show_link`](#show_link) | `"*"` or `String[]` | `[]` (`ShowLink::None`) |
| [`dc_overrides`](#dc_overrides) | `Map<String, String or String[]>` | `{}` |
| [`default_dc`](#default_dc) | `u8` | — (effective fallback: `2` in ME routing) |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`include`](#include) | `String` (special directive) | — | `✔` |
| [`show_link`](#show_link) | `"*"` or `String[]` | `[]` (`ShowLink::None`) | `✘` |
| [`dc_overrides`](#dc_overrides) | `Map<String, String or String[]>` | `{}` | `✘` |
| [`default_dc`](#default_dc) | `u8` | — (effective fallback: `2` in ME routing) | `✘` |
| [`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"` | `✘` |
## include
- **Constraints / validation**: Must be a single-line directive in the form `include = "path/to/file.toml"`. Includes are expanded before TOML parsing. Maximum include depth is 10.
@@ -79,145 +85,152 @@ This document lists all configuration keys accepted by `config.toml`.
# [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 |
| --- | ---- | ------- | ---------- |
| [`data_path`](#data_path) | `String` | — | `` |
| [`quota_state_path`](#quota_state_path) | `Path` | `"telemt.limit.json"` | `` |
| [`config_strict`](#config_strict) | `bool` | `false` | `` |
| [`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_secret_url`](#proxy_secret_url) | `String` | `"https://core.telegram.org/getProxySecret"` | `` |
| [`proxy_config_v4_cache_path`](#proxy_config_v4_cache_path) | `String` | `"cache/proxy-config-v4.txt"` | `` |
| [`proxy_config_v4_url`](#proxy_config_v4_url) | `String` | `"https://core.telegram.org/getProxyConfig"` | `` |
| [`proxy_config_v6_cache_path`](#proxy_config_v6_cache_path) | `String` | `"cache/proxy-config-v6.txt"` | `` |
| [`proxy_config_v6_url`](#proxy_config_v6_url) | `String` | `"https://core.telegram.org/getProxyConfigV6"` | `` |
| [`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` | `false` | `` |
| [`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` | `` |
| [`tg_connect`](#tg_connect) | `u64` | `10` | `` |
| [`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"` | `` |
## data_path
- **Constraints / validation**: `String` (optional).
@@ -228,6 +241,24 @@ This document lists all configuration keys accepted by `config.toml`.
[general]
data_path = "/var/lib/telemt"
```
## quota_state_path
- **Constraints / validation**: `Path`. Relative paths are resolved from the process working directory.
- **Description**: JSON state file used to persist runtime per-user quota consumption.
- **Example**:
```toml
[general]
quota_state_path = "telemt.limit.json"
```
## config_strict
- **Constraints / validation**: `bool`.
- **Description**: Rejects unknown TOML keys during config load. Startup fails fast; hot-reload rejects the new snapshot and keeps the current config.
- **Example**:
```toml
[general]
config_strict = true
```
## prefer_ipv6
- **Constraints / validation**: Deprecated. Use `network.prefer`.
- **Description**: Deprecated legacy IPv6 preference flag migrated to `network.prefer`.
@@ -905,6 +936,15 @@ This document lists all configuration keys accepted by `config.toml`.
[general]
upstream_connect_budget_ms = 3000
```
## tg_connect
- **Constraints / validation**: Must be `> 0` (seconds).
- **Description**: Upstream Telegram connect timeout.
- **Example**:
```toml
[general]
tg_connect = 10
```
## upstream_unhealthy_fail_threshold
- **Constraints / validation**: Must be `> 0`.
- **Description**: Consecutive failed requests before upstream is marked unhealthy.
@@ -1520,11 +1560,11 @@ This document lists all configuration keys accepted by `config.toml`.
# [general.modes]
| Key | Type | Default |
| --- | ---- | ------- |
| [`classic`](#classic) | `bool` | `false` |
| [`secure`](#secure) | `bool` | `false` |
| [`tls`](#tls) | `bool` | `true` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`classic`](#classic) | `bool` | `false` | `` |
| [`secure`](#secure) | `bool` | `false` | `` |
| [`tls`](#tls) | `bool` | `true` | `` |
## classic
- **Constraints / validation**: `bool`.
@@ -1558,11 +1598,11 @@ This document lists all configuration keys accepted by `config.toml`.
# [general.links]
| Key | Type | Default |
| --- | ---- | ------- |
| [`show`](#show) | `"*"` or `String[]` | `"*"` |
| [`public_host`](#public_host) | `String` | — |
| [`public_port`](#public_port) | `u16` | — |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`show`](#show) | `"*"` or `String[]` | `"*"` | `` |
| [`public_host`](#public_host) | `String` | — | `` |
| [`public_port`](#public_port) | `u16` | — | `` |
## show
- **Constraints / validation**: `"*"` or `String[]`. An empty array means "show none".
@@ -1598,11 +1638,11 @@ This document lists all configuration keys accepted by `config.toml`.
# [general.telemetry]
| Key | Type | Default |
| --- | ---- | ------- |
| [`core_enabled`](#core_enabled) | `bool` | `true` |
| [`user_enabled`](#user_enabled) | `bool` | `true` |
| [`me_level`](#me_level) | `"silent"`, `"normal"`, or `"debug"` | `"normal"` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`core_enabled`](#core_enabled) | `bool` | `true` | `` |
| [`user_enabled`](#user_enabled) | `bool` | `true` | `` |
| [`me_level`](#me_level) | `"silent"`, `"normal"`, or `"debug"` | `"normal"` | `` |
## core_enabled
- **Constraints / validation**: `bool`.
@@ -1636,18 +1676,18 @@ This document lists all configuration keys accepted by `config.toml`.
# [network]
| Key | Type | Default |
| --- | ---- | ------- |
| [`ipv4`](#ipv4) | `bool` | `true` |
| [`ipv6`](#ipv6) | `bool` | `false` |
| [`prefer`](#prefer) | `u8` | `4` |
| [`multipath`](#multipath) | `bool` | `false` |
| [`stun_use`](#stun_use) | `bool` | `true` |
| [`stun_servers`](#stun_servers) | `String[]` | Built-in STUN list (13 hosts) |
| [`stun_tcp_fallback`](#stun_tcp_fallback) | `bool` | `true` |
| [`http_ip_detect_urls`](#http_ip_detect_urls) | `String[]` | `["https://ifconfig.me/ip", "https://api.ipify.org"]` |
| [`cache_public_ip_path`](#cache_public_ip_path) | `String` | `"cache/public_ip.txt"` |
| [`dns_overrides`](#dns_overrides) | `String[]` | `[]` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`ipv4`](#ipv4) | `bool` | `true` | `` |
| [`ipv6`](#ipv6) | `bool` | `false` | `` |
| [`prefer`](#prefer) | `u8` | `4` | `` |
| [`multipath`](#multipath) | `bool` | `false` | `` |
| [`stun_use`](#stun_use) | `bool` | `true` | `` |
| [`stun_servers`](#stun_servers) | `String[]` | Built-in STUN list (13 hosts) | `` |
| [`stun_tcp_fallback`](#stun_tcp_fallback) | `bool` | `true` | `` |
| [`http_ip_detect_urls`](#http_ip_detect_urls) | `String[]` | `["https://ifconfig.me/ip", "https://api.ipify.org"]` | `` |
| [`cache_public_ip_path`](#cache_public_ip_path) | `String` | `"cache/public_ip.txt"` | `` |
| [`dns_overrides`](#dns_overrides) | `String[]` | `[]` | `` |
## ipv4
- **Constraints / validation**: `bool`.
@@ -1757,23 +1797,27 @@ This document lists all configuration keys accepted by `config.toml`.
# [server]
| Key | Type | Default |
| --- | ---- | ------- |
| [`port`](#port) | `u16` | `443` |
| [`listen_addr_ipv4`](#listen_addr_ipv4) | `String` | `"0.0.0.0"` |
| [`listen_addr_ipv6`](#listen_addr_ipv6) | `String` | `"::"` |
| [`listen_unix_sock`](#listen_unix_sock) | `String` | — |
| [`listen_unix_sock_perm`](#listen_unix_sock_perm) | `String` | — |
| [`listen_tcp`](#listen_tcp) | `bool` | — (auto) |
| [`proxy_protocol`](#proxy_protocol) | `bool` | `false` |
| [`proxy_protocol_header_timeout_ms`](#proxy_protocol_header_timeout_ms) | `u64` | `500` |
| [`proxy_protocol_trusted_cidrs`](#proxy_protocol_trusted_cidrs) | `IpNetwork[]` | `[]` |
| [`metrics_port`](#metrics_port) | `u16` | — |
| [`metrics_listen`](#metrics_listen) | `String` | — |
| [`metrics_whitelist`](#metrics_whitelist) | `IpNetwork[]` | `["127.0.0.1/32", "::1/128"]` |
| [`max_connections`](#max_connections) | `u32` | `10000` |
| [`accept_permit_timeout_ms`](#accept_permit_timeout_ms) | `u64` | `250` |
| [`listen_backlog`](#listen_backlog) | `u32` | `1024` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`port`](#port) | `u16` | `443` | `` |
| [`listen_addr_ipv4`](#listen_addr_ipv4) | `String` | `"0.0.0.0"` | `` |
| [`listen_addr_ipv6`](#listen_addr_ipv6) | `String` | `"::"` | `` |
| [`listen_unix_sock`](#listen_unix_sock) | `String` | — | `` |
| [`listen_unix_sock_perm`](#listen_unix_sock_perm) | `String` | — | `` |
| [`listen_tcp`](#listen_tcp) | `bool` | — (auto) | `` |
| [`proxy_protocol`](#proxy_protocol) | `bool` | `false` | `` |
| [`proxy_protocol_header_timeout_ms`](#proxy_protocol_header_timeout_ms) | `u64` | `500` | `` |
| [`proxy_protocol_trusted_cidrs`](#proxy_protocol_trusted_cidrs) | `IpNetwork[]` | `[]` | `` |
| [`metrics_port`](#metrics_port) | `u16` | — | `` |
| [`metrics_listen`](#metrics_listen) | `String` | — | `` |
| [`metrics_whitelist`](#metrics_whitelist) | `IpNetwork[]` | `["127.0.0.1/32", "::1/128"]` | `` |
| [`api`](#serverapi) | `Table` | built-in defaults | `` |
| [`admin_api`](#serverapi) | `Table` | alias for `api` | `` |
| [`listeners`](#serverlisteners) | `Table[]` | derived from legacy listener fields | `` |
| [`max_connections`](#max_connections) | `u32` | `10000` | `` |
| [`accept_permit_timeout_ms`](#accept_permit_timeout_ms) | `u64` | `250` | `` |
| [`listen_backlog`](#listen_backlog) | `u32` | `1024` | `` |
| [`conntrack_control`](#serverconntrack_control) | `Table` | built-in defaults | `` |
## port
- **Constraints / validation**: `u16`.
@@ -1930,16 +1974,16 @@ Note: When `server.proxy_protocol` is enabled, incoming PROXY protocol headers a
Note: The conntrack-control worker runs **only on Linux**. On other operating systems it is not started; if `inline_conntrack_control` is `true`, a warning is logged. Effective operation also requires **CAP_NET_ADMIN** and a usable backend (`nft` or `iptables` / `ip6tables` on `PATH`). The `conntrack` utility is used for optional table entry deletes under pressure.
| Key | Type | Default |
| --- | ---- | ------- |
| [`inline_conntrack_control`](#inline_conntrack_control) | `bool` | `true` |
| [`mode`](#mode) | `String` | `"tracked"` |
| [`backend`](#backend) | `String` | `"auto"` |
| [`profile`](#profile) | `String` | `"balanced"` |
| [`hybrid_listener_ips`](#hybrid_listener_ips) | `IpAddr[]` | `[]` |
| [`pressure_high_watermark_pct`](#pressure_high_watermark_pct) | `u8` | `85` |
| [`pressure_low_watermark_pct`](#pressure_low_watermark_pct) | `u8` | `70` |
| [`delete_budget_per_sec`](#delete_budget_per_sec) | `u64` | `4096` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`inline_conntrack_control`](#inline_conntrack_control) | `bool` | `true` | `` |
| [`mode`](#mode) | `String` | `"tracked"` | `` |
| [`backend`](#backend) | `String` | `"auto"` | `` |
| [`profile`](#profile) | `String` | `"balanced"` | `` |
| [`hybrid_listener_ips`](#hybrid_listener_ips) | `IpAddr[]` | `[]` | `` |
| [`pressure_high_watermark_pct`](#pressure_high_watermark_pct) | `u8` | `85` | `` |
| [`pressure_low_watermark_pct`](#pressure_low_watermark_pct) | `u8` | `70` | `` |
| [`delete_budget_per_sec`](#delete_budget_per_sec) | `u64` | `4096` | `` |
## inline_conntrack_control
- **Constraints / validation**: `bool`.
@@ -2021,21 +2065,21 @@ Note: The conntrack-control worker runs **only on Linux**. On other operating sy
Note: This section also accepts the legacy alias `[server.admin_api]` (same schema as `[server.api]`).
| Key | Type | Default |
| --- | ---- | ------- |
| [`enabled`](#enabled) | `bool` | `true` |
| [`listen`](#listen) | `String` | `"0.0.0.0:9091"` |
| [`whitelist`](#whitelist) | `IpNetwork[]` | `["127.0.0.0/8"]` |
| [`auth_header`](#auth_header) | `String` | `""` |
| [`request_body_limit_bytes`](#request_body_limit_bytes) | `usize` | `65536` |
| [`minimal_runtime_enabled`](#minimal_runtime_enabled) | `bool` | `true` |
| [`minimal_runtime_cache_ttl_ms`](#minimal_runtime_cache_ttl_ms) | `u64` | `1000` |
| [`runtime_edge_enabled`](#runtime_edge_enabled) | `bool` | `false` |
| [`runtime_edge_cache_ttl_ms`](#runtime_edge_cache_ttl_ms) | `u64` | `1000` |
| [`runtime_edge_top_n`](#runtime_edge_top_n) | `usize` | `10` |
| [`runtime_edge_events_capacity`](#runtime_edge_events_capacity) | `usize` | `256` |
| [`read_only`](#read_only) | `bool` | `false` |
| [`gray_action`](#gray_action) | `"drop"`, `"api"`, or `"200"` | `"drop"` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`enabled`](#enabled) | `bool` | `true` | `` |
| [`listen`](#listen) | `String` | `"0.0.0.0:9091"` | `` |
| [`whitelist`](#whitelist) | `IpNetwork[]` | `["127.0.0.0/8"]` | `` |
| [`auth_header`](#auth_header) | `String` | `""` | `` |
| [`request_body_limit_bytes`](#request_body_limit_bytes) | `usize` | `65536` | `` |
| [`minimal_runtime_enabled`](#minimal_runtime_enabled) | `bool` | `true` | `` |
| [`minimal_runtime_cache_ttl_ms`](#minimal_runtime_cache_ttl_ms) | `u64` | `1000` | `` |
| [`runtime_edge_enabled`](#runtime_edge_enabled) | `bool` | `false` | `` |
| [`runtime_edge_cache_ttl_ms`](#runtime_edge_cache_ttl_ms) | `u64` | `1000` | `` |
| [`runtime_edge_top_n`](#runtime_edge_top_n) | `usize` | `10` | `` |
| [`runtime_edge_events_capacity`](#runtime_edge_events_capacity) | `usize` | `256` | `` |
| [`read_only`](#read_only) | `bool` | `false` | `` |
| [`gray_action`](#gray_action) | `"drop"`, `"api"`, or `"200"` | `"drop"` | `` |
## enabled
- **Constraints / validation**: `bool`.
@@ -2159,13 +2203,14 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
# [[server.listeners]]
| Key | Type | Default |
| --- | ---- | ------- |
| [`ip`](#ip) | `IpAddr` | — |
| [`announce`](#announce) | `String` | — |
| [`announce_ip`](#announce_ip) | `IpAddr` | — |
| [`proxy_protocol`](#proxy_protocol) | `bool` | — |
| [`reuse_allow`](#reuse_allow) | `bool` | `false` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`ip`](#ip) | `IpAddr` | — | `` |
| [`port`](#port-serverlisteners) | `u16` | `server.port` | `` |
| [`announce`](#announce) | `String` | — | `` |
| [`announce_ip`](#announce_ip) | `IpAddr` | — | `` |
| [`proxy_protocol`](#proxy_protocol) | `bool` | — | `` |
| [`reuse_allow`](#reuse_allow) | `bool` | `false` | `` |
## ip
- **Constraints / validation**: Required field. Must be an `IpAddr`.
@@ -2176,6 +2221,16 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
[[server.listeners]]
ip = "0.0.0.0"
```
## port (server.listeners)
- **Constraints / validation**: `u16` (optional). When omitted, falls back to `server.port`.
- **Description**: Per-listener TCP port.
- **Example**:
```toml
[[server.listeners]]
ip = "0.0.0.0"
port = 443
```
## announce
- **Constraints / validation**: `String` (optional). Must not be empty when set.
- **Description**: Public IP/domain announced in proxy links for this listener. Takes precedence over `announce_ip`.
@@ -2209,8 +2264,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
ip = "0.0.0.0"
proxy_protocol = true
```
## reuse_allow"
- `reuse_allow`
## reuse_allow
- **Constraints / validation**: `bool`.
- **Description**: Enables `SO_REUSEPORT` for multi-instance bind sharing (allows multiple telemt instances to listen on the same `ip:port`).
- **Example**:
@@ -2225,18 +2279,18 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
# [timeouts]
| Key | Type | Default |
| --- | ---- | ------- |
| [`client_handshake`](#client_handshake) | `u64` | `30` |
| [`relay_idle_policy_v2_enabled`](#relay_idle_policy_v2_enabled) | `bool` | `true` |
| [`relay_client_idle_soft_secs`](#relay_client_idle_soft_secs) | `u64` | `120` |
| [`relay_client_idle_hard_secs`](#relay_client_idle_hard_secs) | `u64` | `360` |
| [`relay_idle_grace_after_downstream_activity_secs`](#relay_idle_grace_after_downstream_activity_secs) | `u64` | `30` |
| [`tg_connect`](#tg_connect) | `u64` | `10` |
| [`client_keepalive`](#client_keepalive) | `u64` | `15` |
| [`client_ack`](#client_ack) | `u64` | `90` |
| [`me_one_retry`](#me_one_retry) | `u8` | `12` |
| [`me_one_timeout_ms`](#me_one_timeout_ms) | `u64` | `1200` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`client_first_byte_idle_secs`](#client_first_byte_idle_secs) | `u64` | `300` | `` |
| [`client_handshake`](#client_handshake) | `u64` | `30` | `` |
| [`relay_idle_policy_v2_enabled`](#relay_idle_policy_v2_enabled) | `bool` | `true` | `` |
| [`relay_client_idle_soft_secs`](#relay_client_idle_soft_secs) | `u64` | `120` | `` |
| [`relay_client_idle_hard_secs`](#relay_client_idle_hard_secs) | `u64` | `360` | `` |
| [`relay_idle_grace_after_downstream_activity_secs`](#relay_idle_grace_after_downstream_activity_secs) | `u64` | `30` | `` |
| [`client_keepalive`](#client_keepalive) | `u64` | `15` | `` |
| [`client_ack`](#client_ack) | `u64` | `90` | `` |
| [`me_one_retry`](#me_one_retry) | `u8` | `12` | `` |
| [`me_one_timeout_ms`](#me_one_timeout_ms) | `u64` | `1200` | `` |
## client_handshake
- **Constraints / validation**: Must be `> 0`. Value is in seconds. Also used as an upper bound for some TLS emulation delays (see `censorship.server_hello_delay_max_ms`).
@@ -2292,15 +2346,6 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
[timeouts]
relay_idle_grace_after_downstream_activity_secs = 30
```
## tg_connect
- **Constraints / validation**: `u64`. Value is in seconds.
- **Description**: Upstream Telegram connect timeout (seconds).
- **Example**:
```toml
[timeouts]
tg_connect = 10
```
## client_keepalive
- **Constraints / validation**: `u64`. Value is in seconds.
- **Description**: Client keepalive timeout (seconds).
@@ -2342,41 +2387,40 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
# [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 |
| --- | ---- | ------- | ---------- |
| [`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` | `` |
| [`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` | `` |
## tls_domain
- **Constraints / validation**: Must be a non-empty domain name. Must not contain spaces or `/`.
@@ -2810,15 +2854,15 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
# [censorship.tls_fetch]
| Key | Type | Default |
| --- | ---- | ------- |
| [`profiles`](#profiles) | `String[]` | `["modern_chrome_like", "modern_firefox_like", "compat_tls12", "legacy_minimal"]` |
| [`strict_route`](#strict_route) | `bool` | `true` |
| [`attempt_timeout_ms`](#attempt_timeout_ms) | `u64` | `5000` |
| [`total_budget_ms`](#total_budget_ms) | `u64` | `15000` |
| [`grease_enabled`](#grease_enabled) | `bool` | `false` |
| [`deterministic`](#deterministic) | `bool` | `false` |
| [`profile_cache_ttl_secs`](#profile_cache_ttl_secs) | `u64` | `600` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`profiles`](#profiles) | `String[]` | `["modern_chrome_like", "modern_firefox_like", "compat_tls12", "legacy_minimal"]` | `` |
| [`strict_route`](#strict_route) | `bool` | `true` | `` |
| [`attempt_timeout_ms`](#attempt_timeout_ms) | `u64` | `5000` | `` |
| [`total_budget_ms`](#total_budget_ms) | `u64` | `15000` | `` |
| [`grease_enabled`](#grease_enabled) | `bool` | `false` | `` |
| [`deterministic`](#deterministic) | `bool` | `false` | `` |
| [`profile_cache_ttl_secs`](#profile_cache_ttl_secs) | `u64` | `600` | `` |
## profiles
- **Constraints / validation**: `String[]`. Empty list falls back to defaults; values are deduplicated preserving order.
@@ -2887,24 +2931,24 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
# [access]
| Key | Type | Default |
| --- | ---- | ------- |
| [`users`](#users) | `Map<String, String>` | `{"default": "000…000"}` |
| [`user_ad_tags`](#user_ad_tags) | `Map<String, String>` | `{}` |
| [`user_max_tcp_conns`](#user_max_tcp_conns) | `Map<String, usize>` | `{}` |
| [`user_max_tcp_conns_global_each`](#user_max_tcp_conns_global_each) | `usize` | `0` |
| [`user_expirations`](#user_expirations) | `Map<String, DateTime<Utc>>` | `{}` |
| [`user_data_quota`](#user_data_quota) | `Map<String, u64>` | `{}` |
| [`user_max_unique_ips`](#user_max_unique_ips) | `Map<String, usize>` | `{}` |
| [`user_max_unique_ips_global_each`](#user_max_unique_ips_global_each) | `usize` | `0` |
| [`user_max_unique_ips_mode`](#user_max_unique_ips_mode) | `"active_window"`, `"time_window"`, or `"combined"` | `"active_window"` |
| [`user_max_unique_ips_window_secs`](#user_max_unique_ips_window_secs) | `u64` | `30` |
| [`user_source_deny`](#user_source_deny) | `Map<String, IpNetwork[]>` | `{}` |
| [`replay_check_len`](#replay_check_len) | `usize` | `65536` |
| [`replay_window_secs`](#replay_window_secs) | `u64` | `120` |
| [`ignore_time_skew`](#ignore_time_skew) | `bool` | `false` |
| [`user_rate_limits`](#user_rate_limits) | `Map<String, RateLimitBps>` | `{}` |
| [`cidr_rate_limits`](#cidr_rate_limits) | `Map<IpNetwork, RateLimitBps>` | `{}` |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`users`](#users) | `Map<String, String>` | `{"default": "000…000"}` | `` |
| [`user_ad_tags`](#user_ad_tags) | `Map<String, String>` | `{}` | `` |
| [`user_max_tcp_conns`](#user_max_tcp_conns) | `Map<String, usize>` | `{}` | `` |
| [`user_max_tcp_conns_global_each`](#user_max_tcp_conns_global_each) | `usize` | `0` | `` |
| [`user_expirations`](#user_expirations) | `Map<String, DateTime<Utc>>` | `{}` | `` |
| [`user_data_quota`](#user_data_quota) | `Map<String, u64>` | `{}` | `` |
| [`user_max_unique_ips`](#user_max_unique_ips) | `Map<String, usize>` | `{}` | `` |
| [`user_max_unique_ips_global_each`](#user_max_unique_ips_global_each) | `usize` | `0` | `` |
| [`user_max_unique_ips_mode`](#user_max_unique_ips_mode) | `"active_window"`, `"time_window"`, or `"combined"` | `"active_window"` | `` |
| [`user_max_unique_ips_window_secs`](#user_max_unique_ips_window_secs) | `u64` | `30` | `` |
| [`user_source_deny`](#user_source_deny) | `Map<String, IpNetwork[]>` | `{}` | `` |
| [`replay_check_len`](#replay_check_len) | `usize` | `65536` | `` |
| [`replay_window_secs`](#replay_window_secs) | `u64` | `120` | `` |
| [`ignore_time_skew`](#ignore_time_skew) | `bool` | `false` | `` |
| [`user_rate_limits`](#user_rate_limits) | `Map<String, RateLimitBps>` | `{}` | `` |
| [`cidr_rate_limits`](#cidr_rate_limits) | `Map<IpNetwork, RateLimitBps>` | `{}` | `` |
## users
- **Constraints / validation**: Must not be empty (at least one user must exist). Each value must be **exactly 32 hex characters**.
@@ -3068,19 +3112,23 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
# [[upstreams]]
| Key | Type | Default |
| --- | ---- | ------- |
| [`type`](#type) | `"direct"`, `"socks4"`, `"socks5"`, or `"shadowsocks"` | — |
| [`weight`](#weight) | `u16` | `1` |
| [`enabled`](#enabled) | `bool` | `true` |
| [`scopes`](#scopes) | `String` | `""` |
| [`interface`](#interface) | `String` | — |
| [`bind_addresses`](#bind_addresses) | `String[]` | — |
| [`url`](#url) | `String` | — |
| [`address`](#address) | `String` | — |
| [`user_id`](#user_id) | `String` | — |
| [`username`](#username) | `String` | — |
| [`password`](#password) | `String` | — |
| Key | Type | Default | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`type`](#type) | `"direct"`, `"socks4"`, `"socks5"`, or `"shadowsocks"` | — | `` |
| [`weight`](#weight) | `u16` | `1` | `` |
| [`enabled`](#enabled) | `bool` | `true` | `` |
| [`scopes`](#scopes) | `String` | `""` | `` |
| [`ipv4`](#ipv4-upstreams) | `bool` | — (auto) | `` |
| [`ipv6`](#ipv6-upstreams) | `bool` | — (auto) | `` |
| [`interface`](#interface) | `String` | — | `` |
| [`bind_addresses`](#bind_addresses) | `String[]` | — | `` |
| [`bindtodevice`](#bindtodevice) | `String` | — | `` |
| [`force_bind`](#force_bind) | `String` | — | `` |
| [`url`](#url) | `String` | — | `` |
| [`address`](#address) | `String` | — | `` |
| [`user_id`](#user_id) | `String` | — | `` |
| [`username`](#username) | `String` | — | `` |
| [`password`](#password) | `String` | — | `` |
## type
- **Constraints / validation**: Required field. Must be one of: `"direct"`, `"socks4"`, `"socks5"`, `"shadowsocks"`.
@@ -3131,6 +3179,26 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
address = "10.0.0.10:1080"
scopes = "me, fetch, dc2"
```
## ipv4 (upstreams)
- **Constraints / validation**: `bool` (optional).
- **Description**: Allows IPv4 DC targets for this upstream. When omitted, Telemt auto-detects support from runtime connectivity state.
- **Example**:
```toml
[[upstreams]]
type = "direct"
ipv4 = true
```
## ipv6 (upstreams)
- **Constraints / validation**: `bool` (optional).
- **Description**: Allows IPv6 DC targets for this upstream. When omitted, Telemt auto-detects support from runtime connectivity state.
- **Example**:
```toml
[[upstreams]]
type = "direct"
ipv6 = false
```
## interface
- **Constraints / validation**: `String` (optional).
- For `"direct"`: may be an IP address (used as explicit local bind) or an OS interface name (resolved to an IP at runtime; Unix only).
@@ -3161,6 +3229,26 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
type = "direct"
bind_addresses = ["192.0.2.10", "192.0.2.11"]
```
## bindtodevice
- **Constraints / validation**: `String` (optional). Applies only to `type = "direct"` and is Linux-only.
- **Description**: Hard interface pinning via `SO_BINDTODEVICE` for outgoing direct TCP connects.
- **Example**:
```toml
[[upstreams]]
type = "direct"
bindtodevice = "eth0"
```
## force_bind
- **Constraints / validation**: `String` (optional). Alias for `bindtodevice`.
- **Description**: Backward-compatible alias for Linux `SO_BINDTODEVICE` hard interface pinning.
- **Example**:
```toml
[[upstreams]]
type = "direct"
force_bind = "eth0"
```
## url
- **Constraints / validation**: Applies only to `type = "shadowsocks"`.
- Must be a valid Shadowsocks URL accepted by the `shadowsocks` crate.

View File

@@ -10,6 +10,8 @@
>
> Параметры конфигурации, подробно описанные в этом документе, предназначены для опытных пользователей и для целей тонкой настройки. Изменение этих параметров без четкого понимания их функции может привести к нестабильности приложения или другому неожиданному поведению. Пожалуйста, действуйте осторожно и на свой страх и риск.
> `Hot-Reload` показывает, применяет ли config watcher изменение без перезапуска процесса; `✘` означает, что для runtime-эффекта нужен перезапуск.
# Содержание
- [Ключи верхнего уровня](#top-level-keys)
- [general](#general)
@@ -29,12 +31,16 @@
# Ключи верхнего уровня
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`include`](#include) | `String` (специальная директива) | — |
| [`show_link`](#show_link) | `"*"` or `String[]` | `[]` (`ShowLink::None`) |
| [`dc_overrides`](#dc_overrides) | `Map<String, String or String[]>` | `{}` |
| [`default_dc`](#default_dc) | `u8` | — (эффективный резервный вариант: `2` в ME маршрутизации) |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`include`](#include) | `String` (специальная директива) | — | `✔` |
| [`show_link`](#show_link) | `"*"` or `String[]` | `[]` (`ShowLink::None`) | `✘` |
| [`dc_overrides`](#dc_overrides) | `Map<String, String or String[]>` | `{}` | `✘` |
| [`default_dc`](#default_dc) | `u8` | — (эффективный резервный вариант: `2` в ME маршрутизации) | `✘` |
| [`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"` | `✘` |
## include
- **Ограничения / валидация**: значение должно быть одной строкой в виде `include = "path/to/file.toml"`. Значения параметра обрабатываются перед анализом TOML. Максимальное количество - 10.
@@ -79,145 +85,152 @@
# [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 |
| --- | ---- | ------- | ---------- |
| [`data_path`](#data_path) | `String` | — | `` |
| [`quota_state_path`](#quota_state_path) | `Path` | `"telemt.limit.json"` | `` |
| [`config_strict`](#config_strict) | `bool` | `false` | `` |
| [`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_secret_url`](#proxy_secret_url) | `String` | `"https://core.telegram.org/getProxySecret"` | `` |
| [`proxy_config_v4_cache_path`](#proxy_config_v4_cache_path) | `String` | `"cache/proxy-config-v4.txt"` | `` |
| [`proxy_config_v4_url`](#proxy_config_v4_url) | `String` | `"https://core.telegram.org/getProxyConfig"` | `` |
| [`proxy_config_v6_cache_path`](#proxy_config_v6_cache_path) | `String` | `"cache/proxy-config-v6.txt"` | `` |
| [`proxy_config_v6_url`](#proxy_config_v6_url) | `String` | `"https://core.telegram.org/getProxyConfigV6"` | `` |
| [`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` | `false` | `` |
| [`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` | `` |
| [`tg_connect`](#tg_connect) | `u64` | `10` | `` |
| [`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"` | `` |
## data_path
- **Ограничения / валидация**: `String` (необязательный параметр).
@@ -228,6 +241,24 @@
[general]
data_path = "/var/lib/telemt"
```
## quota_state_path
- **Ограничения / валидация**: `Path`. Относительные пути разрешаются от рабочего каталога процесса.
- **Описание**: JSON-файл состояния для сохранения runtime-расхода квот по пользователям.
- **Пример**:
```toml
[general]
quota_state_path = "telemt.limit.json"
```
## config_strict
- **Ограничения / валидация**: `bool`.
- **Описание**: Отклоняет неизвестные TOML-ключи во время загрузки конфигурации. При запуске процесс завершается с ошибкой; при hot-reload новый снимок отклоняется, а текущая конфигурация сохраняется.
- **Пример**:
```toml
[general]
config_strict = true
```
## prefer_ipv6
- **Ограничения / валидация**: Устарело. Используйте `network.prefer`.
- **Описание**: Устаревший флаг предпочтения IPv6 перенесен в `network.prefer`.
@@ -905,6 +936,15 @@
[general]
upstream_connect_budget_ms = 3000
```
## tg_connect
- **Ограничения / валидация**: Должно быть `> 0` (секунды).
- **Описание**: Таймаут подключения к upstream-серверам Telegram.
- **Пример**:
```toml
[general]
tg_connect = 10
```
## upstream_unhealthy_fail_threshold
- **Ограничения / валидация**: Должно быть `> 0`.
- **Описание**: Количество неудачных запросов подряд, после которого upstream помечается, как неработоспособный.
@@ -1522,11 +1562,11 @@
# [general.modes]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`classic`](#classic) | `bool` | `false` |
| [`secure`](#secure) | `bool` | `false` |
| [`tls`](#tls) | `bool` | `true` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`classic`](#classic) | `bool` | `false` | `` |
| [`secure`](#secure) | `bool` | `false` | `` |
| [`tls`](#tls) | `bool` | `true` | `` |
## classic
- **Ограничения / валидация**: `bool`.
@@ -1560,11 +1600,11 @@
# [general.links]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`show`](#show) | `"*"` or `String[]` | `"*"` |
| [`public_host`](#public_host) | `String` | — |
| [`public_port`](#public_port) | `u16` | — |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`show`](#show) | `"*"` or `String[]` | `"*"` | `` |
| [`public_host`](#public_host) | `String` | — | `` |
| [`public_port`](#public_port) | `u16` | — | `` |
## show
- **Ограничения / валидация**: `"*"` или `String[]`. Пустое значение означает, что нельзя показывать никому.
@@ -1600,11 +1640,11 @@
# [general.telemetry]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`core_enabled`](#core_enabled) | `bool` | `true` |
| [`user_enabled`](#user_enabled) | `bool` | `true` |
| [`me_level`](#me_level) | `"silent"`, `"normal"`, or `"debug"` | `"normal"` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`core_enabled`](#core_enabled) | `bool` | `true` | `` |
| [`user_enabled`](#user_enabled) | `bool` | `true` | `` |
| [`me_level`](#me_level) | `"silent"`, `"normal"`, or `"debug"` | `"normal"` | `` |
## core_enabled
- **Ограничения / валидация**: `bool`.
@@ -1638,18 +1678,18 @@
# [network]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`ipv4`](#ipv4) | `bool` | `true` |
| [`ipv6`](#ipv6) | `bool` | `false` |
| [`prefer`](#prefer) | `u8` | `4` |
| [`multipath`](#multipath) | `bool` | `false` |
| [`stun_use`](#stun_use) | `bool` | `true` |
| [`stun_servers`](#stun_servers) | `String[]` | Встроенный STUN-лист (13 записей) |
| [`stun_tcp_fallback`](#stun_tcp_fallback) | `bool` | `true` |
| [`http_ip_detect_urls`](#http_ip_detect_urls) | `String[]` | `["https://ifconfig.me/ip", "https://api.ipify.org"]` |
| [`cache_public_ip_path`](#cache_public_ip_path) | `String` | `"cache/public_ip.txt"` |
| [`dns_overrides`](#dns_overrides) | `String[]` | `[]` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`ipv4`](#ipv4) | `bool` | `true` | `` |
| [`ipv6`](#ipv6) | `bool` | `false` | `` |
| [`prefer`](#prefer) | `u8` | `4` | `` |
| [`multipath`](#multipath) | `bool` | `false` | `` |
| [`stun_use`](#stun_use) | `bool` | `true` | `` |
| [`stun_servers`](#stun_servers) | `String[]` | Встроенный STUN-лист (13 записей) | `` |
| [`stun_tcp_fallback`](#stun_tcp_fallback) | `bool` | `true` | `` |
| [`http_ip_detect_urls`](#http_ip_detect_urls) | `String[]` | `["https://ifconfig.me/ip", "https://api.ipify.org"]` | `` |
| [`cache_public_ip_path`](#cache_public_ip_path) | `String` | `"cache/public_ip.txt"` | `` |
| [`dns_overrides`](#dns_overrides) | `String[]` | `[]` | `` |
## ipv4
- **Ограничения / валидация**: `bool`.
@@ -1759,23 +1799,27 @@
# [server]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`port`](#port) | `u16` | `443` |
| [`listen_addr_ipv4`](#listen_addr_ipv4) | `String` | `"0.0.0.0"` |
| [`listen_addr_ipv6`](#listen_addr_ipv6) | `String` | `"::"` |
| [`listen_unix_sock`](#listen_unix_sock) | `String` | — |
| [`listen_unix_sock_perm`](#listen_unix_sock_perm) | `String` | — |
| [`listen_tcp`](#listen_tcp) | `bool` | — (auto) |
| [`proxy_protocol`](#proxy_protocol) | `bool` | `false` |
| [`proxy_protocol_header_timeout_ms`](#proxy_protocol_header_timeout_ms) | `u64` | `500` |
| [`proxy_protocol_trusted_cidrs`](#proxy_protocol_trusted_cidrs) | `IpNetwork[]` | `[]` |
| [`metrics_port`](#metrics_port) | `u16` | — |
| [`metrics_listen`](#metrics_listen) | `String` | — |
| [`metrics_whitelist`](#metrics_whitelist) | `IpNetwork[]` | `["127.0.0.1/32", "::1/128"]` |
| [`max_connections`](#max_connections) | `u32` | `10000` |
| [`accept_permit_timeout_ms`](#accept_permit_timeout_ms) | `u64` | `250` |
| [`listen_backlog`](#listen_backlog) | `u32` | `1024` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`port`](#port) | `u16` | `443` | `` |
| [`listen_addr_ipv4`](#listen_addr_ipv4) | `String` | `"0.0.0.0"` | `` |
| [`listen_addr_ipv6`](#listen_addr_ipv6) | `String` | `"::"` | `` |
| [`listen_unix_sock`](#listen_unix_sock) | `String` | — | `` |
| [`listen_unix_sock_perm`](#listen_unix_sock_perm) | `String` | — | `` |
| [`listen_tcp`](#listen_tcp) | `bool` | — (auto) | `` |
| [`proxy_protocol`](#proxy_protocol) | `bool` | `false` | `` |
| [`proxy_protocol_header_timeout_ms`](#proxy_protocol_header_timeout_ms) | `u64` | `500` | `` |
| [`proxy_protocol_trusted_cidrs`](#proxy_protocol_trusted_cidrs) | `IpNetwork[]` | `[]` | `` |
| [`metrics_port`](#metrics_port) | `u16` | — | `` |
| [`metrics_listen`](#metrics_listen) | `String` | — | `` |
| [`metrics_whitelist`](#metrics_whitelist) | `IpNetwork[]` | `["127.0.0.1/32", "::1/128"]` | `` |
| [`api`](#serverapi) | `Table` | встроенные значения | `` |
| [`admin_api`](#serverapi) | `Table` | алиас для `api` | `` |
| [`listeners`](#serverlisteners) | `Table[]` | выводится из legacy listener-полей | `` |
| [`max_connections`](#max_connections) | `u32` | `10000` | `` |
| [`accept_permit_timeout_ms`](#accept_permit_timeout_ms) | `u64` | `250` | `` |
| [`listen_backlog`](#listen_backlog) | `u32` | `1024` | `` |
| [`conntrack_control`](#serverconntrack_control) | `Table` | встроенные значения | `` |
## port
- **Ограничения / валидация**: `u16`.
@@ -1931,16 +1975,16 @@
Примечание. Рабочий процесс `conntrack-control` работает **только в Linux**. В других операционных системах не запускается; если inline_conntrack_control имеет значение `true`, в логи записывается предупреждение. Для эффективной работы также требуется **CAP_NET_ADMIN** и пригодный к использованию бэкенд (nft или iptables/ip6tables в PATH). Утилита `conntrack` используется для удаления необязательных записей таблицы под нагрузкой.
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`inline_conntrack_control`](#inline_conntrack_control) | `bool` | `true` |
| [`mode`](#mode) | `String` | `"tracked"` |
| [`backend`](#backend) | `String` | `"auto"` |
| [`profile`](#profile) | `String` | `"balanced"` |
| [`hybrid_listener_ips`](#hybrid_listener_ips) | `IpAddr[]` | `[]` |
| [`pressure_high_watermark_pct`](#pressure_high_watermark_pct) | `u8` | `85` |
| [`pressure_low_watermark_pct`](#pressure_low_watermark_pct) | `u8` | `70` |
| [`delete_budget_per_sec`](#delete_budget_per_sec) | `u64` | `4096` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`inline_conntrack_control`](#inline_conntrack_control) | `bool` | `true` | `` |
| [`mode`](#mode) | `String` | `"tracked"` | `` |
| [`backend`](#backend) | `String` | `"auto"` | `` |
| [`profile`](#profile) | `String` | `"balanced"` | `` |
| [`hybrid_listener_ips`](#hybrid_listener_ips) | `IpAddr[]` | `[]` | `` |
| [`pressure_high_watermark_pct`](#pressure_high_watermark_pct) | `u8` | `85` | `` |
| [`pressure_low_watermark_pct`](#pressure_low_watermark_pct) | `u8` | `70` | `` |
| [`delete_budget_per_sec`](#delete_budget_per_sec) | `u64` | `4096` | `` |
## inline_conntrack_control
- **Ограничения / валидация**: `bool`.
@@ -2027,21 +2071,21 @@
Примечание: В этом разделе также задается устаревший параметр `[server.admin_api]` (аналогично `[server.api]`).
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`enabled`](#enabled) | `bool` | `true` |
| [`listen`](#listen) | `String` | `"0.0.0.0:9091"` |
| [`whitelist`](#whitelist) | `IpNetwork[]` | `["127.0.0.0/8"]` |
| [`auth_header`](#auth_header) | `String` | `""` |
| [`request_body_limit_bytes`](#request_body_limit_bytes) | `usize` | `65536` |
| [`minimal_runtime_enabled`](#minimal_runtime_enabled) | `bool` | `true` |
| [`minimal_runtime_cache_ttl_ms`](#minimal_runtime_cache_ttl_ms) | `u64` | `1000` |
| [`runtime_edge_enabled`](#runtime_edge_enabled) | `bool` | `false` |
| [`runtime_edge_cache_ttl_ms`](#runtime_edge_cache_ttl_ms) | `u64` | `1000` |
| [`runtime_edge_top_n`](#runtime_edge_top_n) | `usize` | `10` |
| [`runtime_edge_events_capacity`](#runtime_edge_events_capacity) | `usize` | `256` |
| [`read_only`](#read_only) | `bool` | `false` |
| [`gray_action`](#gray_action) | `"drop"`, `"api"`, or `"200"` | `"drop"` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`enabled`](#enabled) | `bool` | `true` | `` |
| [`listen`](#listen) | `String` | `"0.0.0.0:9091"` | `` |
| [`whitelist`](#whitelist) | `IpNetwork[]` | `["127.0.0.0/8"]` | `` |
| [`auth_header`](#auth_header) | `String` | `""` | `` |
| [`request_body_limit_bytes`](#request_body_limit_bytes) | `usize` | `65536` | `` |
| [`minimal_runtime_enabled`](#minimal_runtime_enabled) | `bool` | `true` | `` |
| [`minimal_runtime_cache_ttl_ms`](#minimal_runtime_cache_ttl_ms) | `u64` | `1000` | `` |
| [`runtime_edge_enabled`](#runtime_edge_enabled) | `bool` | `false` | `` |
| [`runtime_edge_cache_ttl_ms`](#runtime_edge_cache_ttl_ms) | `u64` | `1000` | `` |
| [`runtime_edge_top_n`](#runtime_edge_top_n) | `usize` | `10` | `` |
| [`runtime_edge_events_capacity`](#runtime_edge_events_capacity) | `usize` | `256` | `` |
| [`read_only`](#read_only) | `bool` | `false` | `` |
| [`gray_action`](#gray_action) | `"drop"`, `"api"`, or `"200"` | `"drop"` | `` |
## enabled
- **Ограничения / валидация**: `bool`.
@@ -2165,13 +2209,14 @@
# [[server.listeners]]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`ip`](#ip) | `IpAddr` | — |
| [`announce`](#announce) | `String` | — |
| [`announce_ip`](#announce_ip) | `IpAddr` | — |
| [`proxy_protocol`](#proxy_protocol) | `bool` | — |
| [`reuse_allow`](#reuse_allow) | `bool` | `false` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`ip`](#ip) | `IpAddr` | — | `` |
| [`port`](#port-serverlisteners) | `u16` | `server.port` | `` |
| [`announce`](#announce) | `String` | — | `` |
| [`announce_ip`](#announce_ip) | `IpAddr` | — | `` |
| [`proxy_protocol`](#proxy_protocol) | `bool` | — | `` |
| [`reuse_allow`](#reuse_allow) | `bool` | `false` | `` |
## ip
- **Ограничения / валидация**: Обязательный параметр. Значение должно содержать IP-адрес в формате строки.
@@ -2182,6 +2227,16 @@
[[server.listeners]]
ip = "0.0.0.0"
```
## port (server.listeners)
- **Ограничения / валидация**: `u16` (необязательный параметр). Если не задан, используется `server.port`.
- **Описание**: TCP-порт для конкретного listenerа.
- **Пример**:
```toml
[[server.listeners]]
ip = "0.0.0.0"
port = 443
```
## announce
- **Ограничения / валидация**: `String` (необязательный параметр). Не должен быть пустым, если задан.
- **Описание**: Публичный IP-адрес или домен, объявляемый в proxy-ссылках для данного listenerа. Имеет приоритет над `announce_ip`.
@@ -2215,8 +2270,7 @@
ip = "0.0.0.0"
proxy_protocol = true
```
## reuse_allow"
- `reuse_allow`
## reuse_allow
- **Ограничения / валидация**: `bool`.
- **Описание**: Включает `SO_REUSEPORT` для совместного использования привязки нескольких экземпляров (позволяет нескольким экземплярам telemt прослушивать один и тот же `ip:port`).
- **Пример**:
@@ -2231,18 +2285,18 @@
# [timeouts]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`client_handshake`](#client_handshake) | `u64` | `30` |
| [`relay_idle_policy_v2_enabled`](#relay_idle_policy_v2_enabled) | `bool` | `true` |
| [`relay_client_idle_soft_secs`](#relay_client_idle_soft_secs) | `u64` | `120` |
| [`relay_client_idle_hard_secs`](#relay_client_idle_hard_secs) | `u64` | `360` |
| [`relay_idle_grace_after_downstream_activity_secs`](#relay_idle_grace_after_downstream_activity_secs) | `u64` | `30` |
| [`tg_connect`](#tg_connect) | `u64` | `10` |
| [`client_keepalive`](#client_keepalive) | `u64` | `15` |
| [`client_ack`](#client_ack) | `u64` | `90` |
| [`me_one_retry`](#me_one_retry) | `u8` | `12` |
| [`me_one_timeout_ms`](#me_one_timeout_ms) | `u64` | `1200` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`client_first_byte_idle_secs`](#client_first_byte_idle_secs) | `u64` | `300` | `` |
| [`client_handshake`](#client_handshake) | `u64` | `30` | `` |
| [`relay_idle_policy_v2_enabled`](#relay_idle_policy_v2_enabled) | `bool` | `true` | `` |
| [`relay_client_idle_soft_secs`](#relay_client_idle_soft_secs) | `u64` | `120` | `` |
| [`relay_client_idle_hard_secs`](#relay_client_idle_hard_secs) | `u64` | `360` | `` |
| [`relay_idle_grace_after_downstream_activity_secs`](#relay_idle_grace_after_downstream_activity_secs) | `u64` | `30` | `` |
| [`client_keepalive`](#client_keepalive) | `u64` | `15` | `` |
| [`client_ack`](#client_ack) | `u64` | `90` | `` |
| [`me_one_retry`](#me_one_retry) | `u8` | `12` | `` |
| [`me_one_timeout_ms`](#me_one_timeout_ms) | `u64` | `1200` | `` |
## client_handshake
- **Ограничения / валидация**: Должно быть `> 0`. Значение указано в секундах. Также используется в качестве верхней границы некоторых задержек эмуляции TLS (см. `censorship.server_hello_delay_max_ms`).
@@ -2298,15 +2352,6 @@
[timeouts]
relay_idle_grace_after_downstream_activity_secs = 30
```
## tg_connect
- **Ограничения / валидация**: `u64` (секунд).
- **Описание**: Таймаут подключения к upstream-серверу Telegram (в секундах).
- **Пример**:
```toml
[timeouts]
tg_connect = 10
```
## client_keepalive
- **Ограничения / валидация**: `u64` (секунд).
- **Описание**: Таймаут keepalive для клиента..
@@ -2348,41 +2393,40 @@
# [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 |
| --- | ---- | ------- | ---------- |
| [`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` | `` |
| [`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` | `` |
## tls_domain
- **Ограничения / валидация**: Не должно быть пустым. Не должно содержать пробелы или `/`.
@@ -2817,15 +2861,15 @@
# [censorship.tls_fetch]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`profiles`](#profiles) | `String[]` | `["modern_chrome_like", "modern_firefox_like", "compat_tls12", "legacy_minimal"]` |
| [`strict_route`](#strict_route) | `bool` | `true` |
| [`attempt_timeout_ms`](#attempt_timeout_ms) | `u64` | `5000` |
| [`total_budget_ms`](#total_budget_ms) | `u64` | `15000` |
| [`grease_enabled`](#grease_enabled) | `bool` | `false` |
| [`deterministic`](#deterministic) | `bool` | `false` |
| [`profile_cache_ttl_secs`](#profile_cache_ttl_secs) | `u64` | `600` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`profiles`](#profiles) | `String[]` | `["modern_chrome_like", "modern_firefox_like", "compat_tls12", "legacy_minimal"]` | `` |
| [`strict_route`](#strict_route) | `bool` | `true` | `` |
| [`attempt_timeout_ms`](#attempt_timeout_ms) | `u64` | `5000` | `` |
| [`total_budget_ms`](#total_budget_ms) | `u64` | `15000` | `` |
| [`grease_enabled`](#grease_enabled) | `bool` | `false` | `` |
| [`deterministic`](#deterministic) | `bool` | `false` | `` |
| [`profile_cache_ttl_secs`](#profile_cache_ttl_secs) | `u64` | `600` | `` |
## profiles
- **Ограничения / валидация**: `String[]`. Пустой список возвращает значения по умолчанию; дубликаты удаляются с сохранением порядка.
@@ -2894,23 +2938,24 @@
# [access]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`users`](#users) | `Map<String, String>` | `{"default": "000…000"}` |
| [`user_ad_tags`](#user_ad_tags) | `Map<String, String>` | `{}` |
| [`user_max_tcp_conns`](#user_max_tcp_conns) | `Map<String, usize>` | `{}` |
| [`user_max_tcp_conns_global_each`](#user_max_tcp_conns_global_each) | `usize` | `0` |
| [`user_expirations`](#user_expirations) | `Map<String, DateTime<Utc>>` | `{}` |
| [`user_data_quota`](#user_data_quota) | `Map<String, u64>` | `{}` |
| [`user_max_unique_ips`](#user_max_unique_ips) | `Map<String, usize>` | `{}` |
| [`user_max_unique_ips_global_each`](#user_max_unique_ips_global_each) | `usize` | `0` |
| [`user_max_unique_ips_mode`](#user_max_unique_ips_mode) | `"active_window"`, `"time_window"`, or `"combined"` | `"active_window"` |
| [`user_max_unique_ips_window_secs`](#user_max_unique_ips_window_secs) | `u64` | `30` |
| [`replay_check_len`](#replay_check_len) | `usize` | `65536` |
| [`replay_window_secs`](#replay_window_secs) | `u64` | `120` |
| [`ignore_time_skew`](#ignore_time_skew) | `bool` | `false` |
| [`user_rate_limits`](#user_rate_limits) | `Map<String, RateLimitBps>` | `{}` |
| [`cidr_rate_limits`](#cidr_rate_limits) | `Map<IpNetwork, RateLimitBps>` | `{}` |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`users`](#users) | `Map<String, String>` | `{"default": "000…000"}` | `` |
| [`user_ad_tags`](#user_ad_tags) | `Map<String, String>` | `{}` | `` |
| [`user_max_tcp_conns`](#user_max_tcp_conns) | `Map<String, usize>` | `{}` | `` |
| [`user_max_tcp_conns_global_each`](#user_max_tcp_conns_global_each) | `usize` | `0` | `` |
| [`user_expirations`](#user_expirations) | `Map<String, DateTime<Utc>>` | `{}` | `` |
| [`user_data_quota`](#user_data_quota) | `Map<String, u64>` | `{}` | `` |
| [`user_max_unique_ips`](#user_max_unique_ips) | `Map<String, usize>` | `{}` | `` |
| [`user_max_unique_ips_global_each`](#user_max_unique_ips_global_each) | `usize` | `0` | `` |
| [`user_max_unique_ips_mode`](#user_max_unique_ips_mode) | `"active_window"`, `"time_window"`, or `"combined"` | `"active_window"` | `` |
| [`user_max_unique_ips_window_secs`](#user_max_unique_ips_window_secs) | `u64` | `30` | `` |
| [`user_source_deny`](#user_source_deny) | `Map<String, IpNetwork[]>` | `{}` | `` |
| [`replay_check_len`](#replay_check_len) | `usize` | `65536` | `` |
| [`replay_window_secs`](#replay_window_secs) | `u64` | `120` | `` |
| [`ignore_time_skew`](#ignore_time_skew) | `bool` | `false` | `` |
| [`user_rate_limits`](#user_rate_limits) | `Map<String, RateLimitBps>` | `{}` | `` |
| [`cidr_rate_limits`](#cidr_rate_limits) | `Map<IpNetwork, RateLimitBps>` | `{}` | `` |
## users
- **Ограничения / валидация**: Не должно быть пустым (должен существовать хотя бы один пользователь). Каждое значение должно состоять **ровно из 32 шестнадцатеричных символов**.
@@ -3010,6 +3055,20 @@
[access]
user_max_unique_ips_window_secs = 30
```
## user_source_deny
- **Ограничения / валидация**: Таблица `username -> IpNetwork[]`. Каждая сеть должна разбираться как CIDR, например `203.0.113.0/24` или `2001:db8::/32`.
- **Описание**: Deny-list исходных IP/CIDR для конкретного пользователя, применяемый **после успешной аутентификации** в TLS- и MTProto-handshake путях. Совпавший source IP отклоняется тем же fail-closed путём, что и невалидная аутентификация.
- **Пример**:
```toml
[access.user_source_deny]
alice = ["203.0.113.0/24", "2001:db8:abcd::/48"]
bob = ["198.51.100.42/32"]
```
- **Краткая проверка**:
- соединение пользователя `alice` с source `203.0.113.55` отклоняется, потому что совпадает с `203.0.113.0/24`;
- соединение пользователя `alice` с source `198.51.100.10` допускается этим набором правил, потому что совпадений нет.
## replay_check_len
- **Ограничения / валидация**: `usize`.
- **Описание**: Количество последних сообщений/запросов, которое система запоминает, чтобы не допустить их повторной отправки (replay).
@@ -3060,19 +3119,23 @@
# [[upstreams]]
| Ключ | Тип | По умолчанию |
| --- | ---- | ------- |
| [`type`](#type) | `"direct"`, `"socks4"`, `"socks5"`, or `"shadowsocks"` | — |
| [`weight`](#weight) | `u16` | `1` |
| [`enabled`](#enabled) | `bool` | `true` |
| [`scopes`](#scopes) | `String` | `""` |
| [`interface`](#interface) | `String` | — |
| [`bind_addresses`](#bind_addresses) | `String[]` | — |
| [`url`](#url) | `String` | — |
| [`address`](#address) | `String` | — |
| [`user_id`](#user_id) | `String` | — |
| [`username`](#username) | `String` | — |
| [`password`](#password) | `String` | — |
| Ключ | Тип | По умолчанию | Hot-Reload |
| --- | ---- | ------- | ---------- |
| [`type`](#type) | `"direct"`, `"socks4"`, `"socks5"`, or `"shadowsocks"` | — | `` |
| [`weight`](#weight) | `u16` | `1` | `` |
| [`enabled`](#enabled) | `bool` | `true` | `` |
| [`scopes`](#scopes) | `String` | `""` | `` |
| [`ipv4`](#ipv4-upstreams) | `bool` | — (auto) | `` |
| [`ipv6`](#ipv6-upstreams) | `bool` | — (auto) | `` |
| [`interface`](#interface) | `String` | — | `` |
| [`bind_addresses`](#bind_addresses) | `String[]` | — | `` |
| [`bindtodevice`](#bindtodevice) | `String` | — | `` |
| [`force_bind`](#force_bind) | `String` | — | `` |
| [`url`](#url) | `String` | — | `` |
| [`address`](#address) | `String` | — | `` |
| [`user_id`](#user_id) | `String` | — | `` |
| [`username`](#username) | `String` | — | `` |
| [`password`](#password) | `String` | — | `` |
## type
- **Ограничения / валидация**: Обязательный параметр.`"direct"`, `"socks4"`, `"socks5"`, `"shadowsocks"`.
@@ -3123,6 +3186,26 @@
address = "10.0.0.10:1080"
scopes = "me, fetch, dc2"
```
## ipv4 (upstreams)
- **Ограничения / валидация**: `bool` (необязательный параметр).
- **Описание**: Разрешает IPv4 DC-targets для этого upstream. Если не задан, Telemt определяет поддержку автоматически по runtime-состоянию connectivity.
- **Пример**:
```toml
[[upstreams]]
type = "direct"
ipv4 = true
```
## ipv6 (upstreams)
- **Ограничения / валидация**: `bool` (необязательный параметр).
- **Описание**: Разрешает IPv6 DC-targets для этого upstream. Если не задан, Telemt определяет поддержку автоматически по runtime-состоянию connectivity.
- **Пример**:
```toml
[[upstreams]]
type = "direct"
ipv6 = false
```
## interface
- **Ограничения / валидация**: `String` (необязательный параметр).
- для `"direct"`: может быть IP-адресом (используется как явный local bind) или именем сетевого интерфейса ОС (резолвится в IP во время выполнения; только Unix).
@@ -3153,6 +3236,26 @@
type = "direct"
bind_addresses = ["192.0.2.10", "192.0.2.11"]
```
## bindtodevice
- **Ограничения / валидация**: `String` (необязательный параметр). Применяется только для `type = "direct"` и только в Linux.
- **Описание**: Жёсткая привязка исходящих direct TCP-connect к интерфейсу через `SO_BINDTODEVICE`.
- **Пример**:
```toml
[[upstreams]]
type = "direct"
bindtodevice = "eth0"
```
## force_bind
- **Ограничения / валидация**: `String` (необязательный параметр). Алиас для `bindtodevice`.
- **Описание**: Обратно-совместимый алиас для жёсткой Linux-привязки к интерфейсу через `SO_BINDTODEVICE`.
- **Пример**:
```toml
[[upstreams]]
type = "direct"
force_bind = "eth0"
```
## url
- **Ограничения / валидация**: Применяется в случае, если `type = "shadowsocks"`.
- Должен быть действительный URL-адрес Shadowsocks, принятый `shadowsocks` контейнером.

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.
> 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.
### How many people can use one link

View File

@@ -157,7 +157,7 @@ https://github.com/telemt/telemt/discussions/167
## Как клиенты взаимодействуют с дата-центрами Telegram
При регистрации аккаунта Telegram он навсегда привязывается к одному из дата-центров (DC).
Telegram заранее определяет к какому DC привязать аккаунт исходя из региона, к которому относиться номер телефона.
Telegram заранее определяет к какому DC привязать аккаунт исходя из региона, к которому относится номер телефона.
Этот DC становится вашим **домашним**: именно там хранится весь контент, который вы загружаете (фото, видео, файлы, сообщения).
И именно на нем клиент авторизуется при каждом подключении.
@@ -170,7 +170,7 @@ Telegram заранее определяет к какому DC привязат
> Если ваш домашний DC — DC2, и DC2 лежит, вы **не сможете** достучаться и до DC5, даже если сам DC5 полностью исправен.
> У клиента просто нет валидной сессии, через которую можно было бы направить запрос.
По той же причине MTProxy достаточно иметь доступ к инфраструктуре Telegram в целом.
По той же причине MTProxy необходимо иметь доступ к инфраструктуре Telegram целиком, а не частично.
Cамому MTProxy всё равно, на каком DC живёт ваш аккаунт. Клиент cам договаривается о нужном DC через прокси уже после подключения.
## Что такое 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
**1. Отредактируйте `config.toml` в корневом каталоге репозитория (как минимум: порт, пользовательские секреты, tls_domain)**
**1. Создайте директорию `config/` и поместите в неё отрдеактированный `config.toml` (указав как минимум: порт, пользовательские секреты, tls_domain):**
```bash
mkdir config && mv config.toml config/
```
**2. Запустите контейнер:**
```bash
docker compose up -d --build
@@ -249,7 +252,7 @@ docker compose logs -f telemt
docker compose down
```
> [!NOTE]
> - В `docker-compose.yml` файл `./config.toml` монтируется в `/app/config.toml` (доступно только для чтения)
> - Директория `./config/` монтируется в `/etc/telemt/` (read-write), что позволяет API атомарно обновлять config.toml
> - По умолчанию публикуются порты 443:443, а контейнер запускается со сброшенными привилегиями (добавлена только `NET_BIND_SERVICE`)
> - Если вам действительно нужна сеть хоста (обычно это требуется только для некоторых конфигураций IPv6), раскомментируйте `network_mode: host`

View File

@@ -84,27 +84,29 @@ set_language() {
L_INFO_KEEP_CONF="Примечание: Конфигурация сохранена. Используйте 'purge' для очистки."
L_INFO_I_START="Начинается установка"
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_PORT="\nПожалуйста, укажите порт сервера\nНажмите Enter, чтобы оставить по умолчанию [%s]: "
L_WARN_NO_TTY="Интерактивный режим недоступен (нет TTY). Используется:"
L_I_STAGE_2=">>> Этап 2: Загрузка архива"
L_I_STAGE_3=">>> Этап 3: Загрузка архива"
L_ERR_TMP_DIR="Не удалось создать временную директорию"
L_ERR_TMP_INV="Временная директория недействительна"
L_INFO_FALLBACK="Сборка x86_64-v3 не найдена, откат к стандартной x86_64..."
L_ERR_DL_FAIL="Ошибка загрузки архива"
L_I_STAGE_3=">>> Этап 3: Распаковка архива"
L_I_STAGE_4=">>> Этап 4: Распаковка архива"
L_ERR_EXTRACT="Ошибка распаковки архива."
L_ERR_BIN_NOT_FOUND="Бинарный файл не найден в архиве"
L_I_STAGE_4=">>> Этап 4: Настройка окружения (Юзер, Группа, Папки)"
L_I_STAGE_5=">>> Этап 5: Установка бинарного файла"
L_I_STAGE_6=">>> Этап 6: Генерация/Обновление конфигурации"
L_I_STAGE_7=">>> Этап 7: Установка и запуск службы"
L_I_STAGE_5=">>> Этап 5: Настройка окружения (Юзер, Группа, Папки)"
L_I_STAGE_6=">>> Этап 6: Установка бинарного файла"
L_I_STAGE_7=">>> Этап 7: Генерация/Обновление конфигурации"
L_I_STAGE_8=">>> Этап 8: Установка и запуск службы"
L_OUT_WARN_H="УСТАНОВКА ЗАВЕРШЕНА С ПРЕДУПРЕЖДЕНИЯМИ"
L_OUT_WARN_D="Служба установлена, но не запустилась.\nПожалуйста, проверьте логи.\n"
L_OUT_SUCC_H="УСТАНОВКА УСПЕШНО ЗАВЕРШЕНА"
L_OUT_UNINST_H="УДАЛЕНИЕ ЗАВЕРШЕНО"
L_OUT_LINK="Ваша ссылка для подключения к Telegram Proxy:\n"
L_ERR_INCORR_ROOT_LOGIN="Используйте 'su -' или 'sudo -i' для входа под пользователем root"
L_OUT_LOGS="Чтобы посмотреть логи (в случае проблем), используйте команду:"
;;
*)
L_ERR_DOMAIN_REQ="requires a domain argument."
@@ -159,27 +161,29 @@ set_language() {
L_INFO_KEEP_CONF="Note: Configuration kept. Run with 'purge' to remove completely."
L_INFO_I_START="Starting installation of"
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_PORT="\nPlease specify the Server Port\nPress Enter to keep default [%s]: "
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_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_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_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: Installing binary"
L_I_STAGE_6=">>> Stage 6: Generating/Updating configuration"
L_I_STAGE_7=">>> Stage 7: Installing and starting service"
L_I_STAGE_5=">>> Stage 5: Setting up environment (User, Group, Directories)"
L_I_STAGE_6=">>> Stage 6: Installing binary"
L_I_STAGE_7=">>> Stage 7: Generating/Updating configuration"
L_I_STAGE_8=">>> Stage 8: Installing and starting service"
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_SUCC_H="INSTALLATION SUCCESS"
L_OUT_UNINST_H="UNINSTALLATION COMPLETE"
L_OUT_LINK="Your Telegram Proxy connection link:\n"
L_ERR_INCORR_ROOT_LOGIN="Use 'su -' or 'sudo -i' to login under root"
L_OUT_LOGS="To view logs (in case of issues), use the following command:"
;;
esac
}
@@ -267,7 +271,10 @@ say() {
if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
printf '\n'
else
printf '[INFO] %s\n' "$*"
case "$*" in
\[*\]*) printf '%s\n' "$*" ;;
*) printf '[INFO] %s\n' "$*" ;;
esac
fi
}
die() { printf '[ERROR] %s\n' "$*" >&2; exit 1; }
@@ -392,7 +399,7 @@ verify_common() {
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
if [ "$(id -u)" -ne 0 ]; then
if [ "${USER:-}" != "root" ] && [ "${LOGNAME:-}" != "root" ]; then
die "$L_ERR_INCORR_ROOT_LOGIN"
fi
else
@@ -525,9 +532,9 @@ setup_dirs() {
stop_service() {
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
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
fi
}
@@ -539,7 +546,7 @@ install_binary() {
fi
$SUDO mkdir -p "$INSTALL_DIR" || die "$L_ERR_MKDIR"
$SUDO rm -f "$bin_dst" 2>/dev/null || true
if command -v install >/dev/null 2>&1; then
@@ -609,33 +616,33 @@ install_config() {
tmp_conf="${TEMP_DIR}/config.tmp"
$SUDO cat "$CONFIG_FILE" > "$tmp_conf"
escaped_domain="$(printf '%s\n' "$TLS_DOMAIN" | tr -d '[:cntrl:]' | sed 's/\\/\\\\/g; s/"/\\"/g')"
awk -v port="$SERVER_PORT" -v secret="$USER_SECRET" -v domain="$escaped_domain" -v ad_tag="$AD_TAG" \
-v flag_p="$PORT_PROVIDED" -v flag_s="$SECRET_PROVIDED" -v flag_d="$DOMAIN_PROVIDED" -v flag_a="$AD_TAG_PROVIDED" '
BEGIN { ad_tag_handled = 0 }
flag_p == "1" && /^[ \t]*port[ \t]*=/ { print "port = " port; next }
flag_s == "1" && /^[ \t]*hello[ \t]*=/ { print "hello = \"" secret "\""; next }
flag_d == "1" && /^[ \t]*tls_domain[ \t]*=/ { print "tls_domain = \"" domain "\""; next }
flag_a == "1" && /^[ \t]*ad_tag[ \t]*=/ {
if (!ad_tag_handled) {
print "ad_tag = \"" ad_tag "\"";
ad_tag_handled = 1;
}
next
flag_a == "1" && /^[ \t]*ad_tag[ \t]*=/ {
if (!ad_tag_handled) {
print "ad_tag = \"" ad_tag "\"";
ad_tag_handled = 1;
}
next
}
flag_a == "1" && /^\[general\]/ {
print;
if (!ad_tag_handled) {
print "ad_tag = \"" ad_tag "\"";
ad_tag_handled = 1;
}
next
flag_a == "1" && /^\[general\]/ {
print;
if (!ad_tag_handled) {
print "ad_tag = \"" ad_tag "\"";
ad_tag_handled = 1;
}
next
}
{ print }
' "$tmp_conf" > "${tmp_conf}.new" && mv "${tmp_conf}.new" "$tmp_conf"
@@ -785,11 +792,11 @@ uninstall() {
say "$L_U_STAGE_5"
$SUDO rm -rf "$CONFIG_DIR" "$WORK_DIR"
$SUDO rm -f "$CONFIG_FILE"
if check_os_entity passwd telemt; then
$SUDO userdel telemt 2>/dev/null || $SUDO deluser telemt 2>/dev/null || true
fi
if check_os_entity group telemt; then
$SUDO groupdel telemt 2>/dev/null || $SUDO delgroup telemt 2>/dev/null || true
fi
@@ -830,10 +837,36 @@ case "$ACTION" in
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
say "$L_I_STAGE_1_5"
if [ -t 0 ] || [ -c /dev/tty ]; then
printf "$L_I_PROMPT_DOM" "$TLS_DOMAIN"
read -r input_domain </dev/tty || input_domain=""
@@ -846,6 +879,8 @@ case "$ACTION" in
DOMAIN_PROVIDED=1
fi
check_port_availability
if [ "$TARGET_VERSION" != "latest" ]; then
TARGET_VERSION="${TARGET_VERSION#v}"
fi
@@ -859,7 +894,7 @@ case "$ACTION" in
DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}"
fi
say "$L_I_STAGE_2"
say "$L_I_STAGE_3"
TEMP_DIR="$(mktemp -d)" || die "$L_ERR_TMP_DIR"
if [ -z "$TEMP_DIR" ] || [ ! -d "$TEMP_DIR" ]; then
die "$L_ERR_TMP_INV"
@@ -881,7 +916,7 @@ case "$ACTION" in
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
die "$L_ERR_EXTRACT"
fi
@@ -889,16 +924,16 @@ case "$ACTION" in
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"
say "$L_I_STAGE_4"
say "$L_I_STAGE_5"
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}"
say "$L_I_STAGE_6"
say "$L_I_STAGE_7"
install_config
say "$L_I_STAGE_7"
say "$L_I_STAGE_8"
install_service
if [ "${SERVICE_START_FAILED:-0}" -eq 1 ]; then
@@ -916,7 +951,7 @@ case "$ACTION" in
if command -v curl >/dev/null 2>&1; then SERVER_IP="$(curl -s4 -m 3 ifconfig.me 2>/dev/null || curl -s4 -m 3 api.ipify.org 2>/dev/null || true)"
elif command -v wget >/dev/null 2>&1; then SERVER_IP="$(wget -qO- -T 3 ifconfig.me 2>/dev/null || wget -qO- -T 3 api.ipify.org 2>/dev/null || true)"; fi
[ -z "$SERVER_IP" ] && SERVER_IP="<YOUR_SERVER_IP>"
if command -v xxd >/dev/null 2>&1; then HEX_DOMAIN="$(printf '%s' "$TLS_DOMAIN" | xxd -p | tr -d '\n')"
elif command -v hexdump >/dev/null 2>&1; then HEX_DOMAIN="$(printf '%s' "$TLS_DOMAIN" | hexdump -v -e '/1 "%02x"')"
elif command -v od >/dev/null 2>&1; then HEX_DOMAIN="$(printf '%s' "$TLS_DOMAIN" | od -A n -t x1 | tr -d ' \n')"
@@ -927,6 +962,15 @@ case "$ACTION" in
printf '%b\n' "$L_OUT_LINK"
printf ' tg://proxy?server=%s&port=%s&secret=%s\n\n' "$SERVER_IP" "$SERVER_PORT" "$CLIENT_SECRET"
svc="$(get_svc_mgr)"
if [ "$svc" = "systemd" ]; then
printf '%s\n' "$L_OUT_LOGS"
printf ' sudo journalctl -u %s -f\n\n' "$SERVICE_NAME"
elif [ "$svc" = "openrc" ]; then
printf '%s\n' "$L_OUT_LOGS"
printf ' sudo tail -f /var/log/messages /var/log/syslog 2>/dev/null | grep -i %s\n\n' "$SERVICE_NAME"
fi
printf '====================================================================\n'
;;
esac

View File

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

View File

@@ -41,7 +41,9 @@ mod runtime_watch;
mod runtime_zero;
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 http_utils::{error_response, read_json, read_optional_json, success_response};
use model::{
@@ -75,6 +77,10 @@ use users::{
const API_MAX_CONTROL_CONNECTIONS: usize = 1024;
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 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) 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(
listen: SocketAddr,
stats: Arc<Stats>,
@@ -435,22 +492,22 @@ async fn handle(
let data = build_dcs_data(shared.as_ref(), api_cfg).await;
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 data = build_runtime_me_pool_state_data(shared.as_ref()).await;
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 data = build_runtime_me_quality_data(shared.as_ref()).await;
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 data = build_runtime_upstream_quality_data(shared.as_ref()).await;
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 data = build_runtime_nat_stun_data(shared.as_ref()).await;
Ok(success_response(StatusCode::OK, data, revision))
@@ -506,7 +563,7 @@ async fn handle(
.await;
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 disk_cfg = load_config_from_disk(&shared.config_path).await?;
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(
&shared.quota_state_path,
shared.stats.as_ref(),
@@ -761,16 +828,18 @@ async fn handle(
if method == Method::POST {
return Ok(error_response(
request_id,
ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"),
ApiFailure::method_not_allowed(ALLOW_GET_PATCH_DELETE),
));
}
return Ok(error_response(
request_id,
ApiFailure::new(
StatusCode::METHOD_NOT_ALLOWED,
"method_not_allowed",
"Unsupported HTTP method for this route",
),
ApiFailure::method_not_allowed(ALLOW_GET_PATCH_DELETE),
));
}
if let Some(allow) = allowed_methods_for_path(normalized_path) {
return Ok(error_response(
request_id,
ApiFailure::method_not_allowed(allow),
));
}
debug!(

View File

@@ -15,6 +15,7 @@ pub(super) struct ApiFailure {
pub(super) status: StatusCode,
pub(super) code: &'static str,
pub(super) message: String,
pub(super) allow: Option<&'static str>,
}
impl ApiFailure {
@@ -23,6 +24,7 @@ impl ApiFailure {
status,
code,
message: message.into(),
allow: None,
}
}
@@ -33,6 +35,15 @@ impl ApiFailure {
pub(super) fn bad_request(message: impl Into<String>) -> Self {
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)]

View File

@@ -1,6 +1,7 @@
#![allow(clippy::items_after_test_module)]
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::watch;
@@ -18,8 +19,27 @@ use crate::transport::middle_proxy::{
const MAESTRO_COLOR: &str = "\x1b[92m";
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>) {
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(
@@ -274,11 +294,24 @@ mod tests {
use std::path::{Path, PathBuf};
use super::{
expected_handshake_close_description, is_expected_handshake_eof, peer_close_description,
resolve_runtime_base_dir, resolve_runtime_config_path,
expected_handshake_close_description, format_maestro_line, is_expected_handshake_eof,
peer_close_description, resolve_runtime_base_dir, resolve_runtime_config_path,
};
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]
fn resolve_runtime_config_path_anchors_relative_to_startup_cwd() {
let nonce = std::time::SystemTime::now()

View File

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

View File

@@ -1385,6 +1385,7 @@ fn emulated_server_hello_never_places_alpn_in_server_hello_extensions() {
false,
true,
ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng,
Some(b"h2".to_vec()),
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> {
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();
body.extend_from_slice(&TLS_VERSION);
body.extend_from_slice(&[0u8; 32]);
body.push(0);
body.extend_from_slice(&2u16.to_be_bytes());
body.extend_from_slice(&[0x13, 0x01]);
body.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
for suite in cipher_suites {
body.extend_from_slice(suite);
}
body.push(1);
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());
}
#[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]
fn extract_sni_rejects_zero_length_host_name() {
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]
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 client_digest = [0x55u8; TLS_DIGEST_LEN];
let session_id = vec![0xAB; 32];
@@ -2206,8 +2263,8 @@ fn server_hello_application_data_contains_alpn_marker_when_selected() {
assert!(
app_payload
.windows(expected.len())
.any(|window| window == expected),
"first application payload must carry ALPN marker for selected protocol"
.all(|window| window != expected),
"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]
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 client_digest = [0x58u8; TLS_DIGEST_LEN];
let session_id = vec![0xA5; 32];
let rng = crate::crypto::SecureRandom::new();
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(
secret,
&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);
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]

View File

@@ -105,6 +105,8 @@ mod extension_type {
/// TLS Cipher Suites
mod cipher_suite {
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
@@ -241,6 +243,13 @@ impl ServerHelloBuilder {
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)
fn build_message(&self) -> Vec<u8> {
let Ok(session_id_len) = u8::try_from(self.session_id.len()) else {
@@ -520,6 +529,33 @@ pub fn build_server_hello(
rng: &SecureRandom,
alpn: Option<Vec<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> {
const MIN_APP_DATA: usize = 64;
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
@@ -528,6 +564,7 @@ pub fn build_server_hello(
// Build ServerHello
let server_hello = ServerHelloBuilder::new(session_id.to_vec())
.with_cipher_suite(selected_cipher_suite)
.with_x25519_key(&x25519_key)
.with_tls13_version()
.build_record();
@@ -538,28 +575,14 @@ pub fn build_server_hello(
TLS_VERSION[0],
TLS_VERSION[1],
0x00,
0x01, // length = 1
0x01, // CCS byte
0x01,
0x01,
];
// 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);
if let Some(proto) = 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);
}
}
let _ = alpn;
if 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 {
@@ -580,7 +603,7 @@ pub fn build_server_hello(
let ticket_count = new_session_tickets.min(4);
if ticket_count > 0 {
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);
record.push(TLS_RECORD_APPLICATION);
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
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
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 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(
&validated_secret,
&validation_digest,
@@ -1512,17 +1519,20 @@ where
use_full_cert_payload,
config.censorship.serverhello_compact,
client_tls_version,
selected_cipher_suite,
rng,
selected_alpn.clone(),
config.censorship.tls_new_session_tickets,
)
} 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,
&validation_digest,
validation_session_id_slice,
config.censorship.fake_cert_len,
rng,
selected_cipher_suite,
selected_alpn.clone(),
config.censorship.tls_new_session_tickets,
)

View File

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

View File

@@ -437,8 +437,9 @@ where
W: AsyncWrite + Unpin + Send + 'static,
{
tokio::select! {
result = client_writer.write_all(bytes) => result.map_err(ProxyError::Io),
biased;
_ = cancel.cancelled() => Err(ProxyError::MiddleClientWriterCancelled),
result = client_writer.write_all(bytes) => result.map_err(ProxyError::Io),
}
}
@@ -450,7 +451,8 @@ where
W: AsyncWrite + Unpin + Send + 'static,
{
tokio::select! {
result = client_writer.flush() => result.map_err(ProxyError::Io),
biased;
_ = cancel.cancelled() => Err(ProxyError::MiddleClientWriterCancelled),
result = client_writer.flush() => result.map_err(ProxyError::Io),
}
}

View File

@@ -1,4 +1,5 @@
use super::*;
use dashmap::DashMap;
mod read;
@@ -10,10 +11,10 @@ pub(crate) use self::read::{
#[derive(Default)]
pub(crate) struct RelayIdleCandidateRegistry {
pub(in crate::proxy::middle_relay) by_conn_id: HashMap<u64, RelayIdleCandidateMeta>,
pub(in crate::proxy::middle_relay) ordered: BTreeSet<(u64, u64)>,
pressure_event_seq: u64,
pressure_consumed_seq: u64,
pub(in crate::proxy::middle_relay) by_conn_id: DashMap<u64, RelayIdleCandidateMeta>,
pub(in crate::proxy::middle_relay) ordered: parking_lot::Mutex<BTreeSet<(u64, u64)>>,
pressure_event_seq: AtomicU64,
pressure_consumed_seq: AtomicU64,
}
/// 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(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 {
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;
}
@@ -52,24 +38,38 @@ pub(super) fn mark_relay_idle_candidate_in(shared: &ProxySharedState, conn_id: u
.saturating_add(1);
let meta = RelayIdleCandidateMeta {
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);
guard.ordered.insert((meta.mark_order_seq, conn_id));
true
match registry.by_conn_id.entry(conn_id) {
dashmap::mapref::entry::Entry::Occupied(_) => false,
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) {
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) {
guard.ordered.remove(&(meta.mark_order_seq, conn_id));
if let Some((_, meta)) = registry.by_conn_id.remove(&conn_id) {
registry
.ordered
.lock()
.remove(&(meta.mark_order_seq, conn_id));
}
}
pub(super) fn note_relay_pressure_event_in(shared: &ProxySharedState) {
let mut guard = relay_idle_candidate_registry_lock_in(shared);
guard.pressure_event_seq = guard.pressure_event_seq.wrapping_add(1);
shared
.middle_relay
.relay_idle_registry
.pressure_event_seq
.fetch_add(1, Ordering::Relaxed);
}
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 {
let guard = relay_idle_candidate_registry_lock_in(shared);
guard.pressure_event_seq
shared
.middle_relay
.relay_idle_registry
.pressure_event_seq
.load(Ordering::Relaxed)
}
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,
stats: &Stats,
) -> 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 {
return false;
}
*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;
}
if guard.ordered.is_empty() {
guard.pressure_consumed_seq = latest_pressure_seq;
return false;
}
let oldest = guard
.ordered
.iter()
.next()
.map(|(_, candidate_conn_id)| *candidate_conn_id);
let oldest = {
let mut ordered = registry.ordered.lock();
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 _ = registry.pressure_consumed_seq.compare_exchange(
consumed_pressure_seq,
latest_pressure_seq,
Ordering::Relaxed,
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) {
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;
};
@@ -121,10 +143,27 @@ pub(super) fn maybe_evict_idle_candidate_on_pressure_in(
return false;
}
if let Some(meta) = guard.by_conn_id.remove(&conn_id) {
guard.ordered.remove(&(meta.mark_order_seq, conn_id));
// Claim the global pressure budget before removal; otherwise racing sessions
// 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();
true
}
@@ -220,72 +259,32 @@ pub(crate) fn mark_relay_idle_candidate_for_testing(
shared: &ProxySharedState,
conn_id: u64,
) -> bool {
let registry = &shared.middle_relay.relay_idle_registry;
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
mark_relay_idle_candidate_in(shared, conn_id)
}
#[cfg(test)]
pub(crate) fn oldest_relay_idle_candidate_for_testing(shared: &ProxySharedState) -> Option<u64> {
let registry = &shared.middle_relay.relay_idle_registry;
let guard = match registry.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let mut guard = poisoned.into_inner();
*guard = RelayIdleCandidateRegistry::default();
registry.clear_poison();
guard
}
};
guard.ordered.iter().next().map(|(_, conn_id)| *conn_id)
registry
.ordered
.lock()
.iter()
.next()
.map(|(_, conn_id)| *conn_id)
}
#[cfg(test)]
pub(crate) fn clear_relay_idle_candidate_for_testing(shared: &ProxySharedState, conn_id: u64) {
let registry = &shared.middle_relay.relay_idle_registry;
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));
}
clear_relay_idle_candidate_in(shared, conn_id);
}
#[cfg(test)]
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() {
*guard = RelayIdleCandidateRegistry::default();
}
let registry = &shared.middle_relay.relay_idle_registry;
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
.middle_relay
.relay_idle_mark_seq
@@ -327,15 +326,10 @@ pub(crate) fn set_relay_pressure_state_for_testing(
pressure_consumed_seq: u64,
) {
let registry = &shared.middle_relay.relay_idle_registry;
let mut guard = match registry.lock() {
Ok(guard) => guard,
Err(poisoned) => {
let mut guard = poisoned.into_inner();
*guard = RelayIdleCandidateRegistry::default();
registry.clear_poison();
guard
}
};
guard.pressure_event_seq = pressure_event_seq;
guard.pressure_consumed_seq = pressure_consumed_seq;
registry
.pressure_event_seq
.store(pressure_event_seq, Ordering::Relaxed);
registry
.pressure_consumed_seq
.store(pressure_consumed_seq, Ordering::Relaxed);
}

View File

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

View File

@@ -59,7 +59,7 @@ pub(crate) struct MiddleRelaySharedState {
pub(crate) desync_hasher: RandomState,
pub(crate) desync_full_cache_last_emit_at: Mutex<Option<Instant>>,
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,
}
@@ -97,7 +97,7 @@ impl ProxySharedState {
desync_hasher: RandomState::new(),
desync_full_cache_last_emit_at: Mutex::new(None),
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),
},
traffic_limiter: TrafficLimiter::new(),

View File

@@ -1,33 +1,21 @@
use super::*;
use std::panic::{AssertUnwindSafe, catch_unwind};
#[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();
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
let _ = catch_unwind(AssertUnwindSafe(|| {
let mut guard = shared
.middle_relay
.relay_idle_registry
.lock()
.expect("registry lock must be acquired before poison");
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");
}));
shared
.middle_relay
.relay_idle_registry
.ordered
.lock()
.insert((0, 999));
// Helper lock must recover from poison, reset stale state, and continue.
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 42));
assert_eq!(
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
Some(42)
Some(999)
);
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());
assert!(
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());
}
#[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();
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
let _ = catch_unwind(AssertUnwindSafe(|| {
let _guard = shared
.middle_relay
.relay_idle_registry
.lock()
.expect("registry lock must be acquired before poison");
panic!("intentional poison while lock held");
}));
shared.middle_relay.relay_idle_registry.by_conn_id.insert(
999,
RelayIdleCandidateMeta {
mark_order_seq: 1,
mark_pressure_seq: 0,
},
);
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());

View File

@@ -8,12 +8,17 @@ use crate::protocol::constants::{
use crate::protocol::tls::{
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;
const MIN_APP_DATA: usize = 64;
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
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> {
sizes
@@ -185,6 +190,74 @@ fn hash_compact_cert_info_payload(cert_payload: Vec<u8>) -> Option<Vec<u8>> {
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.
pub fn build_emulated_server_hello(
secret: &[u8],
@@ -194,39 +267,28 @@ pub fn build_emulated_server_hello(
use_full_cert_payload: bool,
serverhello_compact: bool,
client_tls_version: ClientHelloTlsVersion,
selected_cipher_suite: [u8; 2],
rng: &SecureRandom,
alpn: Option<Vec<u8>>,
new_session_tickets: u8,
) -> Vec<u8> {
// --- ServerHello ---
let mut extensions = Vec::new();
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 = build_profiled_server_hello_extensions(cached, rng);
let extensions_len = extensions.len() as u16;
let body_len = 2 + // version
32 + // random
1 + session_id.len() + // session id
2 + // cipher
1 + // compression
2 + extensions.len(); // extensions
let body_len = 2 + 32 + 1 + session_id.len() + 2 + 1 + 2 + extensions.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();
message.extend_from_slice(&len_bytes[1..4]);
message.extend_from_slice(&cached.server_hello_template.version); // 0x0303
message.extend_from_slice(&[0u8; 32]); // random placeholder
message.extend_from_slice(&cached.server_hello_template.version);
message.extend_from_slice(&[0u8; 32]);
message.push(session_id.len() as u8);
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]
} else {
cached.server_hello_template.cipher_suite
@@ -303,21 +365,10 @@ pub fn build_emulated_server_hello(
}
let mut app_data = Vec::new();
let alpn_marker = alpn
.as_ref()
.filter(|p| !p.is_empty() && p.len() <= u8::MAX as usize)
.map(|proto| {
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() {
// ALPN selection is encrypted inside EncryptedExtensions in real TLS 1.3.
// Keeping the FakeTLS record body opaque avoids a stable plaintext marker.
let _ = alpn;
for size in sizes {
let mut rec = Vec::with_capacity(5 + size);
rec.push(TLS_RECORD_APPLICATION);
rec.extend_from_slice(&TLS_VERSION);
@@ -334,31 +385,18 @@ pub fn build_emulated_server_hello(
if body_len > copy_len {
rec.extend_from_slice(&rng.bytes(body_len - copy_len));
}
rec.push(0x16); // inner content type marker (handshake)
rec.extend_from_slice(&rng.bytes(16)); // AEAD-like tag
rec.push(0x16);
rec.extend_from_slice(&rng.bytes(16));
} else {
rec.extend_from_slice(&rng.bytes(size));
}
} else if size > 17 {
let body_len = size - 17;
let mut body = Vec::with_capacity(body_len);
if idx == 0
&& 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));
}
body.extend_from_slice(&rng.bytes(body_len));
rec.extend_from_slice(&body);
rec.push(0x16); // inner content type marker (handshake)
rec.extend_from_slice(&rng.bytes(16)); // AEAD-like tag
rec.push(0x16);
rec.extend_from_slice(&rng.bytes(16));
} else {
rec.extend_from_slice(&rng.bytes(size));
}
@@ -408,7 +446,8 @@ mod tests {
use std::time::SystemTime;
use crate::tls_front::types::{
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsProfileSource,
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsExtension,
TlsProfileSource,
};
use super::{
@@ -432,6 +471,38 @@ mod tests {
&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 {
CachedTlsData {
server_hello_template: ParsedServerHello {
@@ -468,6 +539,7 @@ mod tests {
true,
true,
ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng,
None,
0,
@@ -484,6 +556,65 @@ mod tests {
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]
fn test_build_emulated_server_hello_random_fallback_when_no_cert_payload() {
let cached = make_cached(None);
@@ -496,6 +627,7 @@ mod tests {
true,
true,
ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng,
None,
0,
@@ -530,6 +662,7 @@ mod tests {
false,
true,
ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng,
None,
0,
@@ -570,6 +703,7 @@ mod tests {
true,
true,
ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng,
None,
0,
@@ -583,7 +717,7 @@ mod tests {
}
#[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);
cached.cert_info = Some(crate::tls_front::types::ParsedCertificateInfo {
not_after_unix: Some(1_900_000_000),
@@ -602,6 +736,7 @@ mod tests {
false,
false,
ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng,
Some(b"h2".to_vec()),
0,
@@ -610,8 +745,8 @@ mod tests {
let payload = first_app_data_payload(&response);
let expected_alpn_marker = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
assert!(
payload.starts_with(&expected_alpn_marker),
"when compact mode is disabled and no full cert payload exists, the random/alpn path must be used"
!payload.starts_with(&expected_alpn_marker),
"random fallback payload must not expose plaintext ALPN marker bytes"
);
}
@@ -633,6 +768,7 @@ mod tests {
false,
true,
ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng,
None,
0,

View File

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

View File

@@ -58,6 +58,7 @@ fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
true,
true,
ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng,
Some(oversized_alpn),
0,
@@ -84,7 +85,7 @@ fn emulated_server_hello_ignores_oversized_alpn_when_marker_would_not_fit() {
}
#[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 rng = SecureRandom::new();
@@ -96,6 +97,7 @@ fn emulated_server_hello_embeds_full_alpn_marker_when_body_can_fit() {
true,
true,
ClientHelloTlsVersion::Tls13,
[0x13, 0x01],
&rng,
Some(b"h2".to_vec()),
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 expected = [0x00u8, 0x10, 0x00, 0x05, 0x00, 0x03, 0x02, b'h', b'2'];
assert!(
payload.starts_with(&expected),
"when body has enough capacity, emulated first application record must include full ALPN marker"
!payload.starts_with(&expected),
"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,
ClientHelloTlsVersion::Tls12,
[0x13, 0x01],
&rng,
Some(b"h2".to_vec()),
0,

View File

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

View File

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

View File

@@ -77,26 +77,24 @@ struct HotBindingTable {
struct BindingState {
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 {
writers: HashMap<u64, mpsc::Sender<WriterCommand>>,
writer_for_conn: HashMap<u64, u64>,
conns_for_writer: HashMap<u64, HashSet<u64>>,
meta: HashMap<u64, ConnMeta>,
last_meta_for_writer: HashMap<u64, ConnMeta>,
writer_idle_since_epoch_secs: HashMap<u64, u64>,
}
impl BindingInner {
fn new() -> Self {
Self {
writers: HashMap::new(),
writer_for_conn: HashMap::new(),
conns_for_writer: 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 {
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),
route_channel_capacity,

View File

@@ -13,13 +13,63 @@ use super::{
};
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>) {
let mut binding = self.binding.inner.lock().await;
binding.writers.insert(writer_id, tx.clone());
binding
.conns_for_writer
.entry(writer_id)
.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);
}
@@ -29,19 +79,18 @@ impl ConnRegistry {
self.routing.byte_budget.remove(&id);
self.hot_binding.map.remove(&id);
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) {
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.is_empty()
set.len()
} else {
false
0
};
if became_empty {
binding
.writer_idle_since_epoch_secs
.insert(writer_id, Self::now_epoch_secs());
}
self.set_writer_bound_count(writer_id, next_count);
return Some(writer_id);
}
None
@@ -248,7 +297,7 @@ impl ConnRegistry {
if !self.routing.map.contains_key(&conn_id) {
return false;
}
if !binding.writers.contains_key(&writer_id) {
if !self.writers.map.contains_key(&writer_id) {
return false;
}
@@ -256,28 +305,32 @@ impl ConnRegistry {
if let Some(previous_writer_id) = previous_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) {
set.remove(&conn_id);
set.is_empty()
set.len()
} else {
false
0
};
if became_empty {
binding
.writer_idle_since_epoch_secs
.insert(previous_writer_id, Self::now_epoch_secs());
}
self.set_writer_bound_count(previous_writer_id, next_count);
}
binding.meta.insert(conn_id, meta.clone());
binding.last_meta_for_writer.insert(writer_id, meta.clone());
binding.writer_idle_since_epoch_secs.remove(&writer_id);
binding
.conns_for_writer
.entry(writer_id)
.or_insert_with(HashSet::new)
.insert(conn_id);
if let Some(previous_meta) = binding.meta.insert(conn_id, meta.clone()) {
self.adjust_active_target_dc(previous_meta.target_dc, -1);
}
self.adjust_active_target_dc(meta.target_dc, 1);
self.binding
.last_meta_for_writer
.insert(writer_id, meta.clone());
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
.map
.insert(conn_id, HotConnBinding { writer_id, meta });
@@ -290,27 +343,38 @@ impl ConnRegistry {
.conns_for_writer
.entry(writer_id)
.or_insert_with(HashSet::new);
binding
.writer_idle_since_epoch_secs
.entry(writer_id)
.or_insert(Self::now_epoch_secs());
let count = binding
.conns_for_writer
.get(&writer_id)
.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> {
let binding = self.binding.inner.lock().await;
binding.last_meta_for_writer.get(&writer_id).cloned()
self.binding
.last_meta_for_writer
.get(&writer_id)
.map(|entry| entry.value().clone())
}
pub async fn writer_idle_since_snapshot(&self) -> HashMap<u64, u64> {
let binding = self.binding.inner.lock().await;
binding.writer_idle_since_epoch_secs.clone()
self.binding
.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> {
let binding = self.binding.inner.lock().await;
let mut out = HashMap::<u64, u64>::with_capacity(writer_ids.len());
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);
}
}
@@ -320,25 +384,19 @@ impl ConnRegistry {
pub(in crate::transport::middle_proxy) async fn writer_activity_snapshot(
&self,
) -> 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 {
bound_clients_by_writer,
active_sessions_by_target_dc,
bound_clients_by_writer: self
.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> {
let mut binding = self.binding.inner.lock().await;
binding.writers.remove(&writer_id);
self.writers.map.remove(&writer_id);
binding.last_meta_for_writer.remove(&writer_id);
binding.writer_idle_since_epoch_secs.remove(&writer_id);
self.binding.last_meta_for_writer.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
.conns_for_writer
.remove(&writer_id)
@@ -410,6 +468,10 @@ impl ConnRegistry {
continue;
}
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
.hot_binding
.map
@@ -419,11 +481,8 @@ impl ConnRegistry {
if remove_hot {
self.hot_binding.map.remove(&conn_id);
}
if let Some(m) = binding.meta.get(&conn_id) {
out.push(BoundConn {
conn_id,
meta: m.clone(),
});
if let Some(m) = meta {
out.push(BoundConn { conn_id, meta: m });
}
}
out
@@ -438,11 +497,10 @@ impl ConnRegistry {
}
pub async fn is_writer_empty(&self, writer_id: u64) -> bool {
let binding = self.binding.inner.lock().await;
binding
.conns_for_writer
self.binding
.bound_clients_by_writer
.get(&writer_id)
.map(|s| s.is_empty())
.map(|count| *count.value() == 0)
.unwrap_or(true)
}
@@ -457,21 +515,20 @@ impl ConnRegistry {
return false;
}
binding.writers.remove(&writer_id);
self.writers.map.remove(&writer_id);
binding.last_meta_for_writer.remove(&writer_id);
binding.writer_idle_since_epoch_secs.remove(&writer_id);
self.binding.last_meta_for_writer.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);
true
}
#[allow(dead_code)]
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());
for writer_id in writer_ids {
if let Some(conns) = binding.conns_for_writer.get(writer_id)
&& !conns.is_empty()
if let Some(count) = self.binding.bound_clients_by_writer.get(writer_id)
&& *count.value() > 0
{
out.insert(*writer_id);
}

View File

@@ -6,6 +6,7 @@ use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
use tracing::{debug, warn};
@@ -15,7 +16,6 @@ use super::registry::ConnMeta;
use super::wire::build_proxy_req_payload;
use crate::config::{MeRouteNoWriterMode, MeWriterPickMode};
use crate::error::{ProxyError, Result};
use crate::network::IpFamily;
use crate::stream::PooledBuffer;
use rand::seq::SliceRandom;
@@ -34,6 +34,11 @@ mod close;
mod recovery;
mod selection;
enum WriterCommandReserveError {
Closed,
TimedOut,
}
fn proxy_tag_array(tag: Option<&[u8]>) -> Option<[u8; 16]> {
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 {
/// 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(
@@ -105,9 +125,25 @@ impl MePool {
return Ok(());
}
Err(TrySendError::Full(cmd)) => {
if current.tx.send(cmd).await.is_ok() {
self.note_hybrid_route_success();
return Ok(());
match reserve_writer_command_slot(
&current.tx,
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");
self.remove_writer_and_close_clients(current.writer_id)
@@ -124,9 +160,8 @@ impl MePool {
}
let mut writers_snapshot = {
let ws = self.writers.read().await;
let ws = self.writers.snapshot();
if ws.is_empty() {
drop(ws);
match no_writer_mode {
MeRouteNoWriterMode::AsyncRecoveryFailfast => {
let deadline = *no_writer_deadline.get_or_insert_with(|| {
@@ -154,38 +189,28 @@ impl MePool {
for _ in
0..self.route_runtime.me_route_inline_recovery_attempts.max(1)
{
for family in self.family_order() {
let map = match family {
IpFamily::V4 => self.proxy_map_v4.read().await.clone(),
IpFamily::V6 => self.proxy_map_v6.read().await.clone(),
};
for (dc, addrs) in &map {
for (ip, port) in addrs {
let addr = SocketAddr::new(*ip, *port);
let _ = self
.connect_one_for_dc(
addr,
*dc,
self.rng.as_ref(),
)
.await;
}
let preferred = self.preferred_endpoints_by_dc.load_full();
for (dc, addrs) in preferred.iter() {
for addr in addrs {
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;
}
}
}
if !self.writers.read().await.is_empty() {
if !self.writers.snapshot().is_empty() {
continue;
}
let deadline = *no_writer_deadline.get_or_insert_with(|| {
Instant::now() + self.route_runtime.me_route_inline_recovery_wait
});
if !self.wait_for_writer_until(deadline).await {
if !self.writers.read().await.is_empty() {
if !self.writers.snapshot().is_empty() {
continue;
}
self.stats.increment_me_no_writer_failfast_total();
@@ -222,7 +247,7 @@ impl MePool {
}
}
}
ws.clone()
ws
};
let mut candidate_indices = self
@@ -285,7 +310,12 @@ impl MePool {
));
}
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());
for addr in endpoints {
if self
@@ -298,9 +328,7 @@ impl MePool {
}
tokio::time::sleep(Duration::from_millis(100 * emergency_attempts as u64))
.await;
let ws2 = self.writers.read().await;
writers_snapshot = ws2.clone();
drop(ws2);
writers_snapshot = self.writers.snapshot();
candidate_indices = self
.candidate_indices_for_dc(&writers_snapshot, routed_dc, false)
.await;
@@ -563,33 +591,48 @@ impl MePool {
self.note_hybrid_route_success();
return Ok(());
}
Err(TrySendError::Full(cmd)) => match current.tx.send(cmd).await {
Ok(()) => {
self.note_hybrid_route_success();
return Ok(());
}
Err(send_err) => {
let Some(payload) = proxy_req_payload_from_command(send_err.0) else {
Err(TrySendError::Full(cmd)) => {
match reserve_writer_command_slot(
&current.tx,
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 rejected unexpected command type".into(),
"ME writer channel full within blocking send timeout".into(),
));
};
warn!(writer_id = current.writer_id, "ME writer channel closed");
self.remove_writer_and_close_clients(current.writer_id)
.await;
return self
.send_proxy_req(
conn_id,
target_dc,
client_addr,
our_addr,
payload.as_ref(),
proto_flags,
tag.as_ref().map(|tag| tag.as_slice()),
)
.await;
}
Err(WriterCommandReserveError::Closed) => {
let Some(payload) = proxy_req_payload_from_command(cmd) else {
return Err(ProxyError::Proxy(
"ME writer rejected unexpected command type".into(),
));
};
warn!(writer_id = current.writer_id, "ME writer channel closed");
self.remove_writer_and_close_clients(current.writer_id)
.await;
return self
.send_proxy_req(
conn_id,
target_dc,
client_addr,
our_addr,
payload.as_ref(),
proto_flags,
tag.as_ref().map(|tag| tag.as_slice()),
)
.await;
}
}
},
}
Err(TrySendError::Closed(cmd)) => {
let Some(payload) = proxy_req_payload_from_command(cmd) else {
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::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 {
/// Sends an extended close signal for a client-bound ME connection.
pub async fn send_close(self: &Arc<Self>, conn_id: u64) -> Result<()> {
if let Some(w) = self.registry.get_writer(conn_id).await {
let payload = build_control_payload(RPC_CLOSE_EXT_U32, conn_id);
if w.tx
.send(WriterCommand::ControlAndFlush(payload))
.await
.is_err()
{
debug!("ME close write failed");
self.remove_writer_and_close_clients(w.writer_id).await;
match w.tx.try_send(WriterCommand::ControlAndFlush(payload)) {
Ok(()) => {}
Err(TrySendError::Full(cmd)) => {
match reserve_writer_command_slot(&w.tx, Some(ME_CLOSE_SIGNAL_SEND_TIMEOUT))
.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 {
debug!(conn_id, "ME close skipped (writer missing)");
@@ -31,13 +56,16 @@ impl MePool {
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<()> {
if let Some(w) = self.registry.get_writer(conn_id).await {
let payload = build_control_payload(RPC_CLOSE_CONN_U32, conn_id);
match w.tx.try_send(WriterCommand::ControlAndFlush(payload)) {
Ok(()) => {}
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(_)) => {
debug!(conn_id, "ME close_conn skipped: writer channel closed");
@@ -51,6 +79,7 @@ impl MePool {
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 {
let conn_ids = self.registry.active_conn_ids().await;
let total = conn_ids.len();
@@ -60,6 +89,7 @@ impl MePool {
total
}
/// Returns the current number of active ME writers tracked by the pool.
pub fn connection_count(&self) -> usize {
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::atomic::Ordering;
use std::time::{Duration, Instant};
use tracing::warn;
use crate::network::IpFamily;
use super::super::MePool;
use super::{
HYBRID_GLOBAL_BURST_PERIOD_ROUNDS, HYBRID_RECENT_SUCCESS_WINDOW_MS,
@@ -17,18 +13,18 @@ use super::{
impl MePool {
pub(super) async fn wait_for_writer_until(&self, deadline: Instant) -> bool {
let mut rx = self.writer_epoch.subscribe();
if !self.writers.read().await.is_empty() {
if !self.writers.snapshot().is_empty() {
return true;
}
let now = Instant::now();
if now >= deadline {
return !self.writers.read().await.is_empty();
return !self.writers.snapshot().is_empty();
}
let timeout = deadline.saturating_duration_since(now);
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 {
@@ -58,11 +54,11 @@ impl MePool {
pub(super) async fn has_candidate_for_target_dc(&self, routed_dc: i32) -> bool {
let writers_snapshot = {
let ws = self.writers.read().await;
let ws = self.writers.snapshot();
if ws.is_empty() {
return false;
}
ws.clone()
ws
};
let mut candidate_indices = self
.candidate_indices_for_dc(&writers_snapshot, routed_dc, false)
@@ -79,7 +75,7 @@ impl MePool {
self: &Arc<Self>,
routed_dc: i32,
) -> 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() {
return false;
}
@@ -92,33 +88,19 @@ impl MePool {
pub(super) async fn trigger_async_recovery_global(self: &Arc<Self>) {
self.stats.increment_me_async_recovery_trigger_total();
let mut seen = HashSet::<(i32, SocketAddr)>::new();
for family in self.family_order() {
let map_guard = match family {
IpFamily::V4 => self.proxy_map_v4.read().await,
IpFamily::V6 => self.proxy_map_v6.read().await,
};
for (dc, addrs) in map_guard.iter() {
for (ip, port) in addrs {
let addr = SocketAddr::new(*ip, *port);
if seen.insert((*dc, addr)) {
self.trigger_immediate_refill_for_dc(addr, *dc);
}
if seen.len() >= 8 {
return;
}
let preferred = self.preferred_endpoints_by_dc.load();
let mut triggered = 0usize;
for (dc, addrs) in preferred.iter() {
for addr in addrs {
self.trigger_immediate_refill_for_dc(*addr, *dc);
triggered = triggered.saturating_add(1);
if triggered >= 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(
self: &Arc<Self>,
routed_dc: i32,

View File

@@ -15,7 +15,10 @@ impl MePool {
routed_dc: i32,
include_warm: bool,
) -> 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() {
return Vec::new();
}
@@ -25,7 +28,7 @@ impl MePool {
if !self.writer_eligible_for_selection(w, include_warm) {
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);
}
}