Compare commits

..

2 Commits

Author SHA1 Message Date
Danila Yudin 57729f9e0e Merge 06eb112efd into 877d16659e 2026-04-09 19:45:54 +03:00
sabraman 06eb112efd Harden overload auth scans and masking safeguards 2026-04-09 03:45:08 +03:00
117 changed files with 5929 additions and 17133 deletions
-16
View File
@@ -1,16 +0,0 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom:
- https://nowpayments.io/donation?api_key=2bf1afd2-abc2-49f9-a012-f1e715b37223
+56 -45
View File
@@ -3,39 +3,50 @@
## Purpose
**Telemt exists to solve technical problems.**
- Telemt is open to contributors who want to learn, improve and build meaningful systems together.
- It is a place for building, testing, reasoning, documenting, and improving systems.
- Discussions that advance this work are in scope, discussions that divert it are not.
- Technology has consequences, responsibility is inherent.
> **Absicht bestimmt die Form**
Telemt is open to contributors who want to learn, improve and build meaningful systems together.
> Design follows intent
It is a place for building, testing, reasoning, documenting, and improving systems.
Discussions that advance this work are in scope. Discussions that divert it are not.
Technology has consequences. Responsibility is inherent.
> **Zweck bestimmt die Form.**
> Purpose defines form.
---
## Principles
* **Technical over emotional**
- Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
* **Clarity over noise**
- Communication is structured, concise, and relevant.
Communication is structured, concise, and relevant.
* **Openness with standards**
- Participation is open. The work remains disciplined.
Participation is open. The work remains disciplined.
* **Independence of judgment**
- Claims are evaluated on technical merit, not affiliation or posture.
Claims are evaluated on technical merit, not affiliation or posture.
* **Responsibility over capability**
- Capability does not justify careless use.
Capability does not justify careless use.
* **Cooperation over friction**
- Progress depends on coordination, mutual support, and honest review.
Progress depends on coordination, mutual support, and honest review.
* **Good intent, rigorous method**
- Assume good intent, but require rigor.
Assume good intent, but require rigor.
> **Aussagen gelten nach ihrer Begründung.**
@@ -57,9 +68,7 @@ Participants are expected to:
Precision is learned.
- New contributors are welcome
- They are expected to grow into these standards
- Existing contributors are expected to make that growth possible
New contributors are welcome. They are expected to grow into these standards. Existing contributors are expected to make that growth possible.
> **Wer behauptet, belegt.**
@@ -103,7 +112,7 @@ Security is both technical and behavioral.
---
## Openness
## 6. Openness
Telemt is open to contributors of different backgrounds, experience levels, and working styles.
@@ -139,9 +148,10 @@ Judgment should be exercised with restraint, consistency, and institutional resp
All decisions are expected to serve the durability, clarity, and integrity of Telemt.
> **Klarheit vor Zustimmung - Bestand vor Beifall**
> **Ordnung ist Voraussetzung der Funktion.**
> Order is the precondition of function.
> Clarity above approval - substantiality before success
---
## Enforcement
@@ -161,41 +171,42 @@ Actions are taken to maintain function, continuity, and signal quality.
## Final
**Telemt is built on discipline, structure, and shared intent**
- Signal over noise
- Facts over opinion
- Systems over rhetoric
- Work is collective
- Outcomes are shared
- Responsibility is distributed
- Precision is learned
- Rigor is expected
- Help is part of the work
Telemt is built on discipline, structure, and shared intent.
- Signal over noise.
- Facts over opinion.
- Systems over rhetoric.
> **Ordnung ist Voraussetzung der Freiheit**
- Work is collective.
- Outcomes are shared.
- Responsibility is distributed.
- If you contribute — contribute with care
- If you speak — speak with substance
- If you engage — engage constructively
- Precision is learned.
- Rigor is expected.
- Help is part of the work.
> **Ordnung ist Voraussetzung der Freiheit.**
- If you contribute — contribute with care.
- If you speak — speak with substance.
- If you engage — engage constructively.
---
## After All
Systems outlive intentions
- What is built will be used
- What is released will propagate
- What is maintained will define the future state
Systems outlive intentions.
- What is built will be used.
- What is released will propagate.
- What is maintained will define the future state.
There is no neutral infrastructure, only infrastructure shaped well or poorly
There is no neutral infrastructure, only infrastructure shaped well or poorly.
> **Ordnung → Umsetzung → Ergebnis**
> **Jedes System trägt Verantwortung.**
> Order → Implementation → Result
> Every system carries responsibility.
- Stability requires discipline
- Freedom requires structure
- Trust requires honesty
In the end: the system reflects its contributors
- Stability requires discipline.
- Freedom requires structure.
- Trust requires honesty.
In the end: the system reflects its contributors.
Generated
+94 -112
View File
@@ -90,9 +90,9 @@ checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
[[package]]
name = "arc-swap"
version = "1.9.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
checksum = "a07d1f37ff60921c83bdfc7407723bdefe89b44b98a9b772f225c8f9d67141a6"
dependencies = [
"rustversion",
]
@@ -173,9 +173,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "aws-lc-rs"
version = "1.16.3"
version = "1.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -183,9 +183,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
version = "0.40.0"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
dependencies = [
"cc",
"cmake",
@@ -228,9 +228,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.11.1"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "blake3"
@@ -299,9 +299,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.60"
version = "1.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20"
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -346,7 +346,7 @@ checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
"rand_core 0.10.0",
]
[[package]]
@@ -416,9 +416,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.6.1"
version = "4.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
dependencies = [
"clap_builder",
]
@@ -805,9 +805,9 @@ dependencies = [
[[package]]
name = "fastrand"
version = "2.4.1"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fiat-crypto"
@@ -997,7 +997,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core 0.10.1",
"rand_core 0.10.0",
"wasip2",
"wasip3",
]
@@ -1068,12 +1068,6 @@ dependencies = [
"foldhash 0.2.0",
]
[[package]]
name = "hashbrown"
version = "0.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
[[package]]
name = "heck"
version = "0.5.0"
@@ -1102,7 +1096,7 @@ dependencies = [
"idna",
"ipnet",
"once_cell",
"rand 0.9.4",
"rand 0.9.2",
"ring",
"thiserror 2.0.18",
"tinyvec",
@@ -1124,7 +1118,7 @@ dependencies = [
"moka",
"once_cell",
"parking_lot",
"rand 0.9.4",
"rand 0.9.2",
"resolv-conf",
"smallvec",
"thiserror 2.0.18",
@@ -1219,14 +1213,15 @@ dependencies = [
[[package]]
name = "hyper-rustls"
version = "0.27.9"
version = "0.27.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
dependencies = [
"http",
"hyper",
"hyper-util",
"rustls",
"rustls-pki-types",
"tokio",
"tokio-rustls",
"tower-service",
@@ -1390,12 +1385,12 @@ dependencies = [
[[package]]
name = "indexmap"
version = "2.14.0"
version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
"hashbrown 0.17.0",
"hashbrown 0.16.1",
"serde",
"serde_core",
]
@@ -1406,7 +1401,7 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd5b3eaf1a28b758ac0faa5a4254e8ab2705605496f1b1f3fbbc3988ad73d199"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
"inotify-sys",
"libc",
]
@@ -1539,9 +1534,9 @@ dependencies = [
[[package]]
name = "js-sys"
version = "0.3.95"
version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
dependencies = [
"cfg-if",
"futures-util",
@@ -1583,9 +1578,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
[[package]]
name = "libc"
version = "0.2.185"
version = "0.2.184"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f"
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
[[package]]
name = "linux-raw-sys"
@@ -1616,9 +1611,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.16.4"
version = "0.16.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39"
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
dependencies = [
"hashbrown 0.16.1",
]
@@ -1710,7 +1705,7 @@ version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
"cfg-if",
"cfg_aliases",
"libc",
@@ -1733,7 +1728,7 @@ version = "8.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d3d07927151ff8575b7087f245456e549fea62edf0ec4e565a5ee50c8402bc3"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
"fsevent-sys",
"inotify",
"kqueue",
@@ -1751,7 +1746,7 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b8cfee0e339a0337359f3c88165702ac6e600dc01c0cc9579a92d62b08477a"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
]
[[package]]
@@ -2017,9 +2012,9 @@ checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
dependencies = [
"bit-set",
"bit-vec",
"bitflags 2.11.1",
"bitflags 2.11.0",
"num-traits",
"rand 0.9.4",
"rand 0.9.2",
"rand_chacha",
"rand_xorshift",
"regex-syntax",
@@ -2064,7 +2059,7 @@ dependencies = [
"bytes",
"getrandom 0.3.4",
"lru-slab",
"rand 0.9.4",
"rand 0.9.2",
"ring",
"rustc-hash",
"rustls",
@@ -2113,9 +2108,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.9.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
"rand_core 0.9.5",
@@ -2123,13 +2118,13 @@ dependencies = [
[[package]]
name = "rand"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
checksum = "bc266eb313df6c5c09c1c7b1fbe2510961e5bcd3add930c1e31f7ed9da0feff8"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.2",
"rand_core 0.10.1",
"rand_core 0.10.0",
]
[[package]]
@@ -2162,9 +2157,9 @@ dependencies = [
[[package]]
name = "rand_core"
version = "0.10.1"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba"
[[package]]
name = "rand_xorshift"
@@ -2177,9 +2172,9 @@ dependencies = [
[[package]]
name = "rayon"
version = "1.12.0"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
dependencies = [
"either",
"rayon-core",
@@ -2201,7 +2196,7 @@ version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
]
[[package]]
@@ -2331,7 +2326,7 @@ version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
"errno",
"libc",
"linux-raw-sys",
@@ -2340,9 +2335,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.38"
version = "0.23.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21"
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
dependencies = [
"aws-lc-rs",
"once_cell",
@@ -2404,9 +2399,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
[[package]]
name = "rustls-webpki"
version = "0.103.12"
version = "0.103.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06"
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
dependencies = [
"aws-lc-rs",
"ring",
@@ -2479,7 +2474,7 @@ version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
"core-foundation",
"core-foundation-sys",
"libc",
@@ -2498,9 +2493,9 @@ dependencies = [
[[package]]
name = "semver"
version = "1.0.28"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "sendfd"
@@ -2620,7 +2615,7 @@ dependencies = [
"notify",
"percent-encoding",
"pin-project",
"rand 0.9.4",
"rand 0.9.2",
"sealed",
"sendfd",
"serde",
@@ -2651,7 +2646,7 @@ dependencies = [
"chacha20poly1305",
"hkdf",
"md-5",
"rand 0.9.4",
"rand 0.9.2",
"ring-compat",
"sha1",
]
@@ -2746,12 +2741,6 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]]
name = "syn"
version = "2.0.117"
@@ -2791,7 +2780,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "telemt"
version = "3.4.5"
version = "3.3.39"
dependencies = [
"aes",
"anyhow",
@@ -2823,7 +2812,7 @@ dependencies = [
"num-traits",
"parking_lot",
"proptest",
"rand 0.10.1",
"rand 0.10.0",
"regex",
"reqwest",
"rustls",
@@ -2981,9 +2970,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.52.1"
version = "1.50.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d"
dependencies = [
"bytes",
"libc",
@@ -2999,9 +2988,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.7.0"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c"
dependencies = [
"proc-macro2",
"quote",
@@ -3134,7 +3123,7 @@ version = "0.6.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
"bytes",
"futures-util",
"http",
@@ -3171,12 +3160,11 @@ dependencies = [
[[package]]
name = "tracing-appender"
version = "0.2.5"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
checksum = "786d480bce6247ab75f005b14ae1624ad978d3029d9113f0a22fa1ac773faeaf"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
@@ -3251,9 +3239,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
version = "1.20.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "unarray"
@@ -3309,9 +3297,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "uuid"
version = "1.23.1"
version = "1.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
dependencies = [
"getrandom 0.4.2",
"js-sys",
@@ -3366,11 +3354,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "wasip2"
version = "1.0.3+wasi-0.2.9"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen 0.57.1",
"wit-bindgen",
]
[[package]]
@@ -3379,14 +3367,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
dependencies = [
"wit-bindgen 0.51.0",
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.118"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
dependencies = [
"cfg-if",
"once_cell",
@@ -3397,9 +3385,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.68"
version = "0.4.67"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3407,9 +3395,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.118"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3417,9 +3405,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.118"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -3430,9 +3418,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.118"
version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
dependencies = [
"unicode-ident",
]
@@ -3465,7 +3453,7 @@ version = "0.244.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
dependencies = [
"bitflags 2.11.1",
"bitflags 2.11.0",
"hashbrown 0.15.5",
"indexmap",
"semver",
@@ -3473,9 +3461,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.95"
version = "0.3.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3493,18 +3481,18 @@ dependencies = [
[[package]]
name = "webpki-root-certs"
version = "1.0.7"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "webpki-roots"
version = "1.0.7"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
dependencies = [
"rustls-pki-types",
]
@@ -3853,12 +3841,6 @@ dependencies = [
"wit-bindgen-rust-macro",
]
[[package]]
name = "wit-bindgen"
version = "0.57.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
[[package]]
name = "wit-bindgen-core"
version = "0.51.0"
@@ -3908,7 +3890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
dependencies = [
"anyhow",
"bitflags 2.11.1",
"bitflags 2.11.0",
"indexmap",
"log",
"serde",
@@ -3940,9 +3922,9 @@ dependencies = [
[[package]]
name = "writeable"
version = "0.6.3"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]]
name = "x25519-dalek"
+2 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "telemt"
version = "3.4.5"
version = "3.3.39"
edition = "2024"
[features]
@@ -98,3 +98,4 @@ harness = false
[profile.release]
lto = "fat"
codegen-units = 1
-30
View File
@@ -77,34 +77,6 @@ COPY config.toml /app/config.toml
EXPOSE 443 9090 9091
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["/app/telemt", "healthcheck", "/app/config.toml", "--mode", "liveness"]
ENTRYPOINT ["/app/telemt"]
CMD ["config.toml"]
# ==========================
# Production Netfilter Profile
# ==========================
FROM debian:12-slim AS prod-netfilter
RUN set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends \
ca-certificates \
conntrack \
nftables \
iptables; \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=minimal /telemt /app/telemt
COPY config.toml /app/config.toml
EXPOSE 443 9090 9091
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["/app/telemt", "healthcheck", "/app/config.toml", "--mode", "liveness"]
ENTRYPOINT ["/app/telemt"]
CMD ["config.toml"]
@@ -122,7 +94,5 @@ USER nonroot:nonroot
EXPOSE 443 9090 9091
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["/app/telemt", "healthcheck", "/app/config.toml", "--mode", "liveness"]
ENTRYPOINT ["/app/telemt"]
CMD ["config.toml"]
+40 -47
View File
@@ -1,33 +1,31 @@
# Telemt - MTProxy on Rust + Tokio
[![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon)](https://github.com/telemt/telemt/releases/latest) [![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social)](https://github.com/telemt/telemt/stargazers) [![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social)](https://github.com/telemt/telemt/network/members) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs)
[🇷🇺 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
> Fixed TLS ClientHello is now available:
> - in **Telegram Desktop** starting from version **6.7.2**
> - in **Telegram Android Client** starting from version **12.6.4**
> - **release for iOS is "work in progress"**
>
> To work with EE-MTProxy, please update your client!
<p align="center">
<a href="https://t.me/telemtrs">
<img src="/docs/assets/telegram_button.svg" width="150"/>
<img src="/docs/assets/telegram_button.svg" width="200"/>
</a>
</p>
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/Architecture/Model/MODEL.en.md);
- [Full-covered API w/ management](https://github.com/telemt/telemt/blob/main/docs/Architecture/API/API.md);
- Anti-Replay on Sliding Window;
- Prometheus-format Metrics;
- TLS-Fronting and TCP-Splicing for masking from "prying" eyes.
### One-command Install and Update
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
- [Quick Start Guide](docs/Quick_start/QUICK_START_GUIDE.en.md)
- [Инструкция по быстрому запуску](docs/Quick_start/QUICK_START_GUIDE.ru.md)
![telemt_scheme](docs/assets/telemt.png)
## Features
Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](docs/FAQ.en.md#recognizability-for-dpi-and-crawler)
Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
@@ -42,17 +40,29 @@ Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared t
- Graceful shutdown on Ctrl+C;
- Extensive logging via `trace` and `debug` with `RUST_LOG` method.
## One-command installation (update on re-ru)
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
See more in the [Quick Start Guide](docs/Quick_start/QUICK_START_GUIDE.en.md).
# GOTO
- [FAQ](#faq)
- [Architecture](docs/Architecture)
- [Quick Start Guide](#quick-start-guide)
- [Config parameters](docs/Config_params)
- [Build](#build)
- [Why Rust?](#why-rust)
## Quick Start Guide
- [Quick Start Guide RU](docs/Quick_start/QUICK_START_GUIDE.ru.md)
- [Quick Start Guide EN](docs/Quick_start/QUICK_START_GUIDE.en.md)
## FAQ
- [FAQ RU](docs/FAQ.ru.md)
- [FAQ EN](docs/FAQ.en.md)
# Learn more about Telemt
- [Our Architecture](docs/Architecture)
- [All Config Options](docs/Config_params)
- [How to build your own Telemt?](#build)
- [Running on BSD](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md)
- [Why Rust?](#why-rust)
## Build
```bash
# Cloning repo
@@ -62,8 +72,9 @@ cd telemt
# Starting Release Build
cargo build --release
# Current release profile uses lto = "fat" for maximum optimization (see Cargo.toml).
# On low-RAM systems (~1 GB) you can override it to "thin".
# Low-RAM devices (1 GB, e.g. NanoPi Neo3 / Raspberry Pi Zero 2):
# release profile uses lto = "thin" to reduce peak linker memory.
# If your custom toolchain overrides profiles, avoid enabling fat LTO.
# Move to /bin
mv ./target/release/telemt /bin
@@ -73,33 +84,15 @@ chmod +x /bin/telemt
telemt config.toml
```
### OpenBSD
- Build and service setup guide: [OpenBSD Guide (EN)](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md)
- Example rc.d script: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd)
- Status: OpenBSD sandbox hardening with `pledge(2)` and `unveil(2)` is not implemented yet.
## Why Rust?
- Long-running reliability and idempotent behavior
- Rust's deterministic resource management - RAII
- No garbage collector
- Memory safety and reduced attack surface
- Tokio's asynchronous architecture
## Support Telemt
Telemt is free, open-source, and built in personal time.
If it helps you — consider supporting continued development.
Any cryptocurrency (BTC, ETH, USDT, 350+ coins):
<p align="center">
<a href="https://nowpayments.io/donation?api_key=2bf1afd2-abc2-49f9-a012-f1e715b37223" target="_blank" rel="noreferrer noopener">
<img src="https://nowpayments.io/images/embeds/donation-button-white.svg" alt="Cryptocurrency & Bitcoin donation button by NOWPayments" height="80">
</a>
</p>
Monero (XMR) directly:
```
8Bk4tZEYPQWSypeD2hrUXG2rKbAKF16GqEN942ZdAP5cFdSqW6h4DwkP5cJMAdszzuPeHeHZPTyjWWFwzeFdjuci3ktfMoB
```
All donations go toward infrastructure, development, and research.
![telemt_scheme](docs/assets/telemt.png)
+40 -39
View File
@@ -1,31 +1,30 @@
# Telemt — MTProxy на Rust + Tokio
[![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon)](https://github.com/telemt/telemt/releases/latest) [![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social)](https://github.com/telemt/telemt/stargazers) [![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social)](https://github.com/telemt/telemt/network/members) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs)
***Решает проблемы раньше, чем другие узнают об их существовании***
> [!NOTE]
>
> Исправленный TLS ClientHello доступен в Telegram для настольных ПК, Android и iOS.
>
> Пожалуйста, обновите клиентское приложение для работы с EE-MTProxy.
> Исправленный TLS ClientHello доступен в **Telegram Desktop** начиная с версии **6.7.2**: для работы с EE-MTProxy обновите клиент.
>
> Исправленный TLS ClientHello доступен в **Telegram Android** начиная с версии **12.6.4**; **официальный релиз для iOS находится в процессе разработки**.
<p align="center">
<a href="https://t.me/telemtrs">
<img src="/docs/assets/telegram_button.svg" width="150"/>
<img src="/docs/assets/telegram_button.svg" width="200"/>
</a>
</p>
**Telemt** — это быстрый, безопасный и функциональный сервер, написанный на Rust. Он полностью реализует официальный алгоритм прокси Telegram и добавляет множество улучшений для продакшена:
## Установка и обновление одной командой
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + жизненный цикл генераций](https://github.com/telemt/telemt/blob/main/docs/Architecture/Model/MODEL.en.md);
- [Полноценный API с управлением](https://github.com/telemt/telemt/blob/main/docs/Architecture/API/API.md);
- Защита от повторных атак (Anti-Replay on Sliding Window);
- Метрики в формате Prometheus;
- TLS-fronting и TCP-splicing для маскировки от DPI.
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
![telemt_scheme](docs/assets/telemt.png)
- [Инструкция по быстрому запуску](docs/Quick_start/QUICK_START_GUIDE.ru.md)
- [Quick Start Guide](docs/Quick_start/QUICK_START_GUIDE.en.md)
## Особенности
Реализация **TLS-fronting** максимально приближена к поведению реального HTTPS-трафика (подробнее - [FAQ](docs/FAQ.ru.md#распознаваемость-для-dpi-и-сканеров)).
@@ -41,19 +40,35 @@ curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
- Корректное завершение работы (Ctrl+C);
- Подробное логирование через `trace` и `debug`.
# Подробнее о Telemt
## Быстрая установка (обновление при повторном запуске)
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
Подробнее об установке в [Quick Start Guide](docs/Quick_start/QUICK_START_GUIDE.ru.md).
# Навигация
- [FAQ](#faq)
- [Архитектура](docs/Architecture)
- [Быстрый старт](#quick-start-guide)
- [Параметры конфигурационного файла](docs/Config_params)
- [Сборка](#build)
- [Установка на BSD](#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%BD%D0%B0-bsd)
- [Почему Rust?](#why-rust)
- [Известные проблемы](#issues)
- [Планы](#roadmap)
## Быстрый старт
- [Quick Start Guide RU](docs/Quick_start/QUICK_START_GUIDE.ru.md)
- [Quick Start Guide EN](docs/Quick_start/QUICK_START_GUIDE.en.md)
## FAQ
- [FAQ RU](docs/FAQ.ru.md)
- [FAQ EN](docs/FAQ.en.md)
## Сборка
```bash
# Клонируйте репозиторий
git clone https://github.com/telemt/telemt
@@ -62,8 +77,9 @@ cd telemt
# Начните процесс сборки
cargo build --release
# В текущем release-профиле используется lto = "fat" для максимальной оптимизации (см. Cargo.toml).
# На системах с малым объёмом RAM (~1 ГБ) можно переопределить это значение на "thin".
# Устройства с небольшим объёмом оперативной памяти (1 ГБ, например NanoPi Neo3 / Raspberry Pi Zero 2):
# используется параметр lto = «thin» для уменьшения пикового потребления памяти.
# Если ваш пользовательский набор инструментов переопределяет профили, не используйте Fat LTO.
# Перейдите в каталог /bin
mv ./target/release/telemt /bin
@@ -73,37 +89,22 @@ chmod +x /bin/telemt
telemt config.toml
```
## Установка на BSD
### Устройства с малым объемом RAM
Для устройств с ~1 ГБ RAM (например Raspberry Pi):
- используется облегчённая оптимизация линковщика (thin LTO);
- не рекомендуется включать fat LTO.
## OpenBSD
- Руководство по сборке и настройке на английском языке [OpenBSD Guide (EN)](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md);
- Пример rc.d скрипта: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd);
- Поддержка sandbox с `pledge(2)` и `unveil(2)` пока не реализована.
## Почему Rust?
- Надёжность для долгоживущих процессов;
- Детерминированное управление ресурсами (RAII);
- Отсутствие сборщика мусора;
- Безопасность памяти;
- Асинхронная архитектура Tokio.
## Поддержать Telemt
Telemt — это бесплатное программное обеспечение с открытым исходным кодом, разработанное в свободное время.
Если оно оказалось вам полезным, вы можете поддержать дальнейшую разработку.
Принимаемые криптовалюты (BTC, ETH, USDT, 350+ и другие):
<p align="center">
<a href="https://nowpayments.io/donation?api_key=2bf1afd2-abc2-49f9-a012-f1e715b37223" target="_blank" rel="noreferrer noopener">
<img src="https://nowpayments.io/images/embeds/donation-button-white.svg" alt="Cryptocurrency & Bitcoin donation button by NOWPayments" height="80">
</a>
</p>
Monero (XMR) напрямую:
```
8Bk4tZEYPQWSypeD2hrUXG2rKbAKF16GqEN942ZdAP5cFdSqW6h4DwkP5cJMAdszzuPeHeHZPTyjWWFwzeFdjuci3ktfMoB
```
Все пожертвования пойдут на инфраструктуру, разработку и исследования.
![telemt_scheme](docs/assets/telemt.png)
+5 -8
View File
@@ -32,13 +32,13 @@ show = "*"
port = 443
# proxy_protocol = false # Enable if behind HAProxy/nginx with PROXY protocol
# metrics_port = 9090
# metrics_listen = "127.0.0.1:9090" # Listen address for metrics (overrides metrics_port)
# metrics_whitelist = ["127.0.0.1/32", "::1/128"]
# metrics_listen = "0.0.0.0:9090" # Listen address for metrics (overrides metrics_port)
# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"]
[server.api]
enabled = true
listen = "127.0.0.1:9091"
whitelist = ["127.0.0.1/32", "::1/128"]
listen = "0.0.0.0:9091"
whitelist = ["127.0.0.0/8"]
minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000
@@ -48,12 +48,9 @@ ip = "0.0.0.0"
# === Anti-Censorship & Masking ===
[censorship]
# Fake-TLS / SNI masking domain used in generated ee-links.
# Changing tls_domain invalidates previously generated TLS links.
tls_domain = "petrovich.ru"
mask = true
tls_emulation = true # Fetch real cert lengths and emulate TLS records
tls_emulation = true # Fetch real cert lengths and emulate TLS records
tls_front_dir = "tlsfront" # Cache directory for TLS emulation
[access.users]
-10
View File
@@ -1,10 +0,0 @@
services:
telemt:
build:
context: .
target: prod-netfilter
network_mode: host
ports: []
cap_add:
- NET_BIND_SERVICE
- NET_ADMIN
-8
View File
@@ -1,8 +0,0 @@
services:
telemt:
build:
context: .
target: prod-netfilter
cap_add:
- NET_BIND_SERVICE
- NET_ADMIN
+2 -9
View File
@@ -1,9 +1,7 @@
services:
telemt:
image: ghcr.io/telemt/telemt:latest
build:
context: .
target: prod
build: .
container_name: telemt
restart: unless-stopped
ports:
@@ -18,18 +16,13 @@ services:
- /etc/telemt:rw,mode=1777,size=4m
environment:
- RUST_LOG=info
healthcheck:
test: [ "CMD", "/app/telemt", "healthcheck", "/etc/telemt/config.toml", "--mode", "liveness" ]
interval: 30s
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:
- NET_BIND_SERVICE
- NET_ADMIN
read_only: true
security_opt:
- no-new-privileges:true
+4 -4
View File
@@ -9,12 +9,12 @@ API runtime is configured in `[server.api]`.
| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `enabled` | `bool` | `true` | Enables REST API listener. |
| `listen` | `string` (`IP:PORT`) | `0.0.0.0:9091` | API bind address. |
| `whitelist` | `CIDR[]` | `127.0.0.0/8` | Source IP allowlist. Empty list means allow all. |
| `enabled` | `bool` | `false` | Enables REST API listener. |
| `listen` | `string` (`IP:PORT`) | `127.0.0.1:9091` | API bind address. |
| `whitelist` | `CIDR[]` | `127.0.0.1/32, ::1/128` | Source IP allowlist. Empty list means allow all. |
| `auth_header` | `string` | `""` | Exact value for `Authorization` header. Empty disables header auth. |
| `request_body_limit_bytes` | `usize` | `65536` | Maximum request body size. Must be `> 0`. |
| `minimal_runtime_enabled` | `bool` | `true` | Enables runtime snapshot endpoints requiring ME pool read-lock aggregation. |
| `minimal_runtime_enabled` | `bool` | `false` | Enables runtime snapshot endpoints requiring ME pool read-lock aggregation. |
| `minimal_runtime_cache_ttl_ms` | `u64` | `1000` | Cache TTL for minimal snapshots. `0` disables cache; valid range is `[0, 60000]`. |
| `runtime_edge_enabled` | `bool` | `false` | Enables runtime edge endpoints with cached aggregation payloads. |
| `runtime_edge_cache_ttl_ms` | `u64` | `1000` | Cache TTL for runtime edge summary payloads. `0` disables cache. |
@@ -1,225 +0,0 @@
# TLS Front Profile Fidelity
## Overview
This document describes how Telemt reuses captured TLS behavior in the FakeTLS server flight and how to validate the result on a real deployment.
When TLS front emulation is enabled, Telemt can capture useful server-side TLS behavior from the selected origin and reuse that behavior in the emulated success path. The goal is not to reproduce the origin byte-for-byte, but to reduce stable synthetic traits and make the emitted server flight structurally closer to the captured profile.
## Why this change exists
The project already captures useful server-side TLS behavior in the TLS front fetch path:
- `change_cipher_spec_count`
- `app_data_record_sizes`
- `ticket_record_sizes`
Before this change, the emulator used only part of that information. This left a gap between captured origin behavior and emitted FakeTLS server flight.
## What is implemented
- The emulator now replays the observed `ChangeCipherSpec` count from the fetched behavior profile.
- The emulator now replays observed ticket-like tail ApplicationData record sizes when raw or merged TLS profile data is available.
- The emulator now preserves more of the profiled encrypted-flight structure instead of collapsing it into a smaller synthetic shape.
- The emulator still falls back to the previous synthetic behavior when the cached profile does not contain raw TLS behavior information.
- Operator-configured `tls_new_session_tickets` still works as an additive fallback when the profile does not provide enough tail records.
## Practical benefit
- Reduced distinguishability between profiled origin TLS behavior and emulated TLS behavior.
- Lower chance of stable server-flight fingerprints caused by fixed CCS count or synthetic-only tail record sizes.
- Better reuse of already captured TLS profile data without changing MTProto logic, KDF routing, or transport architecture.
## Limitations
This mechanism does not aim to make Telemt byte-identical to the origin server.
It also does not change:
- MTProto business logic;
- KDF routing behavior;
- the overall transport architecture.
The practical goal is narrower:
- reuse more captured profile data;
- reduce fixed synthetic behavior in the server flight;
- preserve a valid FakeTLS success path while changing the emitted shape on the wire.
## Validation targets
- Correct count of emulated `ChangeCipherSpec` records.
- Correct replay of observed ticket-tail record sizes.
- No regression in existing ALPN and payload-placement behavior.
## How to validate the result
Recommended validation consists of two layers:
- focused unit and security tests for CCS-count replay and ticket-tail replay;
- real packet-capture comparison for a selected origin and a successful FakeTLS session.
When testing on the network, the expected result is:
- a valid FakeTLS and MTProto success path is preserved;
- the early encrypted server flight changes shape when richer profile data is available;
- the change is visible on the wire without changing MTProto logic or transport architecture.
This validation is intended to show better reuse of captured TLS profile data.
It is not intended to prove byte-level equivalence with the real origin server.
## How to test on a real deployment
The strongest practical validation is a side-by-side trace comparison between:
- a real TLS origin server used as `mask_host`;
- a Telemt FakeTLS success-path connection for the same SNI;
- optional captures from different Telemt builds or configurations.
The purpose of the comparison is to inspect the shape of the server flight:
- record order;
- count of `ChangeCipherSpec` records;
- count and grouping of early encrypted `ApplicationData` records;
- lengths of tail or continuation `ApplicationData` records.
## Recommended environment
Use a Linux host or Docker container for the cleanest reproduction.
Recommended setup:
1. One Telemt instance.
2. One real HTTPS origin as `mask_host`.
3. One Telegram client configured with an `ee` proxy link for the Telemt instance.
4. `tcpdump` or Wireshark available for capture analysis.
## Step-by-step test procedure
### 1. Prepare the origin
1. Choose a real HTTPS origin.
2. Set both `censorship.tls_domain` and `censorship.mask_host` to that hostname.
3. Confirm that a direct TLS request works:
```bash
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
```
### 2. Configure Telemt
Use a configuration that enables:
- `censorship.mask = true`
- `censorship.tls_emulation = true`
- `censorship.mask_host`
- `censorship.mask_port`
Recommended for cleaner testing:
- keep `censorship.tls_new_session_tickets = 0`, so the result depends primarily on fetched profile data rather than operator-forced synthetic tail records;
- keep `censorship.tls_fetch.strict_route = true`, if cleaner provenance for captured profile data is important.
### 3. Refresh TLS profile data
1. Start Telemt.
2. Let it fetch TLS front profile data for the configured domain.
3. If `tls_front_dir` is persisted, confirm that the TLS front cache is populated.
Persisted cache artifacts are useful, but they are not required if packet captures already demonstrate the runtime result.
### 4. Capture a direct-origin trace
From a separate client host, connect directly to the origin:
```bash
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
```
Capture with:
```bash
sudo tcpdump -i any -w origin-direct.pcap host ORIGIN_IP and port 443
```
### 5. Capture a Telemt FakeTLS success-path trace
Now connect to Telemt with a real Telegram client through an `ee` proxy link that targets the Telemt instance.
`openssl s_client` is useful for direct-origin capture and fallback sanity checks, but it does not exercise the successful FakeTLS and MTProto path.
Capture with:
```bash
sudo tcpdump -i any -w telemt-emulated.pcap host TELEMT_IP and port 443
```
### 6. Decode TLS record structure
Use `tshark` to print record-level structure:
```bash
tshark -r origin-direct.pcap -Y "tls.record" -T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.record.content_type \
-e tls.record.length
```
```bash
tshark -r telemt-emulated.pcap -Y "tls.record" -T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.record.content_type \
-e tls.record.length
```
Focus on the server flight after ClientHello:
- `22` = Handshake
- `20` = ChangeCipherSpec
- `23` = ApplicationData
### 7. Build a comparison table
A compact table like the following is usually enough:
| Path | CCS count | AppData count in first encrypted flight | Tail AppData lengths |
| --- | --- | --- | --- |
| Origin | `N` | `M` | `[a, b, ...]` |
| Telemt build A | `...` | `...` | `...` |
| Telemt build B | `...` | `...` | `...` |
The comparison should make it easy to see that:
- the FakeTLS success path remains valid;
- the early encrypted server flight changes when richer profile data is reused;
- the result is backed by packet evidence.
## Example capture set
One practical example of this workflow uses:
- `origin-direct-nginx.pcap`
- `telemt-ee-before-nginx.pcap`
- `telemt-ee-after-nginx.pcap`
Practical notes:
- `origin` was captured as a direct TLS 1.2 connection to `nginx.org`;
- `before` and `after` were captured on the Telemt FakeTLS success path with a real Telegram client;
- the first server-side FakeTLS response remains valid in both cases;
- the early encrypted server-flight segmentation differs between `before` and `after`, which is consistent with better reuse of captured profile data;
- this kind of result shows a wire-visible effect without breaking the success path, but it does not claim full indistinguishability from the origin.
## Stronger validation
For broader confidence, repeat the same comparison on:
1. one CDN-backed origin;
2. one regular nginx origin;
3. one origin with a multi-record encrypted flight and visible ticket-like tails.
If the same directional improvement appears across all three, confidence in the result will be much higher than for a single-origin example.
@@ -1,225 +0,0 @@
# Fidelity TLS Front Profile
## Обзор
Этот документ описывает, как Telemt переиспользует захваченное TLS-поведение в FakeTLS server flight и как проверять результат на реальной инсталляции.
Когда включена TLS front emulation, Telemt может собирать полезное серверное TLS-поведение выбранного origin и использовать его в emulated success path. Цель здесь не в побайтном копировании origin, а в уменьшении устойчивых synthetic признаков и в том, чтобы emitted server flight был структурно ближе к захваченному profile.
## Зачем нужно это изменение
Проект уже умеет собирать полезное серверное TLS-поведение в пути TLS front fetch:
- `change_cipher_spec_count`
- `app_data_record_sizes`
- `ticket_record_sizes`
До этого изменения эмулятор использовал только часть этой информации. Из-за этого оставался разрыв между захваченным поведением origin и тем FakeTLS server flight, который реально уходил на провод.
## Что реализовано
- Эмулятор теперь воспроизводит наблюдаемое значение `ChangeCipherSpec` из полученного `behavior_profile`.
- Эмулятор теперь воспроизводит наблюдаемые размеры ticket-like tail ApplicationData records, когда доступны raw или merged TLS profile data.
- Эмулятор теперь сохраняет больше структуры профилированного encrypted flight, а не схлопывает его в более маленькую synthetic форму.
- Для профилей без raw TLS behavior по-прежнему сохраняется прежний synthetic fallback.
- Операторский `tls_new_session_tickets` по-прежнему работает как дополнительный fallback, если профиль не даёт достаточного количества tail records.
## Практическая польза
- Снижается различимость между профилированным origin TLS-поведением и эмулируемым TLS-поведением.
- Уменьшается шанс устойчивых server-flight fingerprint, вызванных фиксированным CCS count или полностью synthetic tail record sizes.
- Уже собранные TLS profile data используются лучше, без изменения MTProto logic, KDF routing или transport architecture.
## Ограничения
Этот механизм не ставит целью сделать Telemt побайтно идентичным origin server.
Он также не меняет:
- MTProto business logic;
- поведение KDF routing;
- общую transport architecture.
Практическая цель уже:
- использовать больше уже собранных profile data;
- уменьшить fixed synthetic behavior в server flight;
- сохранить валидный FakeTLS success path, одновременно меняя форму emitted traffic на проводе.
## Цели валидации
- Корректное количество эмулируемых `ChangeCipherSpec` records.
- Корректное воспроизведение наблюдаемых ticket-tail record sizes.
- Отсутствие регрессии в существующем ALPN и payload-placement behavior.
## Как проверять результат
Рекомендуемая валидация состоит из двух слоёв:
- focused unit и security tests для CCS-count replay и ticket-tail replay;
- сравнение реальных packet capture для выбранного origin и успешной FakeTLS session.
При проверке на сети ожидаемый результат такой:
- валидный FakeTLS и MTProto success path сохраняется;
- форма раннего encrypted server flight меняется, когда доступно более богатое profile data;
- изменение видно на проводе без изменения MTProto logic и transport architecture.
Такая проверка нужна для подтверждения того, что уже собранные TLS profile data используются лучше.
Она не предназначена для доказательства побайтной эквивалентности с реальным origin server.
## Как проверить на реальной инсталляции
Самая сильная практическая проверка — side-by-side trace comparison между:
- реальным TLS origin server, используемым как `mask_host`;
- Telemt FakeTLS success-path connection для того же SNI;
- при необходимости capture от разных Telemt builds или configurations.
Смысл сравнения состоит в том, чтобы посмотреть на форму server flight:
- порядок records;
- количество `ChangeCipherSpec` records;
- количество и группировку ранних encrypted `ApplicationData` records;
- размеры tail или continuation `ApplicationData` records.
## Рекомендуемое окружение
Для самой чистой проверки лучше использовать Linux host или Docker container.
Рекомендуемый setup:
1. Один экземпляр Telemt.
2. Один реальный HTTPS origin как `mask_host`.
3. Один Telegram client, настроенный на `ee` proxy link для Telemt instance.
4. `tcpdump` или Wireshark для анализа capture.
## Пошаговая процедура проверки
### 1. Подготовить origin
1. Выберите реальный HTTPS origin.
2. Установите и `censorship.tls_domain`, и `censorship.mask_host` в hostname этого origin.
3. Убедитесь, что прямой TLS request работает:
```bash
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
```
### 2. Настроить Telemt
Используйте config, где включены:
- `censorship.mask = true`
- `censorship.tls_emulation = true`
- `censorship.mask_host`
- `censorship.mask_port`
Для более чистой проверки рекомендуется:
- держать `censorship.tls_new_session_tickets = 0`, чтобы результат в первую очередь зависел от fetched profile data, а не от операторских synthetic tail records;
- держать `censorship.tls_fetch.strict_route = true`, если важна более чистая provenance для captured profile data.
### 3. Обновить TLS profile data
1. Запустите Telemt.
2. Дайте ему получить TLS front profile data для выбранного домена.
3. Если `tls_front_dir` хранится persistently, убедитесь, что TLS front cache заполнен.
Persisted cache artifacts полезны, но не обязательны, если packet capture уже показывают runtime result.
### 4. Снять direct-origin trace
С отдельной клиентской машины подключитесь напрямую к origin:
```bash
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
```
Capture:
```bash
sudo tcpdump -i any -w origin-direct.pcap host ORIGIN_IP and port 443
```
### 5. Снять Telemt FakeTLS success-path trace
Теперь подключитесь к Telemt через реальный Telegram client с `ee` proxy link, который указывает на Telemt instance.
`openssl s_client` полезен для direct-origin capture и для fallback sanity checks, но он не проходит успешный FakeTLS и MTProto path.
Capture:
```bash
sudo tcpdump -i any -w telemt-emulated.pcap host TELEMT_IP and port 443
```
### 6. Декодировать структуру TLS records
Используйте `tshark`, чтобы вывести record-level structure:
```bash
tshark -r origin-direct.pcap -Y "tls.record" -T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.record.content_type \
-e tls.record.length
```
```bash
tshark -r telemt-emulated.pcap -Y "tls.record" -T fields \
-e frame.number \
-e ip.src \
-e ip.dst \
-e tls.record.content_type \
-e tls.record.length
```
Смотрите на server flight после ClientHello:
- `22` = Handshake
- `20` = ChangeCipherSpec
- `23` = ApplicationData
### 7. Собрать сравнительную таблицу
Обычно достаточно короткой таблицы такого вида:
| Path | CCS count | AppData count in first encrypted flight | Tail AppData lengths |
| --- | --- | --- | --- |
| Origin | `N` | `M` | `[a, b, ...]` |
| Telemt build A | `...` | `...` | `...` |
| Telemt build B | `...` | `...` | `...` |
По такой таблице должно быть легко увидеть, что:
- FakeTLS success path остаётся валидным;
- ранний encrypted server flight меняется, когда переиспользуется более богатое profile data;
- результат подтверждён packet evidence.
## Пример набора capture
Один практический пример такой проверки использует:
- `origin-direct-nginx.pcap`
- `telemt-ee-before-nginx.pcap`
- `telemt-ee-after-nginx.pcap`
Практические замечания:
- `origin` снимался как прямое TLS 1.2 connection к `nginx.org`;
- `before` и `after` снимались на Telemt FakeTLS success path с реальным Telegram client;
- первый server-side FakeTLS response остаётся валидным в обоих случаях;
- сегментация раннего encrypted server flight отличается между `before` и `after`, что согласуется с лучшим использованием captured profile data;
- такой результат показывает заметный эффект на проводе без поломки success path, но не заявляет полной неотличимости от origin.
## Более сильная валидация
Для более широкой проверки повторите ту же процедуру ещё на:
1. одном CDN-backed origin;
2. одном regular nginx origin;
3. одном origin с multi-record encrypted flight и заметными ticket-like tails.
Если одно и то же направление улучшения повторится на всех трёх, уверенность в результате будет значительно выше, чем для одного origin example.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+3 -32
View File
@@ -36,11 +36,8 @@ hello2 = "ad_tag2"
On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS,
based on the ECH extension and the ordering of cipher suites,
as well as an overall unique JA3/JA4 fingerprint
that does not occur in modern browsers.
> [!IMPORTANT]
> TLS fingerprint has been fixed in latest version of clients for Desktop / Android / iOS.
> Please update your client for MTProxy Fake-TLS to work correctly.
that does not occur in modern browsers:
we have already submitted initial changes to the Telegram Desktop developers and are working on updates for other clients.
- We consider this a breakthrough aspect, which has no stable analogues today
- Based on this: if `telemt` configured correctly, **TLS mode is completely identical to real-life handshake + communication** with a specified host
@@ -157,24 +154,6 @@ Keep-Alive: timeout=60
### Why do you need a middle proxy (ME)
https://github.com/telemt/telemt/discussions/167
## How clients interact with Telegram DCs
When you register a Telegram account, it gets permanently bound to one of Telegram's data centers (DCs).
It is deciced beforehand by Telegram based on the phone number's region.
This DC becomes your **home DC**: all content you upload (photos, videos, files, messages) is stored there.
Your client authenticates on it with every connection.
For example, if your account is registered on **DC2**, your client will always connect to DC2 first.
When you open a chat with another user whose home DC is **DC5**, your client opens an additional connection to DC5 to download their media.
Those cross-DC requests are normal and happen constantly.
> [!WARNING]
> Because every session is anchored to your home DC, an outage there causes other DCs to be unavaliable.
> 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.
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
By default, an unlimited number of people can use a single link.
However, you can limit the number of unique IP addresses for each user:
@@ -182,8 +161,7 @@ However, you can limit the number of unique IP addresses for each user:
[access.user_max_unique_ips]
hello = 1
```
This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect.
At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network).
This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect. At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network).
### How to create multiple different links
1. Generate the required number of secrets using the command: `openssl rand -hex 16`.
@@ -210,13 +188,6 @@ If you need to allow connections with any domains (ignoring SNI mismatches), add
unknown_sni_action = "mask"
```
Alternatively, if you want telemt to behave like a vanilla nginx with `ssl_reject_handshake on;` on unknown SNI (emit a TLS `unrecognized_name` alert and close the connection), use:
```toml
[censorship]
unknown_sni_action = "reject_handshake"
```
This does not recover stale clients, but it makes port 443 wire-indistinguishable from a stock web server that simply does not host the requested vhost.
### How to view metrics
1. Open the configuration file: `nano /etc/telemt/telemt.toml`.
+2 -29
View File
@@ -33,12 +33,9 @@ hello = "ad_tag"
hello2 = "ad_tag2"
```
## Распознаваемость для DPI и сканеров
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах.
> [!IMPORTANT]
> Проблема с TLS отпечатком исправлена в последних версиях клиентов Telegram для Desktop / Android / iOS.
> Обновите свой клиент для корректной работы с MTProxy Fake-TLS!
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах: мы уже отправили первоначальные изменения разработчикам Telegram Desktop и работаем над обновлениями для других клиентов.
- Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов;
- Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом;
@@ -155,23 +152,6 @@ Keep-Alive: timeout=60
## Зачем нужен middle proxy (ME)
https://github.com/telemt/telemt/discussions/167
## Как клиенты взаимодействуют с дата-центрами Telegram
При регистрации аккаунта Telegram он навсегда привязывается к одному из дата-центров (DC).
Telegram заранее определяет к какому DC привязать аккаунт исходя из региона, к которому относиться номер телефона.
Этот DC становится вашим **домашним**: именно там хранится весь контент, который вы загружаете (фото, видео, файлы, сообщения).
И именно на нем клиент авторизуется при каждом подключении.
Например, если ваш аккаунт зарегистрирован на **DC2**, клиент всегда будет подключаться в первую очередь к DC2.
Когда вы открываете переписку с пользователем, чей домашний DC — **DC5**, клиент устанавливает доп. соединение с DC5, чтобы загрузить его контент.
Такие кросс-запросы к DC — это нормальная часть работы Telegram.
> [!WARNING]
> Поскольку аккаунт всегда привязан к домашнему DC, при его падении контент с других DC будет недоступен.
> Если ваш домашний DC — DC2, и DC2 лежит, вы **не сможете** достучаться и до DC5, даже если сам DC5 полностью исправен.
> У клиента просто нет валидной сессии, через которую можно было бы направить запрос.
По той же причине MTProxy достаточно иметь доступ к инфраструктуре Telegram в целом.
Cамому MTProxy всё равно, на каком DC живёт ваш аккаунт. Клиент cам договаривается о нужном DC через прокси уже после подключения.
## Что такое dd и ee в контексте MTProxy?
@@ -227,13 +207,6 @@ curl -s http://127.0.0.1:9091/v1/users | jq
unknown_sni_action = "mask"
```
Альтернатива: если вы хотите, чтобы telemt на неизвестный SNI вёл себя как обычный nginx с `ssl_reject_handshake on;` (отдавал TLS-alert `unrecognized_name` и закрывал соединение), используйте:
```toml
[censorship]
unknown_sni_action = "reject_handshake"
```
Это не пропускает старых клиентов, но делает поведение на 443-м порту неотличимым от стокового веб-сервера, у которого просто нет такого виртуального хоста.
## Как посмотреть метрики
1. Откройте файл конфигурации: `nano /etc/telemt/telemt.toml`.
@@ -27,8 +27,7 @@ cargo build --release
./target/release/telemt --version
```
For low-RAM systems, note that this repository currently uses `lto = "fat"` in release profile.
On constrained builders, a local override to `lto = "thin"` may be more practical.
For low-RAM systems, this repository already uses `lto = "thin"` in release profile.
## 3. Install binary and config
+10 -37
View File
@@ -1,36 +1,9 @@
# Installation Options
There are three options for installing Telemt:
- [Automated installation using a script](#very-quick-start).
- [Manual installation of Telemt as a service](#telemt-via-systemd).
- [Installation using Docker Compose](#telemt-via-docker-compose).
# Very quick start
### One-command installation / update on re-run
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
After starting, the script will prompt for:
- Your language (1 - English, 2 - Russian);
- Your TLS domain (press Enter for petrovich.ru).
The script checks if the port (default **443**) is free. If the port is already in use, installation will fail. You need to free up the port or use the **-p** flag with a different port to retry the installation.
To modify the scripts startup parameters, you can use the following flags:
- **-d, --domain** - TLS domain;
- **-p, --port** - server port (165535);
- **-s, --secret** - 32 hex secret;
- **-a, --ad-tag** - ad_tag;
- **-l, --lan**g - language (1/en or 2/ru);
Providing all options skips interactive prompts.
After completion, the script will provide a link for client connections:
```bash
tg://proxy?server=IP&port=PORT&secret=SECRET
```
### Installing a specific version
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
@@ -137,15 +110,15 @@ show = "*"
# === Server Binding ===
[server]
port = 443
# proxy_protocol = false # Enable if behind HAProxy/nginx with PROXY protocol
# proxy_protocol = false # Enable if behind HAProxy/nginx with PROXY protocol
# metrics_port = 9090
# metrics_listen = "127.0.0.1:9090" # Listen address for metrics (overrides metrics_port)
# metrics_whitelist = ["127.0.0.1/32", "::1/128"]
# metrics_listen = "0.0.0.0:9090" # Listen address for metrics (overrides metrics_port)
# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"]
[server.api]
enabled = true
listen = "127.0.0.1:9091"
whitelist = ["127.0.0.1/32", "::1/128"]
listen = "0.0.0.0:9091"
whitelist = ["127.0.0.0/8"]
minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000
@@ -155,9 +128,9 @@ ip = "0.0.0.0"
# === Anti-Censorship & Masking ===
[censorship]
tls_domain = "petrovich.ru" # Fake-TLS / SNI masking domain used in generated ee-links
tls_domain = "petrovich.ru"
mask = true
tls_emulation = true # Fetch real cert lengths and emulate TLS records
tls_emulation = true # Fetch real cert lengths and emulate TLS records
tls_front_dir = "tlsfront" # Cache directory for TLS emulation
[access.users]
@@ -168,9 +141,9 @@ hello = "00000000000000000000000000000000"
then Ctrl+S -> Ctrl+X to save
> [!WARNING]
> Replace the value of the `hello` parameter with the value you obtained in step 0.
> Additionally, change the value of the `tls_domain` parameter to a different website.
> Changing the `tls_domain` parameter will break all links that use the old domain!
> Replace the value of the hello parameter with the value you obtained in step 0.
> Additionally, change the value of the tls_domain parameter to a different website.
> Changing the tls_domain parameter will break all links that use the old domain!
---
+13 -39
View File
@@ -1,35 +1,9 @@
# Варианты установки
Имеется три варианта установки Telemt:
- [Автоматизированная установка с помощью скрипта](#очень-быстрый-старт).
- [Ручная установка Telemt в качестве службы](#telemt-через-systemd-вручную).
- [Установка через Docker Compose](#telemt-через-docker-compose).
# Очень быстрый старт
### Установка одной командой / обновление при повторном запуске
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
После запуска скрипт запросит:
- ваш язык (1 - English, 2 - Русский);
- ваш TLS-домен (нажмите Enter для petrovich.ru).
Во время установки скрипт проверяет, свободен ли порт (по умолчанию **443**). Если порт занят другим процессом - установка завершится с ошибкой. Для повторной установки необходимо освободить порт или указать другой через флаг **-p**.
Для изменения параметров запуска скрипта можно использовать следующие флаги:
- **-d, --domain** - TLS-домен;
- **-p, --port** - порт (165535);
- **-s, --secret** - секрет (32 hex символа);
- **-a, --ad-tag** - ad_tag;
- **-l, --lang** - язык (1/en или 2/ru).
Если заданы флаги для языка и домена, интерактивных вопросов не будет.
После завершения установки скрипт выдаст ссылку для подключения клиентов:
```bash
tg://proxy?server=IP&port=PORT&secret=SECRET
```
### Установка нужной версии
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
@@ -129,22 +103,22 @@ tls = true
[general.links]
show = "*"
# show = ["alice", "bob"] # Показывать ссылки только для alice и bob
# show = "*" # Показывать ссылки для всех пользователей
# public_host = "proxy.example.com" # Хост (IP-адрес или домен) для ссылок tg://
# public_port = 443 # Порт для ссылок tg:// (по умолчанию: server.port)
# show = "*"              # Показывать ссылки для всех пользователей
# public_host = "proxy.example.com"  # Хост (IP-адрес или домен) для ссылок tg://
# public_port = 443                  # Порт для ссылок tg:// (по умолчанию: server.port)
# === Привязка сервера ===
[server]
port = 443
# proxy_protocol = false # Включите, если сервер находится за HAProxy/nginx с протоколом PROXY
# proxy_protocol = false           # Включите, если сервер находится за HAProxy/nginx с протоколом PROXY
# metrics_port = 9090
# metrics_listen = "127.0.0.1:9090" # Адрес прослушивания для метрик (переопределяет metrics_port)
# metrics_whitelist = ["127.0.0.1/32", "::1/128"]
# metrics_listen = "0.0.0.0:9090"  # Адрес прослушивания для метрик (переопределяет metrics_port)
# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"]
[server.api]
enabled = true
listen = "127.0.0.1:9091"
whitelist = ["127.0.0.1/32", "::1/128"]
listen = "0.0.0.0:9091"
whitelist = ["127.0.0.0/8"]
minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000
@@ -154,9 +128,9 @@ ip = "0.0.0.0"
# === Обход блокировок и маскировка ===
[censorship]
tls_domain = "petrovich.ru" # Домен Fake-TLS / SNI, который будет использоваться в сгенерированных ee-ссылках
tls_domain = "petrovich.ru"
mask = true
tls_emulation = true # Получить реальную длину сертификата и эмулировать запись TLS
tls_emulation = true # Получить реальную длину сертификата и эмулировать запись TLS
tls_front_dir = "tlsfront" # Директория кэша для эмуляции TLS
[access.users]
@@ -167,9 +141,9 @@ hello = "00000000000000000000000000000000"
Затем нажмите Ctrl+S -> Ctrl+X, чтобы сохранить
> [!WARNING]
> Замените значение параметра `hello` на значение, которое вы получили в пункте 0.
> Так же замените значение параметра `tls_domain` на другой сайт.
> Изменение параметра `tls_domain` сделает нерабочими все ссылки, использующие старый домен!
> Замените значение параметра hello на значение, которое вы получили в пункте 0.
> Так же замените значение параметра tls_domain на другой сайт.
> Изменение параметра tls_domain сделает нерабочими все ссылки, использующие старый домен!
---
+1 -1
View File
@@ -163,7 +163,7 @@ PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
---
## Step 2. Installing telemt on Server B (conditionally Netherlands)
Installation and configuration are described [here](https://github.com/telemt/telemt/blob/main/docs/Quick_start/QUICK_START_GUIDE.en.md) or [here](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
Installation and configuration are described [here](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_GUIDE.ru.md) or [here](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
It is assumed that telemt expects connections on port `443\tcp`.
In the telemt config, you must enable the `Proxy` protocol and restrict connections to it only through the tunnel.
+1 -1
View File
@@ -166,7 +166,7 @@ PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
## Шаг 2. Установка telemt на Сервере B (_условно Нидерланды_)
Установка и настройка описаны [здесь](https://github.com/telemt/telemt/blob/main/docs/Quick_start/QUICK_START_GUIDE.ru.md) или [здесь](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
Установка и настройка описаны [здесь](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_GUIDE.ru.md) или [здесь](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
Подразумевается что telemt ожидает подключения на порту `443\tcp`.
В конфиге telemt необходимо включить протокол `Proxy` и ограничить подключения к нему только через туннель.
-273
View File
@@ -1,273 +0,0 @@
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
## Concept
- **Server A** (_e.g., RU_):\
Entry point, accepts Telegram proxy user traffic via **Xray** (port `443\tcp`)\
and sends it through the tunnel to Server **B**.\
Public port for Telegram clients — `443\tcp`
- **Server B** (_e.g., NL_):\
Exit point, runs the **Xray server** (to terminate the tunnel entry point) and **telemt**.\
The server must have unrestricted access to Telegram Data Centers.\
Public port for VLESS/REALITY (incoming) — `443\tcp`\
Internal telemt port (where decrypted Xray traffic ends up) — `8443\tcp`
The tunnel works over the `VLESS-XTLS-Reality` (or `VLESS/xhttp/reality`) protocol. The original client IP address is preserved thanks to the PROXYv2 protocol, which Xray on Server A dynamically injects via a local loopback before wrapping the traffic into Reality, transparently delivering the real IPs to telemt on Server B.
---
## Step 1. Setup Xray Tunnel (A <-> B)
You must install **Xray-core** (version 1.8.4 or newer recommended) on both servers.
Official installation script (run on both servers):
```bash
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
```
### Key and Parameter Generation (Run Once)
For configuration, you need a unique UUID and Xray Reality keys. Run on any server with Xray installed:
1. **Client UUID:**
```bash
xray uuid
# Save the output (e.g.: 12345678-abcd-1234-abcd-1234567890ab) — this is <XRAY_UUID>
```
2. **X25519 Keypair (Private & Public) for Reality:**
```bash
xray x25519
# Save the Private key (<SERVER_B_PRIVATE_KEY>) and Public key (<SERVER_B_PUBLIC_KEY>)
```
3. **Short ID (Reality identifier):**
```bash
openssl rand -hex 8
# Save the output (e.g.: abc123def456) — this is <SHORT_ID>
```
4. **Random Path (for xhttp):**
```bash
openssl rand -hex 16
# Save the output (e.g., 0123456789abcdef0123456789abcdef) to replace <YOUR_RANDOM_PATH> in configs
```
---
### Configuration for Server B (_EU_):
Create or edit the file `/usr/local/etc/xray/config.json`.
This Xray instance will listen on the public `443` port and proxy valid Reality traffic, while routing "disguised" traffic (e.g., direct web browser scans) to `yahoo.com`.
```bash
nano /usr/local/etc/xray/config.json
```
File content:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "vless-in",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "<XRAY_UUID>"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"dest": "yahoo.com:443",
"serverNames": [
"yahoo.com"
],
"privateKey": "<SERVER_B_PRIVATE_KEY>",
"shortIds": [
"<SHORT_ID>"
]
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>",
"mode": "auto"
}
}
}
],
"outbounds": [
{
"tag": "tunnel-to-telemt",
"protocol": "freedom",
"settings": {
"destination": "127.0.0.1:8443"
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": [
"vless-in"
],
"outboundTag": "tunnel-to-telemt"
}
]
}
}
```
Open the firewall port (if enabled):
```bash
sudo ufw allow 443/tcp
```
Restart and setup Xray to run at boot:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
### Configuration for Server A (_RU_):
Similarly, edit `/usr/local/etc/xray/config.json`.
Here Xray acts as the public entry point: it listens on `443\tcp`, uses a local loopback (via internal port `10444`) to prepend the `PROXYv2` header, and encapsulates the payload via Reality to Server B, instructing Server B to deliver it to its *local* `127.0.0.1:8443` port (where telemt will listen).
```bash
nano /usr/local/etc/xray/config.json
```
File content:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "public-in",
"port": 443,
"listen": "0.0.0.0",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 10444,
"network": "tcp"
}
},
{
"tag": "tunnel-in",
"port": 10444,
"listen": "127.0.0.1",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 8443,
"network": "tcp"
}
}
],
"outbounds": [
{
"tag": "local-injector",
"protocol": "freedom",
"settings": {
"proxyProtocol": 2
}
},
{
"tag": "vless-out",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "<PUBLIC_IP_SERVER_B>",
"port": 443,
"users": [
{
"id": "<XRAY_UUID>",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"serverName": "yahoo.com",
"publicKey": "<SERVER_B_PUBLIC_KEY>",
"shortId": "<SHORT_ID>",
"spiderX": "/",
"fingerprint": "chrome"
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>"
}
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": ["public-in"],
"outboundTag": "local-injector"
},
{
"type": "field",
"inboundTag": ["tunnel-in"],
"outboundTag": "vless-out"
}
]
}
}
```
*Replace `<PUBLIC_IP_SERVER_B>` with the public IP address of Server B.*
Open the firewall port for clients (if enabled):
```bash
sudo ufw allow 443/tcp
```
Restart and setup Xray to run at boot:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
## Step 2. Install telemt on Server B (_EU_)
telemt installation is heavily covered in the [Quick Start Guide](../Quick_start/QUICK_START_GUIDE.en.md).
By contrast to standard setups, telemt must listen strictly _locally_ (since Xray occupies the public `443` interface) and must expect `PROXYv2` packets.
Edit the configuration file (`config.toml`) on Server B accordingly:
```toml
[server]
port = 8443
listen_addr_ipv4 = "127.0.0.1"
proxy_protocol = true
[general.links]
show = "*"
public_host = "<FQDN_OR_IP_SERVER_A>"
public_port = 443
```
- Address `127.0.0.1` and `port = 8443` instructs the core proxy router to process connections unpacked locally via Xray-server.
- `proxy_protocol = true` commands telemt to parse the injected PROXY header (from Server A's Xray local loopback) and log genuine end-user IPs.
- Under `public_host`, place Server A's public IP address or FQDN to ensure working links are generated for Telegram users.
Restart `telemt`. Your server is now robust against DPI scanners, passing traffic optimally.
-272
View File
@@ -1,272 +0,0 @@
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
## Концепция
- **Сервер A** (_РФ_):\
Точка входа, принимает трафик пользователей Telegram-прокси напрямую через **Xray** (порт `443\tcp`)\
и отправляет его в туннель на Сервер **B**.\
Порт для клиентов Telegram — `443\tcp`
- **Сервер B** (_условно Нидерланды_):\
Точка выхода, на нем работает **Xray-сервер** (принимает подключения точки входа) и **telemt**.\
На сервере должен быть неограниченный доступ до серверов Telegram.\
Порт для VLESS/REALITY (вход) — `443\tcp`\
Внутренний порт telemt (куда пробрасывается трафик) — `8443\tcp`
Туннель работает по протоколу VLESS-XTLS-Reality (или VLESS/xhttp/reality). Оригинальный IP-адрес клиента сохраняется благодаря протоколу PROXYv2, который Xray на Сервере А добавляет через локальный loopback перед упаковкой в туннель, благодаря чему прозрачно доходит до telemt.
---
## Шаг 1. Настройка туннеля Xray (A <-> B)
На обоих серверах необходимо установить **Xray-core** (рекомендуется версия 1.8.4 или новее).
Официальный скрипт установки (выполнить на обоих серверах):
```bash
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
```
### Генерация ключей и параметров (выполнить один раз)
Для конфигурации потребуются уникальные ID и ключи Xray Reality. Выполните на любом сервере с установленным Xray:
1. **UUID клиента:**
```bash
xray uuid
# Сохраните вывод (например: 12345678-abcd-1234-abcd-1234567890ab) — это <XRAY_UUID>
```
2. **Пара ключей X25519 (Private & Public) для Reality:**
```bash
xray x25519
# Сохраните Private key (<SERVER_B_PRIVATE_KEY>) и Public key (<SERVER_B_PUBLIC_KEY>)
```
3. **Short ID (идентификатор Reality):**
```bash
openssl rand -hex 8
# Сохраните вывод (например: abc123def456) — это <SHORT_ID>
```
4. **Random Path (путь для xhttp):**
```bash
openssl rand -hex 16
# Сохраните вывод (например, 0123456789abcdef0123456789abcdef), чтобы заменить <YOUR_RANDOM_PATH> в конфигах
```
---
### Конфигурация Сервера B (_Нидерланды_):
Создаем или редактируем файл `/usr/local/etc/xray/config.json`.
Этот Xray-сервер будет слушать порт `443` и прозрачно пропускать валидный Reality трафик дальше, а "замаскированный" трафик (например, если кто-то стучится в лоб веб-браузером) пойдет на `yahoo.com`.
```bash
nano /usr/local/etc/xray/config.json
```
Содержимое файла:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "vless-in",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "<XRAY_UUID>"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"dest": "yahoo.com:443",
"serverNames": [
"yahoo.com"
],
"privateKey": "<SERVER_B_PRIVATE_KEY>",
"shortIds": [
"<SHORT_ID>"
]
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>",
"mode": "auto"
}
}
}
],
"outbounds": [
{
"tag": "tunnel-to-telemt",
"protocol": "freedom",
"settings": {
"destination": "127.0.0.1:8443"
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": [
"vless-in"
],
"outboundTag": "tunnel-to-telemt"
}
]
}
}
```
Открываем порт на фаерволе (если включен):
```bash
sudo ufw allow 443/tcp
```
Перезапускаем Xray:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
### Конфигурация Сервера A (_РФ_):
Аналогично, редактируем `/usr/local/etc/xray/config.json`.
Здесь Xray выступает публичной точкой: он принимает трафик на внешний порт `443\tcp`, пропускает через локальный loopback (порт `10444`) для добавления PROXYv2-заголовка, и упаковывает в Reality до Сервера B, прося тот доставить данные на *свой локальный* порт `127.0.0.1:8443` (именно там будет слушать telemt).
```bash
nano /usr/local/etc/xray/config.json
```
Содержимое файла:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "public-in",
"port": 443,
"listen": "0.0.0.0",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 10444,
"network": "tcp"
}
},
{
"tag": "tunnel-in",
"port": 10444,
"listen": "127.0.0.1",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 8443,
"network": "tcp"
}
}
],
"outbounds": [
{
"tag": "local-injector",
"protocol": "freedom",
"settings": {
"proxyProtocol": 2
}
},
{
"tag": "vless-out",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "<PUBLIC_IP_SERVER_B>",
"port": 443,
"users": [
{
"id": "<XRAY_UUID>",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"serverName": "yahoo.com",
"publicKey": "<SERVER_B_PUBLIC_KEY>",
"shortId": "<SHORT_ID>",
"spiderX": "/",
"fingerprint": "chrome"
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>"
}
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": ["public-in"],
"outboundTag": "local-injector"
},
{
"type": "field",
"inboundTag": ["tunnel-in"],
"outboundTag": "vless-out"
}
]
}
}
```
*Замените `<PUBLIC_IP_SERVER_B>` на внешний IP-адрес Сервера B.*
Открываем порт на фаерволе для клиентов:
```bash
sudo ufw allow 443/tcp
```
Перезапускаем Xray:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
## Шаг 2. Установка и настройка telemt на Сервере B (_Нидерланды_)
Установка telemt описана [в основной инструкции](../Quick_start/QUICK_START_GUIDE.ru.md).
Отличие в том, что telemt должен слушать *внутренний* порт (так как 443 занят Xray-сервером), а также ожидать `PROXY` протокол из Xray туннеля.
В конфиге `config.toml` прокси (на Сервере B) укажите:
```toml
[server]
port = 8443
listen_addr_ipv4 = "127.0.0.1"
proxy_protocol = true
[general.links]
show = "*"
public_host = "<FQDN_OR_IP_SERVER_A>"
public_port = 443
```
- `port = 8443` и `listen_addr_ipv4 = "127.0.0.1"` означают, что telemt принимает подключения только изнутри (приходящие от локального Xray-процесса).
- `proxy_protocol = true` заставляет telemt парсить PROXYv2-заголовок (который добавил Xray на Сервере A через loopback), восстанавливая IP-адрес конечного пользователя (РФ).
- В `public_host` укажите публичный IP-адрес или домен Сервера A, чтобы ссылки на подключение генерировались корректно.
Перезапустите `telemt`, и клиенты смогут подключаться по выданным ссылкам.
Regular → Executable
+11 -1
View File
@@ -1 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 150 30" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><path d="M150,15c0,8.279 -6.721,15 -15,15l-120,0c-8.279,0 -15,-6.721 -15,-15c0,-8.279 6.721,-15 15,-15l120,0c8.279,0 15,6.721 15,15Z" style="fill:#24a1ed;"/><g transform="matrix(20.833333,0,0,20.833333,111.464184,22.329305)"></g><text x="39.666px" y="22.329px" style="font-family:'ArialMT', 'Arial', sans-serif;font-size:20.833px;fill:#fff;">Join us!</text></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="250" height="50" viewBox="0 0 250 50">
<defs>
<style>
.cls-1 {
fill: #1d98dc;
}
</style>
</defs>
<rect id="Прямоугольник_скругл._углы_1" data-name="Прямоугольник, скругл. углы 1" class="cls-1" width="250" height="50" rx="25" ry="25"/>
<image id="Join_us_" data-name="Join us!" x="53" y="12" width="144" height="24" xlink:href="data:img/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJAAAAAYCAYAAAAVpXQNAAAFnElEQVRoge2aS2hdRRjHf2Pro1aRW1BBpGpLUQRbNRbxsbCSSKWCD7gVFUFEko1gdykaxQeU1NdGEBpEXLjQdOPCLqSh4koXLUhXUk1a8YFQSVqlYm3tJ1/5bpl8mTn33Nxzb25q/nDbnDMzZ+Z88/+ec4KI0EFcDdwM/A183cmJlrBAUAJ16LdOREZEZJ+IfFTRHLtkNtZ0UmoiMm6zTYpI3/+ZoyprJ3vdC5ZXOEcALgT6gacAFfgNwEXAsQrn6QpEZBio21xK1F3AHYvtPapCCGHKeasZ/acKAl0KXAM8CjwNrAMucX3+6fYLL6HjaItAy4DVwHrgceARYIW16YNPAZfbtdJ2arHtZwhhp7mtuq1/qAeW1XNolUCrzDXdbRZng2vfB3wFPAncaPdOAN8vRuGEELb2wDJ6GmUItMJigAeAe4H7jEgxfgXeM/I8B6yN2v5YrARaQnMUEUgD4NuALcDtwK2Zfp8BO4DfgXfNncX4DfhhaS/OC0yZMTmHC9xbqbXZBIwAHwNjwLMZ8hwCtgFPACeB3QnyKL6zX9chIoMiMipzoWn5cFEZIJG2Dif6zCorRPfrIrLXjR9tp+zgU+iCfoOu72CmX5/JYNr1n7b75coWVl9ZLSLPi8gnIvKziJxOCD3GByKy0cZuFpEjmX6nROQVEbmim3UgE87+Ju/QwBxi0AaBEmuMMT1fErnntEWgjFJ5pMZNxvJo1IE+BO4Crk+k3x4HgLeAzy04fsbcVi3TXyvQh4G/WpTXvGGas7+F8SrMs1lXBXOPAkmNN9SsnjTQ7lzzha0xqTQt4mwary5sI3BTE/IoAd4GHgM+tesXgPcLyKM4CkwC/3ZJOLqWcXdbXetAMFgC4MmiJOqvYAm6MRNacIzm2+769C9UVdvkExNcY5qtIYIVS3PKNONvLLfM6jrgYWCz/X2lVZC1nvMN8CrwpdV0VpqgXkrEUB4/WgZ2pjIpFGPQBXljIYRZ9ZsQggphu4jo/6NR06Btfjs4EEIYcPPtNLcVb1y/WfNuo+YUXuWz2633QMHa5hBICXAcOAi8AdwDPAS8Duyxe1usvqPkucqE/nIJ8ih+MivULdSjeWYS2h8LaqcTVBUWKKe5Y+66yGp3Ep4Ag+2eJ6bS+G/t56Ex0ptuk4pw2txXV2DmOXYNE2ZtijARjampazENnC9yFsxX4heEQCoPERmLrKGSZ9LuTXhrVAZlrAgWH420QB7Fn2bZugW/KWWIUOnG5ghbgsjdxPYE0ZVQ440UvpW1lCWQuq5bWnzJoxlL1g4WyvSfN1AyW5w2lFGgUSuBpFxbrAhnx7ZigVqFEuhIxYKf9VL6iUF06bW8DNm8kBbLoW/bihRC0AB6rWVdPlbss3KDx1b7bCc0rFhZAq10p+uvAe/Y3zkcatLeKnyMM8tFmZuISVRUj2kgDpxnHCF7DfG7FQW+LQXFGvNZQrHKJxUWVxZiPgTaZtnZiBUSU1bmZAfSVO+bUwFfnO3UrGiWhPn6vszYXkRM7r5U3SpRLpjTnotxTAG9TJvXq0oeITwoIr+IyDYRWebaNojIFyJyJipzHxeRO9s4shi244jGtS+9T6e0wwTkz3bGY2HruMzz1iSeFaP0WVhGzqWOIgrG+zVr9lSP2uvuqGHOUUb0TnvjsdbWnzj+qWXWMG3zlSbQehHZJCIXZ9p1U3aIyDGbQD9/vKzCM6+sUBKCrjcZm0JSm12/hSaQX08O4+5+ikBlMOrm93JVEpV2YQetEn0y067m70Uzn3vsfOxEq0IqiSENAHNdrZYxkKqaJjBlxxztVqA7DovPmn0VmUrR54PdIYRsETZCrYoT8vi3UkTuF5Fr23zOcEJDCj+/SMHGeI0Uu1dY7+g1CxQ9R12Nt9DnXFKz03gbn5Jv4zOObEXeudG6iPAfDgYBtHqBCrAAAAAASUVORK5CYII="/>
</svg>

Before

Width:  |  Height:  |  Size: 804 B

After

Width:  |  Height:  |  Size: 2.5 KiB

+16 -67
View File
@@ -1,6 +1,6 @@
#![allow(clippy::too_many_arguments)]
use std::io::{Error as IoError, ErrorKind};
use std::convert::Infallible;
use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf;
use std::sync::Arc;
@@ -16,7 +16,7 @@ use tokio::net::TcpListener;
use tokio::sync::{Mutex, RwLock, watch};
use tracing::{debug, info, warn};
use crate::config::{ApiGrayAction, ProxyConfig};
use crate::config::ProxyConfig;
use crate::ip_tracker::UserIpTracker;
use crate::proxy::route_mode::RouteRuntimeController;
use crate::startup::StartupTracker;
@@ -41,8 +41,8 @@ use config_store::{current_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::{
ApiFailure, CreateUserRequest, DeleteUserResponse, HealthData, HealthReadyData,
PatchUserRequest, RotateSecretRequest, SummaryData, UserActiveIps,
ApiFailure, CreateUserRequest, DeleteUserResponse, HealthData, PatchUserRequest,
RotateSecretRequest, SummaryData, UserActiveIps,
};
use runtime_edge::{
EdgeConnectionsCacheEntry, build_runtime_connections_summary_data,
@@ -184,9 +184,7 @@ pub async fn serve(
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
.await
{
if !error.is_user() {
debug!(error = %error, "API connection error");
}
debug!(error = %error, "API connection error");
}
});
}
@@ -197,7 +195,7 @@ async fn handle(
peer: SocketAddr,
shared: Arc<ApiShared>,
config_rx: watch::Receiver<Arc<ProxyConfig>>,
) -> Result<Response<Full<Bytes>>, IoError> {
) -> Result<Response<Full<Bytes>>, Infallible> {
let request_id = shared.next_request_id();
let cfg = config_rx.borrow().clone();
let api_cfg = &cfg.server.api;
@@ -215,25 +213,14 @@ async fn handle(
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
{
return match api_cfg.gray_action {
ApiGrayAction::Api => Ok(error_response(
request_id,
ApiFailure::new(
StatusCode::FORBIDDEN,
"forbidden",
"Source IP is not allowed",
),
)),
ApiGrayAction::Ok200 => Ok(Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/html; charset=utf-8")
.body(Full::new(Bytes::new()))
.unwrap()),
ApiGrayAction::Drop => Err(IoError::new(
ErrorKind::ConnectionAborted,
"api request dropped by gray_action=drop",
)),
};
return Ok(error_response(
request_id,
ApiFailure::new(
StatusCode::FORBIDDEN,
"forbidden",
"Source IP is not allowed",
),
));
}
if !api_cfg.auth_header.is_empty() {
@@ -257,16 +244,11 @@ async fn handle(
let method = req.method().clone();
let path = req.uri().path().to_string();
let normalized_path = if path.len() > 1 {
path.trim_end_matches('/')
} else {
path.as_str()
};
let query = req.uri().query().map(str::to_string);
let body_limit = api_cfg.request_body_limit_bytes;
let result: Result<Response<Full<Bytes>>, ApiFailure> = async {
match (method.as_str(), normalized_path) {
match (method.as_str(), path.as_str()) {
("GET", "/v1/health") => {
let revision = current_revision(&shared.config_path).await?;
let data = HealthData {
@@ -275,33 +257,6 @@ async fn handle(
};
Ok(success_response(StatusCode::OK, data, revision))
}
("GET", "/v1/health/ready") => {
let revision = current_revision(&shared.config_path).await?;
let admission_open = shared.runtime_state.admission_open.load(Ordering::Relaxed);
let upstream_health = shared.upstream_manager.api_health_summary().await;
let ready = admission_open && upstream_health.healthy_total > 0;
let reason = if ready {
None
} else if !admission_open {
Some("admission_closed")
} else {
Some("no_healthy_upstreams")
};
let data = HealthReadyData {
ready,
status: if ready { "ready" } else { "not_ready" },
reason,
admission_open,
healthy_upstreams: upstream_health.healthy_total,
total_upstreams: upstream_health.configured_total,
};
let status_code = if ready {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
};
Ok(success_response(status_code, data, revision))
}
("GET", "/v1/system/info") => {
let revision = current_revision(&shared.config_path).await?;
let data = build_system_info_data(shared.as_ref(), cfg.as_ref(), &revision);
@@ -476,7 +431,7 @@ async fn handle(
Ok(success_response(status, data, revision))
}
_ => {
if let Some(user) = normalized_path.strip_prefix("/v1/users/")
if let Some(user) = path.strip_prefix("/v1/users/")
&& !user.is_empty()
&& !user.contains('/')
{
@@ -645,12 +600,6 @@ async fn handle(
),
));
}
debug!(
method = method.as_str(),
path = %path,
normalized_path = %normalized_path,
"API route not found"
);
Ok(error_response(
request_id,
ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"),
-11
View File
@@ -60,17 +60,6 @@ pub(super) struct HealthData {
pub(super) read_only: bool,
}
#[derive(Serialize)]
pub(super) struct HealthReadyData {
pub(super) ready: bool,
pub(super) status: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub(super) reason: Option<&'static str>,
pub(super) admission_open: bool,
pub(super) healthy_upstreams: usize,
pub(super) total_upstreams: usize,
}
#[derive(Serialize)]
pub(super) struct SummaryData {
pub(super) uptime_seconds: f64,
+1 -13
View File
@@ -452,11 +452,7 @@ fn build_user_links(
startup_detected_ip_v6: Option<IpAddr>,
) -> UserLinks {
let hosts = resolve_link_hosts(cfg, startup_detected_ip_v4, startup_detected_ip_v6);
let port = cfg
.general
.links
.public_port
.unwrap_or(resolve_default_link_port(cfg));
let port = cfg.general.links.public_port.unwrap_or(cfg.server.port);
let tls_domains = resolve_tls_domains(cfg);
let mut classic = Vec::new();
@@ -494,14 +490,6 @@ fn build_user_links(
}
}
fn resolve_default_link_port(cfg: &ProxyConfig) -> u16 {
cfg.server
.listeners
.first()
.and_then(|listener| listener.port)
.unwrap_or(cfg.server.port)
}
fn resolve_link_hosts(
cfg: &ProxyConfig,
startup_detected_ip_v4: Option<IpAddr>,
+2 -71
View File
@@ -6,15 +6,12 @@
//! - `reload [--pid-file PATH]` - Reload configuration (SIGHUP)
//! - `status [--pid-file PATH]` - Check daemon status
//! - `run [OPTIONS] [config.toml]` - Run in foreground (default behavior)
//! - `healthcheck [OPTIONS] [config.toml]` - Run control-plane health probe
use rand::RngExt;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use crate::healthcheck::{self, HealthcheckMode};
#[cfg(unix)]
use crate::daemon::{self, DEFAULT_PID_FILE, DaemonOptions};
@@ -31,8 +28,6 @@ pub enum Subcommand {
Reload,
/// Check daemon status (`status` subcommand).
Status,
/// Run health probe and exit with status code.
Healthcheck,
/// Fire-and-forget setup (`--init`).
Init,
}
@@ -43,8 +38,6 @@ pub struct ParsedCommand {
pub subcommand: Subcommand,
pub pid_file: PathBuf,
pub config_path: String,
pub healthcheck_mode: HealthcheckMode,
pub healthcheck_mode_invalid: Option<String>,
#[cfg(unix)]
pub daemon_opts: DaemonOptions,
pub init_opts: Option<InitOptions>,
@@ -59,8 +52,6 @@ impl Default for ParsedCommand {
#[cfg(not(unix))]
pid_file: PathBuf::from("/var/run/telemt.pid"),
config_path: "config.toml".to_string(),
healthcheck_mode: HealthcheckMode::Liveness,
healthcheck_mode_invalid: None,
#[cfg(unix)]
daemon_opts: DaemonOptions::default(),
init_opts: None,
@@ -100,9 +91,6 @@ pub fn parse_command(args: &[String]) -> ParsedCommand {
"status" => {
cmd.subcommand = Subcommand::Status;
}
"healthcheck" => {
cmd.subcommand = Subcommand::Healthcheck;
}
"run" => {
cmd.subcommand = Subcommand::Run;
#[cfg(unix)]
@@ -125,35 +113,7 @@ pub fn parse_command(args: &[String]) -> ParsedCommand {
while i < args.len() {
match args[i].as_str() {
// Skip subcommand names
"start" | "stop" | "reload" | "status" | "run" | "healthcheck" => {}
"--mode" => {
i += 1;
if i < args.len() {
match HealthcheckMode::from_cli_arg(&args[i]) {
Some(mode) => {
cmd.healthcheck_mode = mode;
cmd.healthcheck_mode_invalid = None;
}
None => {
cmd.healthcheck_mode_invalid = Some(args[i].clone());
}
}
} else {
cmd.healthcheck_mode_invalid = Some(String::new());
}
}
s if s.starts_with("--mode=") => {
let raw = s.trim_start_matches("--mode=");
match HealthcheckMode::from_cli_arg(raw) {
Some(mode) => {
cmd.healthcheck_mode = mode;
cmd.healthcheck_mode_invalid = None;
}
None => {
cmd.healthcheck_mode_invalid = Some(raw.to_string());
}
}
}
"start" | "stop" | "reload" | "status" | "run" => {}
// PID file option (for stop/reload/status)
"--pid-file" => {
i += 1;
@@ -192,20 +152,6 @@ pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
Subcommand::Stop => Some(cmd_stop(&cmd.pid_file)),
Subcommand::Reload => Some(cmd_reload(&cmd.pid_file)),
Subcommand::Status => Some(cmd_status(&cmd.pid_file)),
Subcommand::Healthcheck => {
if let Some(invalid_mode) = cmd.healthcheck_mode_invalid.as_ref() {
if invalid_mode.is_empty() {
eprintln!("[telemt] Missing value for --mode (supported: liveness, ready)");
} else {
eprintln!(
"[telemt] Invalid --mode value '{invalid_mode}' (supported: liveness, ready)"
);
}
Some(2)
} else {
Some(healthcheck::run(&cmd.config_path, cmd.healthcheck_mode))
}
}
Subcommand::Init => {
if let Some(opts) = cmd.init_opts.clone() {
match run_init(opts) {
@@ -231,20 +177,6 @@ pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
eprintln!("[telemt] Subcommand not supported on this platform");
Some(1)
}
Subcommand::Healthcheck => {
if let Some(invalid_mode) = cmd.healthcheck_mode_invalid.as_ref() {
if invalid_mode.is_empty() {
eprintln!("[telemt] Missing value for --mode (supported: liveness, ready)");
} else {
eprintln!(
"[telemt] Invalid --mode value '{invalid_mode}' (supported: liveness, ready)"
);
}
Some(2)
} else {
Some(healthcheck::run(&cmd.config_path, cmd.healthcheck_mode))
}
}
Subcommand::Init => {
if let Some(opts) = cmd.init_opts.clone() {
match run_init(opts) {
@@ -666,17 +598,16 @@ secure = false
tls = true
[server]
port = {port}
listen_addr_ipv4 = "0.0.0.0"
listen_addr_ipv6 = "::"
[[server.listeners]]
ip = "0.0.0.0"
port = {port}
# reuse_allow = false # Set true only when intentionally running multiple telemt instances on same port
[[server.listeners]]
ip = "::"
port = {port}
[timeouts]
client_first_byte_idle_secs = 300
+1 -31
View File
@@ -21,8 +21,6 @@ const DEFAULT_ME_ADAPTIVE_FLOOR_MAX_ACTIVE_WRITERS_PER_CORE: u16 = 64;
const DEFAULT_ME_ADAPTIVE_FLOOR_MAX_WARM_WRITERS_PER_CORE: u16 = 64;
const DEFAULT_ME_ADAPTIVE_FLOOR_MAX_ACTIVE_WRITERS_GLOBAL: u32 = 256;
const DEFAULT_ME_ADAPTIVE_FLOOR_MAX_WARM_WRITERS_GLOBAL: u32 = 256;
const DEFAULT_ME_ROUTE_BACKPRESSURE_ENABLED: bool = false;
const DEFAULT_ME_ROUTE_FAIRSHARE_ENABLED: bool = false;
const DEFAULT_ME_WRITER_CMD_CHANNEL_CAPACITY: usize = 4096;
const DEFAULT_ME_ROUTE_CHANNEL_CAPACITY: usize = 768;
const DEFAULT_ME_C2ME_CHANNEL_CAPACITY: usize = 1024;
@@ -212,7 +210,7 @@ pub(crate) fn default_proxy_protocol_header_timeout_ms() -> u64 {
}
pub(crate) fn default_proxy_protocol_trusted_cidrs() -> Vec<IpNetwork> {
vec!["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()]
Vec::new()
}
pub(crate) fn default_server_max_connections() -> u32 {
@@ -531,14 +529,6 @@ pub(crate) fn default_me_route_backpressure_base_timeout_ms() -> u64 {
25
}
pub(crate) fn default_me_route_backpressure_enabled() -> bool {
DEFAULT_ME_ROUTE_BACKPRESSURE_ENABLED
}
pub(crate) fn default_me_route_fairshare_enabled() -> bool {
DEFAULT_ME_ROUTE_FAIRSHARE_ENABLED
}
pub(crate) fn default_me_route_backpressure_high_timeout_ms() -> u64 {
120
}
@@ -625,26 +615,6 @@ pub(crate) fn default_mask_relay_max_bytes() -> usize {
32 * 1024
}
#[cfg(not(test))]
pub(crate) fn default_mask_relay_timeout_ms() -> u64 {
60_000
}
#[cfg(test)]
pub(crate) fn default_mask_relay_timeout_ms() -> u64 {
200
}
#[cfg(not(test))]
pub(crate) fn default_mask_relay_idle_timeout_ms() -> u64 {
5_000
}
#[cfg(test)]
pub(crate) fn default_mask_relay_idle_timeout_ms() -> u64 {
100
}
pub(crate) fn default_mask_classifier_prefetch_timeout_ms() -> u64 {
5
}
+4 -46
View File
@@ -17,9 +17,8 @@
//! | `network` | `dns_overrides` | Applied immediately |
//! | `access` | All user/quota fields | Effective immediately |
//!
//! Fields that require re-binding sockets (`server.listeners`, legacy
//! `server.port`, `censorship.*`, `network.*`, `use_middle_proxy`) are **not**
//! applied; a warning is emitted.
//! Fields that require re-binding sockets (`server.port`, `censorship.*`,
//! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted.
//! Non-hot changes are never mixed into the runtime config snapshot.
use std::collections::BTreeSet;
@@ -86,8 +85,6 @@ pub struct HotFields {
pub telemetry_user_enabled: bool,
pub telemetry_me_level: MeTelemetryLevel,
pub me_socks_kdf_policy: MeSocksKdfPolicy,
pub me_route_backpressure_enabled: bool,
pub me_route_fairshare_enabled: bool,
pub me_floor_mode: MeFloorMode,
pub me_adaptive_floor_idle_secs: u64,
pub me_adaptive_floor_min_writers_single_endpoint: u8,
@@ -123,9 +120,6 @@ pub struct HotFields {
pub user_max_tcp_conns_global_each: usize,
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
pub user_data_quota: std::collections::HashMap<String, u64>,
pub user_rate_limits: std::collections::HashMap<String, crate::config::RateLimitBps>,
pub cidr_rate_limits:
std::collections::HashMap<ipnetwork::IpNetwork, crate::config::RateLimitBps>,
pub user_max_unique_ips: std::collections::HashMap<String, usize>,
pub user_max_unique_ips_global_each: usize,
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,
@@ -189,8 +183,6 @@ impl HotFields {
telemetry_user_enabled: cfg.general.telemetry.user_enabled,
telemetry_me_level: cfg.general.telemetry.me_level,
me_socks_kdf_policy: cfg.general.me_socks_kdf_policy,
me_route_backpressure_enabled: cfg.general.me_route_backpressure_enabled,
me_route_fairshare_enabled: cfg.general.me_route_fairshare_enabled,
me_floor_mode: cfg.general.me_floor_mode,
me_adaptive_floor_idle_secs: cfg.general.me_adaptive_floor_idle_secs,
me_adaptive_floor_min_writers_single_endpoint: cfg
@@ -252,8 +244,6 @@ impl HotFields {
user_max_tcp_conns_global_each: cfg.access.user_max_tcp_conns_global_each,
user_expirations: cfg.access.user_expirations.clone(),
user_data_quota: cfg.access.user_data_quota.clone(),
user_rate_limits: cfg.access.user_rate_limits.clone(),
cidr_rate_limits: cfg.access.cidr_rate_limits.clone(),
user_max_unique_ips: cfg.access.user_max_unique_ips.clone(),
user_max_unique_ips_global_each: cfg.access.user_max_unique_ips_global_each,
user_max_unique_ips_mode: cfg.access.user_max_unique_ips_mode,
@@ -309,7 +299,6 @@ fn listeners_equal(
}
lhs.iter().zip(rhs.iter()).all(|(a, b)| {
a.ip == b.ip
&& a.port == b.port
&& a.announce == b.announce
&& a.announce_ip == b.announce_ip
&& a.proxy_protocol == b.proxy_protocol
@@ -317,14 +306,6 @@ fn listeners_equal(
})
}
fn resolve_default_link_port(cfg: &ProxyConfig) -> u16 {
cfg.server
.listeners
.first()
.and_then(|listener| listener.port)
.unwrap_or(cfg.server.port)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
struct WatchManifest {
files: BTreeSet<PathBuf>,
@@ -533,8 +514,6 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
new.general.me_route_backpressure_high_timeout_ms;
cfg.general.me_route_backpressure_high_watermark_pct =
new.general.me_route_backpressure_high_watermark_pct;
cfg.general.me_route_backpressure_enabled = new.general.me_route_backpressure_enabled;
cfg.general.me_route_fairshare_enabled = new.general.me_route_fairshare_enabled;
cfg.general.me_reader_route_data_wait_ms = new.general.me_reader_route_data_wait_ms;
cfg.general.me_d2c_flush_batch_max_frames = new.general.me_d2c_flush_batch_max_frames;
cfg.general.me_d2c_flush_batch_max_bytes = new.general.me_d2c_flush_batch_max_bytes;
@@ -556,8 +535,6 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
cfg.access.user_max_tcp_conns_global_each = new.access.user_max_tcp_conns_global_each;
cfg.access.user_expirations = new.access.user_expirations.clone();
cfg.access.user_data_quota = new.access.user_data_quota.clone();
cfg.access.user_rate_limits = new.access.user_rate_limits.clone();
cfg.access.cidr_rate_limits = new.access.cidr_rate_limits.clone();
cfg.access.user_max_unique_ips = new.access.user_max_unique_ips.clone();
cfg.access.user_max_unique_ips_global_each = new.access.user_max_unique_ips_global_each;
cfg.access.user_max_unique_ips_mode = new.access.user_max_unique_ips_mode;
@@ -583,7 +560,6 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
if old.server.api.enabled != new.server.api.enabled
|| old.server.api.listen != new.server.api.listen
|| old.server.api.whitelist != new.server.api.whitelist
|| old.server.api.gray_action != new.server.api.gray_action
|| old.server.api.auth_header != new.server.api.auth_header
|| old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes
|| old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
@@ -635,8 +611,6 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|| old.censorship.mask_shape_above_cap_blur_max_bytes
!= new.censorship.mask_shape_above_cap_blur_max_bytes
|| old.censorship.mask_relay_max_bytes != new.censorship.mask_relay_max_bytes
|| old.censorship.mask_relay_timeout_ms != new.censorship.mask_relay_timeout_ms
|| old.censorship.mask_relay_idle_timeout_ms != new.censorship.mask_relay_idle_timeout_ms
|| old.censorship.mask_classifier_prefetch_timeout_ms
!= new.censorship.mask_classifier_prefetch_timeout_ms
|| old.censorship.mask_timing_normalization_enabled
@@ -1059,8 +1033,6 @@ fn log_changes(
!= new_hot.me_route_backpressure_high_timeout_ms
|| old_hot.me_route_backpressure_high_watermark_pct
!= new_hot.me_route_backpressure_high_watermark_pct
|| old_hot.me_route_backpressure_enabled != new_hot.me_route_backpressure_enabled
|| old_hot.me_route_fairshare_enabled != new_hot.me_route_fairshare_enabled
|| old_hot.me_reader_route_data_wait_ms != new_hot.me_reader_route_data_wait_ms
|| old_hot.me_health_interval_ms_unhealthy != new_hot.me_health_interval_ms_unhealthy
|| old_hot.me_health_interval_ms_healthy != new_hot.me_health_interval_ms_healthy
@@ -1068,12 +1040,10 @@ fn log_changes(
|| old_hot.me_warn_rate_limit_ms != new_hot.me_warn_rate_limit_ms
{
info!(
"config reload: me_route_backpressure: enabled={} base={}ms high={}ms watermark={}%; me_route_fairshare_enabled={}; me_reader_route_data_wait_ms={}; me_health_interval: unhealthy={}ms healthy={}ms; me_admission_poll={}ms; me_warn_rate_limit={}ms",
new_hot.me_route_backpressure_enabled,
"config reload: me_route_backpressure: base={}ms high={}ms watermark={}%; me_reader_route_data_wait_ms={}; me_health_interval: unhealthy={}ms healthy={}ms; me_admission_poll={}ms; me_warn_rate_limit={}ms",
new_hot.me_route_backpressure_base_timeout_ms,
new_hot.me_route_backpressure_high_timeout_ms,
new_hot.me_route_backpressure_high_watermark_pct,
new_hot.me_route_fairshare_enabled,
new_hot.me_reader_route_data_wait_ms,
new_hot.me_health_interval_ms_unhealthy,
new_hot.me_health_interval_ms_healthy,
@@ -1147,7 +1117,7 @@ fn log_changes(
.general
.links
.public_port
.unwrap_or(resolve_default_link_port(new_cfg));
.unwrap_or(new_cfg.server.port);
for user in &added {
if let Some(secret) = new_hot.users.get(*user) {
print_user_links(user, secret, &host, port, new_cfg);
@@ -1200,18 +1170,6 @@ fn log_changes(
new_hot.user_data_quota.len()
);
}
if old_hot.user_rate_limits != new_hot.user_rate_limits {
info!(
"config reload: user_rate_limits updated ({} entries)",
new_hot.user_rate_limits.len()
);
}
if old_hot.cidr_rate_limits != new_hot.cidr_rate_limits {
info!(
"config reload: cidr_rate_limits updated ({} entries)",
new_hot.cidr_rate_limits.len()
);
}
if old_hot.user_max_unique_ips != new_hot.user_max_unique_ips {
info!(
"config reload: user_max_unique_ips updated ({} entries)",
+46 -285
View File
@@ -47,12 +47,18 @@ pub(crate) struct UserAuthEntry {
impl UserAuthSnapshot {
fn from_users(users: &HashMap<String, String>) -> Result<Self> {
// Keep runtime user ids stable across reloads so overload scans and
// sticky hints do not depend on HashMap iteration order.
let mut sorted_users: Vec<_> = users.iter().collect();
sorted_users
.sort_unstable_by(|(left, _), (right, _)| left.as_bytes().cmp(right.as_bytes()));
let mut entries = Vec::with_capacity(users.len());
let mut by_name = HashMap::with_capacity(users.len());
let mut sni_index = HashMap::with_capacity(users.len());
let mut sni_initial_index = HashMap::with_capacity(users.len());
for (user, secret_hex) in users {
for (user, secret_hex) in sorted_users {
let decoded = hex::decode(secret_hex).map_err(|_| ProxyError::InvalidSecret {
user: user.clone(),
reason: "Must be 32 hex characters".to_string(),
@@ -253,12 +259,6 @@ fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
}
for upstream in &config.upstreams {
if matches!(upstream.ipv4, Some(false)) && matches!(upstream.ipv6, Some(false)) {
return Err(ProxyError::Config(
"upstream.ipv4 and upstream.ipv6 cannot both be false".to_string(),
));
}
if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type {
let parsed = ShadowsocksServerConfig::from_url(url)
.map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?;
@@ -343,108 +343,27 @@ impl ProxyConfig {
let network_table = parsed_toml
.get("network")
.and_then(|value| value.as_table());
let server_table = parsed_toml.get("server").and_then(|value| value.as_table());
let conntrack_control_table = server_table
.and_then(|table| table.get("conntrack_control"))
.and_then(|value| value.as_table());
let update_every_is_explicit = general_table
.map(|table| table.contains_key("update_every"))
.unwrap_or(false);
let beobachten_is_explicit = general_table
.map(|table| table.contains_key("beobachten"))
.unwrap_or(false);
let beobachten_minutes_is_explicit = general_table
.map(|table| table.contains_key("beobachten_minutes"))
.unwrap_or(false);
let beobachten_flush_secs_is_explicit = general_table
.map(|table| table.contains_key("beobachten_flush_secs"))
.unwrap_or(false);
let beobachten_file_is_explicit = general_table
.map(|table| table.contains_key("beobachten_file"))
.unwrap_or(false);
let legacy_secret_is_explicit = general_table
.map(|table| table.contains_key("proxy_secret_auto_reload_secs"))
.unwrap_or(false);
let legacy_config_is_explicit = general_table
.map(|table| table.contains_key("proxy_config_auto_reload_secs"))
.unwrap_or(false);
let legacy_top_level_beobachten = parsed_toml.get("beobachten").cloned();
let legacy_top_level_beobachten_minutes = parsed_toml.get("beobachten_minutes").cloned();
let legacy_top_level_beobachten_flush_secs =
parsed_toml.get("beobachten_flush_secs").cloned();
let legacy_top_level_beobachten_file = parsed_toml.get("beobachten_file").cloned();
let stun_servers_is_explicit = network_table
.map(|table| table.contains_key("stun_servers"))
.unwrap_or(false);
let inline_conntrack_control_is_explicit = conntrack_control_table
.map(|table| table.contains_key("inline_conntrack_control"))
.unwrap_or(false);
let mut config: ProxyConfig = parsed_toml
.try_into()
.map_err(|e| ProxyError::Config(e.to_string()))?;
config
.server
.conntrack_control
.inline_conntrack_control_explicit = inline_conntrack_control_is_explicit;
if !update_every_is_explicit && (legacy_secret_is_explicit || legacy_config_is_explicit) {
config.general.update_every = None;
}
// Backward compatibility: legacy top-level beobachten* keys.
// Prefer `[general].*` when both are present.
let mut legacy_beobachten_applied = false;
if !beobachten_is_explicit && let Some(value) = legacy_top_level_beobachten.as_ref() {
let parsed = value.as_bool().ok_or_else(|| {
ProxyError::Config("beobachten (top-level) must be a boolean".to_string())
})?;
config.general.beobachten = parsed;
legacy_beobachten_applied = true;
}
if !beobachten_minutes_is_explicit
&& let Some(value) = legacy_top_level_beobachten_minutes.as_ref()
{
let raw = value.as_integer().ok_or_else(|| {
ProxyError::Config("beobachten_minutes (top-level) must be an integer".to_string())
})?;
let parsed = u64::try_from(raw).map_err(|_| {
ProxyError::Config(
"beobachten_minutes (top-level) must be within u64 range".to_string(),
)
})?;
config.general.beobachten_minutes = parsed;
legacy_beobachten_applied = true;
}
if !beobachten_flush_secs_is_explicit
&& let Some(value) = legacy_top_level_beobachten_flush_secs.as_ref()
{
let raw = value.as_integer().ok_or_else(|| {
ProxyError::Config(
"beobachten_flush_secs (top-level) must be an integer".to_string(),
)
})?;
let parsed = u64::try_from(raw).map_err(|_| {
ProxyError::Config(
"beobachten_flush_secs (top-level) must be within u64 range".to_string(),
)
})?;
config.general.beobachten_flush_secs = parsed;
legacy_beobachten_applied = true;
}
if !beobachten_file_is_explicit
&& let Some(value) = legacy_top_level_beobachten_file.as_ref()
{
let parsed = value.as_str().ok_or_else(|| {
ProxyError::Config("beobachten_file (top-level) must be a string".to_string())
})?;
config.general.beobachten_file = parsed.to_string();
legacy_beobachten_applied = true;
}
if legacy_beobachten_applied {
warn!("top-level beobachten* keys are deprecated; use general.beobachten* instead");
}
let legacy_nat_stun = config.general.middle_proxy_nat_stun.take();
let legacy_nat_stun_servers =
std::mem::take(&mut config.general.middle_proxy_nat_stun_servers);
@@ -640,6 +559,12 @@ impl ProxyConfig {
));
}
if config.censorship.mask_relay_max_bytes == 0 {
return Err(ProxyError::Config(
"censorship.mask_relay_max_bytes must be > 0".to_string(),
));
}
if config.censorship.mask_relay_max_bytes > 67_108_864 {
return Err(ProxyError::Config(
"censorship.mask_relay_max_bytes must be <= 67108864".to_string(),
@@ -866,22 +791,6 @@ impl ProxyConfig {
));
}
for (user, limit) in &config.access.user_rate_limits {
if limit.up_bps == 0 && limit.down_bps == 0 {
return Err(ProxyError::Config(format!(
"access.user_rate_limits.{user} must set at least one non-zero direction"
)));
}
}
for (cidr, limit) in &config.access.cidr_rate_limits {
if limit.up_bps == 0 && limit.down_bps == 0 {
return Err(ProxyError::Config(format!(
"access.cidr_rate_limits.{cidr} must set at least one non-zero direction"
)));
}
}
if config.general.me_reinit_every_secs == 0 {
return Err(ProxyError::Config(
"general.me_reinit_every_secs must be > 0".to_string(),
@@ -1347,7 +1256,6 @@ impl ProxyConfig {
if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() {
config.server.listeners.push(ListenerConfig {
ip: ipv4,
port: Some(config.server.port),
announce: None,
announce_ip: None,
proxy_protocol: None,
@@ -1359,7 +1267,6 @@ impl ProxyConfig {
{
config.server.listeners.push(ListenerConfig {
ip: ipv6,
port: Some(config.server.port),
announce: None,
announce_ip: None,
proxy_protocol: None,
@@ -1368,13 +1275,6 @@ impl ProxyConfig {
}
}
// Migration: listeners[].port fallback to legacy server.port.
for listener in &mut config.server.listeners {
if listener.port.is_none() {
listener.port = Some(config.server.port);
}
}
// Migration: announce_ip → announce for each listener.
for listener in &mut config.server.listeners {
if listener.announce.is_none()
@@ -1395,14 +1295,11 @@ impl ProxyConfig {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
});
}
@@ -1494,21 +1391,6 @@ mod tests {
const TEST_SHADOWSOCKS_URL: &str =
"ss://2022-blake3-aes-256-gcm:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=@127.0.0.1:8388";
fn load_config_from_temp_toml(toml: &str) -> ProxyConfig {
let nonce = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos();
let dir = std::env::temp_dir().join(format!("telemt_load_cfg_{nonce}"));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("config.toml");
std::fs::write(&path, toml).unwrap();
let cfg = ProxyConfig::load(&path).unwrap();
let _ = std::fs::remove_file(path);
let _ = std::fs::remove_dir(dir);
cfg
}
#[test]
fn serde_defaults_remain_unchanged_for_present_sections() {
let toml = r#"
@@ -1605,7 +1487,6 @@ mod tests {
cfg.general.rpc_proxy_req_every,
default_rpc_proxy_req_every()
);
assert_eq!(cfg.general.beobachten_file, default_beobachten_file());
assert_eq!(cfg.general.update_every, default_update_every());
assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4());
assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt());
@@ -1616,7 +1497,6 @@ mod tests {
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
assert_eq!(cfg.server.api.listen, default_api_listen());
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
assert_eq!(cfg.server.api.gray_action, ApiGrayAction::Drop);
assert_eq!(
cfg.server.api.request_body_limit_bytes,
default_api_request_body_limit_bytes()
@@ -1773,7 +1653,6 @@ mod tests {
default_upstream_connect_failfast_hard_errors()
);
assert_eq!(general.rpc_proxy_req_every, default_rpc_proxy_req_every());
assert_eq!(general.beobachten_file, default_beobachten_file());
assert_eq!(general.update_every, default_update_every());
let server = ServerConfig::default();
@@ -1788,7 +1667,6 @@ mod tests {
);
assert_eq!(server.api.listen, default_api_listen());
assert_eq!(server.api.whitelist, default_api_whitelist());
assert_eq!(server.api.gray_action, ApiGrayAction::Drop);
assert_eq!(
server.api.request_body_limit_bytes,
default_api_request_body_limit_bytes()
@@ -1862,10 +1740,7 @@ mod tests {
"#,
)
.unwrap();
assert_eq!(
cfg_missing.server.proxy_protocol_trusted_cidrs,
default_proxy_protocol_trusted_cidrs()
);
assert!(cfg_missing.server.proxy_protocol_trusted_cidrs.is_empty());
let cfg_explicit_empty: ProxyConfig = toml::from_str(
r#"
@@ -1887,40 +1762,43 @@ mod tests {
}
#[test]
fn conntrack_inline_explicit_flag_is_false_when_omitted() {
let cfg = load_config_from_temp_toml(
r#"
[general]
[network]
[server]
[server.conntrack_control]
[access]
"#,
fn runtime_user_auth_snapshot_order_is_stable_across_hashmap_insertion_orders() {
let mut left_users = HashMap::new();
left_users.insert(
"beta".to_string(),
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
);
assert!(
!cfg.server
.conntrack_control
.inline_conntrack_control_explicit
left_users.insert(
"alpha".to_string(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
);
}
#[test]
fn conntrack_inline_explicit_flag_is_true_when_present() {
let cfg = load_config_from_temp_toml(
r#"
[general]
[network]
[server]
[server.conntrack_control]
inline_conntrack_control = true
[access]
"#,
let mut right_users = HashMap::new();
right_users.insert(
"alpha".to_string(),
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
);
assert!(
cfg.server
.conntrack_control
.inline_conntrack_control_explicit
right_users.insert(
"beta".to_string(),
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
);
let left_snapshot = UserAuthSnapshot::from_users(&left_users).unwrap();
let right_snapshot = UserAuthSnapshot::from_users(&right_users).unwrap();
let left_names: Vec<_> = left_snapshot
.entries()
.iter()
.map(|entry| entry.user.as_str())
.collect();
let right_names: Vec<_> = right_snapshot
.entries()
.iter()
.map(|entry| entry.user.as_str())
.collect();
assert_eq!(left_names, ["alpha", "beta"]);
assert_eq!(left_names, right_names);
}
#[test]
@@ -1971,123 +1849,6 @@ mod tests {
cfg_accept.censorship.unknown_sni_action,
UnknownSniAction::Accept
);
let cfg_reject: ProxyConfig = toml::from_str(
r#"
[server]
[general]
[network]
[access]
[censorship]
unknown_sni_action = "reject_handshake"
"#,
)
.unwrap();
assert_eq!(
cfg_reject.censorship.unknown_sni_action,
UnknownSniAction::RejectHandshake
);
}
#[test]
fn api_gray_action_parses_and_defaults_to_drop() {
let cfg_default: ProxyConfig = toml::from_str(
r#"
[server]
[general]
[network]
[access]
"#,
)
.unwrap();
assert_eq!(cfg_default.server.api.gray_action, ApiGrayAction::Drop);
let cfg_api: ProxyConfig = toml::from_str(
r#"
[server]
[general]
[network]
[access]
[server.api]
gray_action = "api"
"#,
)
.unwrap();
assert_eq!(cfg_api.server.api.gray_action, ApiGrayAction::Api);
let cfg_200: ProxyConfig = toml::from_str(
r#"
[server]
[general]
[network]
[access]
[server.api]
gray_action = "200"
"#,
)
.unwrap();
assert_eq!(cfg_200.server.api.gray_action, ApiGrayAction::Ok200);
let cfg_drop: ProxyConfig = toml::from_str(
r#"
[server]
[general]
[network]
[access]
[server.api]
gray_action = "drop"
"#,
)
.unwrap();
assert_eq!(cfg_drop.server.api.gray_action, ApiGrayAction::Drop);
}
#[test]
fn top_level_beobachten_keys_migrate_to_general_when_general_not_explicit() {
let cfg = load_config_from_temp_toml(
r#"
beobachten = false
beobachten_minutes = 7
beobachten_flush_secs = 3
beobachten_file = "tmp/legacy-beob.txt"
[server]
[general]
[network]
[access]
"#,
);
assert!(!cfg.general.beobachten);
assert_eq!(cfg.general.beobachten_minutes, 7);
assert_eq!(cfg.general.beobachten_flush_secs, 3);
assert_eq!(cfg.general.beobachten_file, "tmp/legacy-beob.txt");
}
#[test]
fn general_beobachten_keys_have_priority_over_legacy_top_level() {
let cfg = load_config_from_temp_toml(
r#"
beobachten = true
beobachten_minutes = 30
beobachten_flush_secs = 30
beobachten_file = "tmp/legacy-beob.txt"
[server]
[general]
beobachten = false
beobachten_minutes = 5
beobachten_flush_secs = 2
beobachten_file = "tmp/general-beob.txt"
[network]
[access]
"#,
);
assert!(!cfg.general.beobachten);
assert_eq!(cfg.general.beobachten_minutes, 5);
assert_eq!(cfg.general.beobachten_flush_secs, 2);
assert_eq!(cfg.general.beobachten_file, "tmp/general-beob.txt");
}
#[test]
@@ -238,7 +238,7 @@ mask_shape_above_cap_blur_max_bytes = 8
}
#[test]
fn load_accepts_zero_mask_relay_max_bytes_as_unlimited() {
fn load_rejects_zero_mask_relay_max_bytes() {
let path = write_temp_config(
r#"
[censorship]
@@ -246,9 +246,12 @@ mask_relay_max_bytes = 0
"#,
);
let cfg = ProxyConfig::load(&path)
.expect("mask_relay_max_bytes=0 must be accepted as unlimited relay cap");
assert_eq!(cfg.censorship.mask_relay_max_bytes, 0);
let err = ProxyConfig::load(&path).expect_err("mask_relay_max_bytes must be > 0");
let msg = err.to_string();
assert!(
msg.contains("censorship.mask_relay_max_bytes must be > 0"),
"error must explain non-zero relay cap invariant, got: {msg}"
);
remove_temp_config(&path);
}
+3 -145
View File
@@ -159,21 +159,6 @@ impl MeBindStaleMode {
}
}
/// RST-on-close mode for accepted client sockets.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum RstOnCloseMode {
/// Normal FIN on all closes (default, no behaviour change).
#[default]
Off,
/// SO_LINGER(0) on accept; cleared after successful auth.
/// Pre-handshake failures (scanners, DPI, timeouts) send RST;
/// authenticated relay sessions close gracefully with FIN.
Errors,
/// SO_LINGER(0) on accept, never cleared — all closes send RST.
Always,
}
/// Middle-End writer floor policy mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
@@ -392,26 +377,14 @@ pub struct GeneralConfig {
#[serde(default = "default_proxy_secret_path")]
pub proxy_secret_path: Option<String>,
/// Optional custom URL for infrastructure secret (https://core.telegram.org/getProxySecret if absent).
#[serde(default)]
pub proxy_secret_url: Option<String>,
/// Optional path to cache raw getProxyConfig (IPv4) snapshot for startup fallback.
#[serde(default = "default_proxy_config_v4_cache_path")]
pub proxy_config_v4_cache_path: Option<String>,
/// Optional custom URL for getProxyConfig (https://core.telegram.org/getProxyConfig if absent).
#[serde(default)]
pub proxy_config_v4_url: Option<String>,
/// Optional path to cache raw getProxyConfigV6 snapshot for startup fallback.
#[serde(default = "default_proxy_config_v6_cache_path")]
pub proxy_config_v6_cache_path: Option<String>,
/// Optional custom URL for getProxyConfigV6 (https://core.telegram.org/getProxyConfigV6 if absent).
#[serde(default)]
pub proxy_config_v6_url: Option<String>,
/// Global ad_tag (32 hex chars from @MTProxybot). Fallback when user has no per-user tag in access.user_ad_tags.
#[serde(default)]
pub ad_tag: Option<String>,
@@ -729,14 +702,6 @@ pub struct GeneralConfig {
#[serde(default)]
pub me_socks_kdf_policy: MeSocksKdfPolicy,
/// Enable route-level ME backpressure controls in reader fairness path.
#[serde(default = "default_me_route_backpressure_enabled")]
pub me_route_backpressure_enabled: bool,
/// Enable worker-local fairshare scheduler for ME reader routing.
#[serde(default = "default_me_route_fairshare_enabled")]
pub me_route_fairshare_enabled: bool,
/// Base backpressure timeout in milliseconds for ME route channel send.
#[serde(default = "default_me_route_backpressure_base_timeout_ms")]
pub me_route_backpressure_base_timeout_ms: u64,
@@ -960,14 +925,6 @@ pub struct GeneralConfig {
/// Minimum unavailable ME DC groups before degrading.
#[serde(default = "default_degradation_min_unavailable_dc_groups")]
pub degradation_min_unavailable_dc_groups: u8,
/// RST-on-close mode for accepted client sockets.
/// `off` — normal FIN on all closes (default).
/// `errors` — SO_LINGER(0) on accept, cleared after successful auth;
/// pre-handshake failures send RST, relayed sessions close gracefully.
/// `always` — SO_LINGER(0) on accept, never cleared; all closes send RST.
#[serde(default)]
pub rst_on_close: RstOnCloseMode,
}
impl Default for GeneralConfig {
@@ -980,11 +937,8 @@ impl Default for GeneralConfig {
use_middle_proxy: default_true(),
ad_tag: None,
proxy_secret_path: default_proxy_secret_path(),
proxy_secret_url: None,
proxy_config_v4_cache_path: default_proxy_config_v4_cache_path(),
proxy_config_v4_url: None,
proxy_config_v6_cache_path: default_proxy_config_v6_cache_path(),
proxy_config_v6_url: None,
middle_proxy_nat_ip: None,
middle_proxy_nat_probe: default_true(),
middle_proxy_nat_stun: default_middle_proxy_nat_stun(),
@@ -1067,8 +1021,6 @@ impl Default for GeneralConfig {
disable_colors: false,
telemetry: TelemetryConfig::default(),
me_socks_kdf_policy: MeSocksKdfPolicy::Strict,
me_route_backpressure_enabled: default_me_route_backpressure_enabled(),
me_route_fairshare_enabled: default_me_route_fairshare_enabled(),
me_route_backpressure_base_timeout_ms: default_me_route_backpressure_base_timeout_ms(),
me_route_backpressure_high_timeout_ms: default_me_route_backpressure_high_timeout_ms(),
me_route_backpressure_high_watermark_pct:
@@ -1134,7 +1086,6 @@ impl Default for GeneralConfig {
ntp_servers: default_ntp_servers(),
auto_degradation_enabled: default_true(),
degradation_min_unavailable_dc_groups: default_degradation_min_unavailable_dc_groups(),
rst_on_close: RstOnCloseMode::default(),
}
}
}
@@ -1178,8 +1129,7 @@ pub struct LinksConfig {
#[serde(default)]
pub public_host: Option<String>,
/// Public port for tg:// link generation.
/// Overrides listener ports and legacy `server.port`.
/// Public port for tg:// link generation (overrides server.port).
#[serde(default)]
pub public_port: Option<u16>,
}
@@ -1209,13 +1159,6 @@ pub struct ApiConfig {
#[serde(default = "default_api_whitelist")]
pub whitelist: Vec<IpNetwork>,
/// Behavior for requests from source IPs outside `whitelist`.
/// - `api`: return structured API forbidden response.
/// - `200`: return `200 OK` with an empty body.
/// - `drop`: close the connection without HTTP response.
#[serde(default)]
pub gray_action: ApiGrayAction,
/// Optional static value for `Authorization` header validation.
/// Empty string disables header auth.
#[serde(default)]
@@ -1260,7 +1203,6 @@ impl Default for ApiConfig {
enabled: default_true(),
listen: default_api_listen(),
whitelist: default_api_whitelist(),
gray_action: ApiGrayAction::default(),
auth_header: String::new(),
request_body_limit_bytes: default_api_request_body_limit_bytes(),
minimal_runtime_enabled: default_api_minimal_runtime_enabled(),
@@ -1274,19 +1216,6 @@ impl Default for ApiConfig {
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ApiGrayAction {
/// Preserve current API behavior for denied source IPs.
Api,
/// Mimic a plain web endpoint by returning `200 OK` with an empty body.
#[serde(rename = "200")]
Ok200,
/// Drop connection without HTTP response for denied source IPs.
#[default]
Drop,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")]
pub enum ConntrackMode {
@@ -1354,10 +1283,6 @@ pub struct ConntrackControlConfig {
#[serde(default = "default_conntrack_control_enabled")]
pub inline_conntrack_control: bool,
/// Tracks whether inline_conntrack_control was explicitly set in config.
#[serde(skip)]
pub inline_conntrack_control_explicit: bool,
/// Conntrack mode for listener ingress traffic.
#[serde(default)]
pub mode: ConntrackMode,
@@ -1392,7 +1317,6 @@ impl Default for ConntrackControlConfig {
fn default() -> Self {
Self {
inline_conntrack_control: default_conntrack_control_enabled(),
inline_conntrack_control_explicit: false,
mode: ConntrackMode::default(),
backend: ConntrackBackend::default(),
profile: ConntrackPressureProfile::default(),
@@ -1406,8 +1330,6 @@ impl Default for ConntrackControlConfig {
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
/// Legacy listener port used for backward compatibility.
/// For new configs prefer `[[server.listeners]].port`.
#[serde(default = "default_port")]
pub port: u16,
@@ -1441,9 +1363,8 @@ pub struct ServerConfig {
/// Trusted source CIDRs allowed to send incoming PROXY protocol headers.
///
/// If this field is omitted in config, it defaults to trust-all CIDRs
/// (`0.0.0.0/0` and `::/0`). If it is explicitly set to an empty list,
/// all PROXY protocol headers are rejected.
/// If this field is omitted in config, it defaults to an empty list and
/// all PROXY protocol headers are rejected until trusted CIDRs are set.
#[serde(default = "default_proxy_protocol_trusted_cidrs")]
pub proxy_protocol_trusted_cidrs: Vec<IpNetwork>,
@@ -1581,13 +1502,6 @@ pub enum UnknownSniAction {
Drop,
Mask,
Accept,
/// Reject the TLS handshake by sending a fatal `unrecognized_name` alert
/// (RFC 6066, AlertDescription = 112) before closing the connection.
/// Mimics nginx `ssl_reject_handshake on;` behavior on the default vhost —
/// the wire response indistinguishable from a stock modern web server
/// that simply does not host the requested name.
#[serde(rename = "reject_handshake")]
RejectHandshake,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -1768,23 +1682,9 @@ pub struct AntiCensorshipConfig {
pub mask_shape_above_cap_blur_max_bytes: usize,
/// Maximum bytes relayed per direction on unauthenticated masking fallback paths.
/// Set to 0 to disable byte cap (unlimited within relay/idle timeouts).
#[serde(default = "default_mask_relay_max_bytes")]
pub mask_relay_max_bytes: usize,
/// Wall-clock cap for the full masking relay on non-MTProto fallback paths.
/// Raise when the mask target is a long-lived service (e.g. WebSocket).
/// Default: 60 000 ms (60 s).
#[serde(default = "default_mask_relay_timeout_ms")]
pub mask_relay_timeout_ms: u64,
/// Per-read idle timeout on masking relay and drain paths.
/// Limits resource consumption by slow-loris attacks and port scanners.
/// A read call stalling beyond this is treated as an abandoned connection.
/// Default: 5 000 ms (5 s).
#[serde(default = "default_mask_relay_idle_timeout_ms")]
pub mask_relay_idle_timeout_ms: u64,
/// Prefetch timeout (ms) for extending fragmented masking classifier window.
#[serde(default = "default_mask_classifier_prefetch_timeout_ms")]
pub mask_classifier_prefetch_timeout_ms: u64,
@@ -1830,8 +1730,6 @@ impl Default for AntiCensorshipConfig {
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(),
mask_shape_above_cap_blur_max_bytes: default_mask_shape_above_cap_blur_max_bytes(),
mask_relay_max_bytes: default_mask_relay_max_bytes(),
mask_relay_timeout_ms: default_mask_relay_timeout_ms(),
mask_relay_idle_timeout_ms: default_mask_relay_idle_timeout_ms(),
mask_classifier_prefetch_timeout_ms: default_mask_classifier_prefetch_timeout_ms(),
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
@@ -1864,21 +1762,6 @@ pub struct AccessConfig {
#[serde(default)]
pub user_data_quota: HashMap<String, u64>,
/// Per-user transport rate limits in bits-per-second.
///
/// Each entry supports independent upload (`up_bps`) and download
/// (`down_bps`) ceilings. A value of `0` in one direction means
/// "unlimited" for that direction.
#[serde(default)]
pub user_rate_limits: HashMap<String, RateLimitBps>,
/// Per-CIDR aggregate transport rate limits in bits-per-second.
///
/// Matching uses longest-prefix-wins semantics. A value of `0` in one
/// direction means "unlimited" for that direction.
#[serde(default)]
pub cidr_rate_limits: HashMap<IpNetwork, RateLimitBps>,
#[serde(default)]
pub user_max_unique_ips: HashMap<String, usize>,
@@ -1912,8 +1795,6 @@ impl Default for AccessConfig {
user_max_tcp_conns_global_each: default_user_max_tcp_conns_global_each(),
user_expirations: HashMap::new(),
user_data_quota: HashMap::new(),
user_rate_limits: HashMap::new(),
cidr_rate_limits: HashMap::new(),
user_max_unique_ips: HashMap::new(),
user_max_unique_ips_global_each: default_user_max_unique_ips_global_each(),
user_max_unique_ips_mode: UserMaxUniqueIpsMode::default(),
@@ -1925,14 +1806,6 @@ impl Default for AccessConfig {
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct RateLimitBps {
#[serde(default)]
pub up_bps: u64,
#[serde(default)]
pub down_bps: u64,
}
// ============= Aux Structures =============
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
@@ -1943,10 +1816,6 @@ pub enum UpstreamType {
interface: Option<String>,
#[serde(default)]
bind_addresses: Option<Vec<String>>,
/// Linux-only hard interface pinning via `SO_BINDTODEVICE`.
/// Optional alias: `force_bind`.
#[serde(default, alias = "force_bind")]
bindtodevice: Option<String>,
},
Socks4 {
address: String,
@@ -1983,22 +1852,11 @@ pub struct UpstreamConfig {
pub scopes: String,
#[serde(skip)]
pub selected_scope: String,
/// Allow IPv4 DC targets for this upstream.
/// `None` means auto-detect from runtime connectivity state.
#[serde(default)]
pub ipv4: Option<bool>,
/// Allow IPv6 DC targets for this upstream.
/// `None` means auto-detect from runtime connectivity state.
#[serde(default)]
pub ipv6: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListenerConfig {
pub ip: IpAddr,
/// Per-listener TCP port. If omitted, falls back to legacy `server.port`.
#[serde(default)]
pub port: Option<u16>,
/// IP address or hostname to announce in proxy links.
/// Takes precedence over `announce_ip` if both are set.
#[serde(default)]
+54 -125
View File
@@ -24,13 +24,6 @@ enum NetfilterBackend {
Iptables,
}
#[derive(Clone, Copy)]
struct ConntrackRuntimeSupport {
netfilter_backend: Option<NetfilterBackend>,
has_cap_net_admin: bool,
has_conntrack_binary: bool,
}
#[derive(Clone, Copy)]
struct PressureSample {
conn_pct: Option<u8>,
@@ -63,8 +56,11 @@ pub(crate) fn spawn_conntrack_controller(
shared: Arc<ProxySharedState>,
) {
if !cfg!(target_os = "linux") {
let cfg = config_rx.borrow();
let enabled = cfg.server.conntrack_control.inline_conntrack_control;
let enabled = config_rx
.borrow()
.server
.conntrack_control
.inline_conntrack_control;
stats.set_conntrack_control_enabled(enabled);
stats.set_conntrack_control_available(false);
stats.set_conntrack_pressure_active(false);
@@ -72,14 +68,9 @@ pub(crate) fn spawn_conntrack_controller(
stats.set_conntrack_rule_apply_ok(false);
shared.disable_conntrack_close_sender();
shared.set_conntrack_pressure_active(false);
if enabled
&& cfg
.server
.conntrack_control
.inline_conntrack_control_explicit
{
if enabled {
warn!(
"conntrack control explicitly enabled but unsupported on this OS; disabling runtime worker"
"conntrack control is configured but unsupported on this OS; disabling runtime worker"
);
}
return;
@@ -101,17 +92,16 @@ async fn run_conntrack_controller(
let mut cfg = config_rx.borrow().clone();
let mut pressure_state = PressureState::new(stats.as_ref());
let mut delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
let mut runtime_support = probe_runtime_support(cfg.server.conntrack_control.backend);
let mut effective_enabled = effective_conntrack_enabled(&cfg, runtime_support);
let mut backend = pick_backend(cfg.server.conntrack_control.backend);
apply_runtime_state(
stats.as_ref(),
shared.as_ref(),
&cfg,
runtime_support,
backend.is_some(),
false,
);
reconcile_rules(&cfg, runtime_support, stats.as_ref()).await;
reconcile_rules(&cfg, backend, stats.as_ref()).await;
loop {
tokio::select! {
@@ -120,18 +110,17 @@ async fn run_conntrack_controller(
break;
}
cfg = config_rx.borrow_and_update().clone();
runtime_support = probe_runtime_support(cfg.server.conntrack_control.backend);
effective_enabled = effective_conntrack_enabled(&cfg, runtime_support);
backend = pick_backend(cfg.server.conntrack_control.backend);
delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
apply_runtime_state(stats.as_ref(), shared.as_ref(), &cfg, runtime_support, pressure_state.active);
reconcile_rules(&cfg, runtime_support, stats.as_ref()).await;
apply_runtime_state(stats.as_ref(), shared.as_ref(), &cfg, backend.is_some(), pressure_state.active);
reconcile_rules(&cfg, backend, stats.as_ref()).await;
}
event = close_rx.recv() => {
let Some(event) = event else {
break;
};
stats.set_conntrack_event_queue_depth(close_rx.len() as u64);
if !effective_enabled {
if !cfg.server.conntrack_control.inline_conntrack_control {
continue;
}
if !pressure_state.active {
@@ -167,7 +156,6 @@ async fn run_conntrack_controller(
stats.as_ref(),
shared.as_ref(),
&cfg,
effective_enabled,
&sample,
&mut pressure_state,
);
@@ -187,30 +175,20 @@ fn apply_runtime_state(
stats: &Stats,
shared: &ProxySharedState,
cfg: &ProxyConfig,
runtime_support: ConntrackRuntimeSupport,
backend_available: bool,
pressure_active: bool,
) {
let enabled = cfg.server.conntrack_control.inline_conntrack_control;
let available = effective_conntrack_enabled(cfg, runtime_support);
if enabled
&& !available
&& cfg
.server
.conntrack_control
.inline_conntrack_control_explicit
{
let available = enabled && backend_available && has_cap_net_admin();
if enabled && !available {
warn!(
has_cap_net_admin = runtime_support.has_cap_net_admin,
backend_available = runtime_support.netfilter_backend.is_some(),
conntrack_binary_available = runtime_support.has_conntrack_binary,
configured_backend = ?cfg.server.conntrack_control.backend,
"conntrack control explicitly enabled but unavailable; disabling runtime features"
"conntrack control enabled but unavailable (missing CAP_NET_ADMIN or backend binaries)"
);
}
stats.set_conntrack_control_enabled(enabled);
stats.set_conntrack_control_available(available);
shared.set_conntrack_pressure_active(available && pressure_active);
stats.set_conntrack_pressure_active(available && pressure_active);
shared.set_conntrack_pressure_active(enabled && pressure_active);
stats.set_conntrack_pressure_active(enabled && pressure_active);
}
fn collect_pressure_sample(
@@ -250,11 +228,10 @@ fn update_pressure_state(
stats: &Stats,
shared: &ProxySharedState,
cfg: &ProxyConfig,
effective_enabled: bool,
sample: &PressureSample,
state: &mut PressureState,
) {
if !effective_enabled {
if !cfg.server.conntrack_control.inline_conntrack_control {
if state.active {
state.active = false;
state.low_streak = 0;
@@ -308,26 +285,22 @@ fn update_pressure_state(
state.low_streak = 0;
}
async fn reconcile_rules(
cfg: &ProxyConfig,
runtime_support: ConntrackRuntimeSupport,
stats: &Stats,
) {
async fn reconcile_rules(cfg: &ProxyConfig, backend: Option<NetfilterBackend>, stats: &Stats) {
if !cfg.server.conntrack_control.inline_conntrack_control {
clear_notrack_rules_all_backends().await;
stats.set_conntrack_rule_apply_ok(true);
return;
}
if !effective_conntrack_enabled(cfg, runtime_support) {
clear_notrack_rules_all_backends().await;
if !has_cap_net_admin() {
stats.set_conntrack_rule_apply_ok(false);
return;
}
let backend = runtime_support
.netfilter_backend
.expect("netfilter backend must be available for effective conntrack control");
let Some(backend) = backend else {
stats.set_conntrack_rule_apply_ok(false);
return;
};
let apply_result = match backend {
NetfilterBackend::Nftables => apply_nft_rules(cfg).await,
@@ -342,24 +315,6 @@ async fn reconcile_rules(
}
}
fn probe_runtime_support(configured_backend: ConntrackBackend) -> ConntrackRuntimeSupport {
ConntrackRuntimeSupport {
netfilter_backend: pick_backend(configured_backend),
has_cap_net_admin: has_cap_net_admin(),
has_conntrack_binary: command_exists("conntrack"),
}
}
fn effective_conntrack_enabled(
cfg: &ProxyConfig,
runtime_support: ConntrackRuntimeSupport,
) -> bool {
cfg.server.conntrack_control.inline_conntrack_control
&& runtime_support.has_cap_net_admin
&& runtime_support.netfilter_backend.is_some()
&& runtime_support.has_conntrack_binary
}
fn pick_backend(configured: ConntrackBackend) -> Option<NetfilterBackend> {
match configured {
ConntrackBackend::Auto => {
@@ -388,28 +343,15 @@ fn command_exists(binary: &str) -> bool {
})
}
fn listener_port_set(cfg: &ProxyConfig) -> Vec<u16> {
let mut ports: BTreeSet<u16> = BTreeSet::new();
if cfg.server.listeners.is_empty() {
ports.insert(cfg.server.port);
} else {
for listener in &cfg.server.listeners {
ports.insert(listener.port.unwrap_or(cfg.server.port));
}
}
ports.into_iter().collect()
}
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<(Option<IpAddr>, u16)>, Vec<(Option<IpAddr>, u16)>) {
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr>>) {
let mode = cfg.server.conntrack_control.mode;
let mut v4_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
let mut v6_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
let mut v4_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
let mut v6_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
match mode {
ConntrackMode::Tracked => {}
ConntrackMode::Notrack => {
if cfg.server.listeners.is_empty() {
let port = cfg.server.port;
if let Some(ipv4) = cfg
.server
.listen_addr_ipv4
@@ -417,9 +359,9 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<(Option<IpAddr>, u16)>, Vec<(Optio
.and_then(|s| s.parse::<IpAddr>().ok())
{
if ipv4.is_unspecified() {
v4_targets.insert((None, port));
v4_targets.insert(None);
} else {
v4_targets.insert((Some(ipv4), port));
v4_targets.insert(Some(ipv4));
}
}
if let Some(ipv6) = cfg
@@ -429,39 +371,33 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<(Option<IpAddr>, u16)>, Vec<(Optio
.and_then(|s| s.parse::<IpAddr>().ok())
{
if ipv6.is_unspecified() {
v6_targets.insert((None, port));
v6_targets.insert(None);
} else {
v6_targets.insert((Some(ipv6), port));
v6_targets.insert(Some(ipv6));
}
}
} else {
for listener in &cfg.server.listeners {
let port = listener.port.unwrap_or(cfg.server.port);
if listener.ip.is_ipv4() {
if listener.ip.is_unspecified() {
v4_targets.insert((None, port));
v4_targets.insert(None);
} else {
v4_targets.insert((Some(listener.ip), port));
v4_targets.insert(Some(listener.ip));
}
} else if listener.ip.is_unspecified() {
v6_targets.insert((None, port));
v6_targets.insert(None);
} else {
v6_targets.insert((Some(listener.ip), port));
v6_targets.insert(Some(listener.ip));
}
}
}
}
ConntrackMode::Hybrid => {
let ports = listener_port_set(cfg);
for ip in &cfg.server.conntrack_control.hybrid_listener_ips {
if ip.is_ipv4() {
for port in &ports {
v4_targets.insert((Some(*ip), *port));
}
v4_targets.insert(Some(*ip));
} else {
for port in &ports {
v6_targets.insert((Some(*ip), *port));
}
v6_targets.insert(Some(*ip));
}
}
}
@@ -486,19 +422,19 @@ async fn apply_nft_rules(cfg: &ProxyConfig) -> Result<(), String> {
let (v4_targets, v6_targets) = notrack_targets(cfg);
let mut rules = Vec::new();
for (ip, port) in v4_targets {
for ip in v4_targets {
let rule = if let Some(ip) = ip {
format!("tcp dport {} ip daddr {} notrack", port, ip)
format!("tcp dport {} ip daddr {} notrack", cfg.server.port, ip)
} else {
format!("tcp dport {} notrack", port)
format!("tcp dport {} notrack", cfg.server.port)
};
rules.push(rule);
}
for (ip, port) in v6_targets {
for ip in v6_targets {
let rule = if let Some(ip) = ip {
format!("tcp dport {} ip6 daddr {} notrack", port, ip)
format!("tcp dport {} ip6 daddr {} notrack", cfg.server.port, ip)
} else {
format!("tcp dport {} notrack", port)
format!("tcp dport {} notrack", cfg.server.port)
};
rules.push(rule);
}
@@ -562,7 +498,7 @@ async fn apply_iptables_rules_for_binary(
let (v4_targets, v6_targets) = notrack_targets(cfg);
let selected = if ipv4 { v4_targets } else { v6_targets };
for (ip, port) in selected {
for ip in selected {
let mut args = vec![
"-t".to_string(),
"raw".to_string(),
@@ -571,7 +507,7 @@ async fn apply_iptables_rules_for_binary(
"-p".to_string(),
"tcp".to_string(),
"--dport".to_string(),
port.to_string(),
cfg.server.port.to_string(),
];
if let Some(ip) = ip {
args.push("-d".to_string());
@@ -755,7 +691,7 @@ mod tests {
me_queue_pressure_delta: 0,
};
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &sample, &mut state);
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
assert!(state.active);
assert!(shared.conntrack_pressure_active());
@@ -776,14 +712,7 @@ mod tests {
accept_timeout_delta: 0,
me_queue_pressure_delta: 0,
};
update_pressure_state(
&stats,
shared.as_ref(),
&cfg,
true,
&high_sample,
&mut state,
);
update_pressure_state(&stats, shared.as_ref(), &cfg, &high_sample, &mut state);
assert!(state.active);
let low_sample = PressureSample {
@@ -792,11 +721,11 @@ mod tests {
accept_timeout_delta: 0,
me_queue_pressure_delta: 0,
};
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &low_sample, &mut state);
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
assert!(state.active);
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &low_sample, &mut state);
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
assert!(state.active);
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &low_sample, &mut state);
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
assert!(!state.active);
assert!(!shared.conntrack_pressure_active());
@@ -817,7 +746,7 @@ mod tests {
me_queue_pressure_delta: 10,
};
update_pressure_state(&stats, shared.as_ref(), &cfg, false, &sample, &mut state);
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
assert!(!state.active);
assert!(!shared.conntrack_pressure_active());
+4 -26
View File
@@ -8,7 +8,6 @@ use std::io::{self, Read, Write};
use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf};
use nix::errno::Errno;
use nix::fcntl::{Flock, FlockArg};
use nix::unistd::{self, ForkResult, Gid, Pid, Uid, chdir, close, fork, getpid, setsid};
use tracing::{debug, info, warn};
@@ -158,15 +157,15 @@ fn redirect_stdio_to_devnull() -> Result<(), DaemonError> {
unsafe {
// Redirect stdin (fd 0)
if libc::dup2(devnull_fd, 0) < 0 {
return Err(DaemonError::RedirectFailed(Errno::last()));
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
}
// Redirect stdout (fd 1)
if libc::dup2(devnull_fd, 1) < 0 {
return Err(DaemonError::RedirectFailed(Errno::last()));
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
}
// Redirect stderr (fd 2)
if libc::dup2(devnull_fd, 2) < 0 {
return Err(DaemonError::RedirectFailed(Errno::last()));
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
}
}
@@ -338,27 +337,6 @@ fn is_process_running(pid: i32) -> bool {
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
}
// macOS gates nix::unistd::setgroups differently in the current dependency set,
// so call libc directly there while preserving the original nix path elsewhere.
fn set_supplementary_groups(gid: Gid) -> Result<(), nix::Error> {
#[cfg(target_os = "macos")]
{
let groups = [gid.as_raw()];
let rc = unsafe {
libc::setgroups(
i32::try_from(groups.len()).expect("single supplementary group must fit in c_int"),
groups.as_ptr(),
)
};
if rc == 0 { Ok(()) } else { Err(Errno::last()) }
}
#[cfg(not(target_os = "macos"))]
{
unistd::setgroups(&[gid])
}
}
/// Drops privileges to the specified user and group.
///
/// This should be called after binding privileged ports but before entering
@@ -390,7 +368,7 @@ pub fn drop_privileges(
if let Some(gid) = target_gid {
unistd::setgid(gid).map_err(DaemonError::PrivilegeDrop)?;
set_supplementary_groups(gid).map_err(DaemonError::PrivilegeDrop)?;
unistd::setgroups(&[gid]).map_err(DaemonError::PrivilegeDrop)?;
info!(gid = gid.as_raw(), "Dropped group privileges");
}
-211
View File
@@ -1,211 +0,0 @@
use std::io::{Read, Write};
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream};
use std::time::Duration;
use serde_json::Value;
use crate::config::ProxyConfig;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum HealthcheckMode {
Liveness,
Ready,
}
impl HealthcheckMode {
pub(crate) fn from_cli_arg(value: &str) -> Option<Self> {
match value {
"liveness" => Some(Self::Liveness),
"ready" => Some(Self::Ready),
_ => None,
}
}
fn request_path(self) -> &'static str {
match self {
Self::Liveness => "/v1/health",
Self::Ready => "/v1/health/ready",
}
}
}
pub(crate) fn run(config_path: &str, mode: HealthcheckMode) -> i32 {
match run_inner(config_path, mode) {
Ok(()) => 0,
Err(error) => {
eprintln!("[telemt] healthcheck failed: {error}");
1
}
}
}
fn run_inner(config_path: &str, mode: HealthcheckMode) -> Result<(), String> {
let config =
ProxyConfig::load(config_path).map_err(|error| format!("config load failed: {error}"))?;
let api_cfg = &config.server.api;
if !api_cfg.enabled {
return Ok(());
}
let listen: SocketAddr = api_cfg
.listen
.parse()
.map_err(|_| format!("invalid API listen address: {}", api_cfg.listen))?;
if listen.port() == 0 {
return Err("API listen port is 0".to_string());
}
let target = probe_target(listen);
let mut stream = TcpStream::connect_timeout(&target, Duration::from_secs(2))
.map_err(|error| format!("connect {target} failed: {error}"))?;
stream
.set_read_timeout(Some(Duration::from_secs(2)))
.map_err(|error| format!("set read timeout failed: {error}"))?;
stream
.set_write_timeout(Some(Duration::from_secs(2)))
.map_err(|error| format!("set write timeout failed: {error}"))?;
let request = build_request(target, mode.request_path(), &api_cfg.auth_header);
stream
.write_all(request.as_bytes())
.map_err(|error| format!("request write failed: {error}"))?;
stream
.flush()
.map_err(|error| format!("request flush failed: {error}"))?;
let mut raw_response = Vec::new();
stream
.read_to_end(&mut raw_response)
.map_err(|error| format!("response read failed: {error}"))?;
let response =
String::from_utf8(raw_response).map_err(|_| "response is not valid UTF-8".to_string())?;
let (status_code, body) = split_response(&response)?;
if status_code != 200 {
return Err(format!("HTTP status {status_code}"));
}
validate_payload(mode, body)?;
Ok(())
}
fn probe_target(listen: SocketAddr) -> SocketAddr {
match listen {
SocketAddr::V4(addr) => {
let ip = if addr.ip().is_unspecified() {
Ipv4Addr::LOCALHOST
} else {
*addr.ip()
};
SocketAddr::from((ip, addr.port()))
}
SocketAddr::V6(addr) => {
let ip = if addr.ip().is_unspecified() {
Ipv6Addr::LOCALHOST
} else {
*addr.ip()
};
SocketAddr::from((ip, addr.port()))
}
}
}
fn build_request(target: SocketAddr, path: &str, auth_header: &str) -> String {
let mut request = format!(
"GET {path} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n",
target
);
if !auth_header.is_empty() {
request.push_str("Authorization: ");
request.push_str(auth_header);
request.push_str("\r\n");
}
request.push_str("\r\n");
request
}
fn split_response(response: &str) -> Result<(u16, &str), String> {
let header_end = response
.find("\r\n\r\n")
.ok_or_else(|| "invalid HTTP response headers".to_string())?;
let header = &response[..header_end];
let body = &response[header_end + 4..];
let status_line = header
.lines()
.next()
.ok_or_else(|| "missing HTTP status line".to_string())?;
let status_code = parse_status_code(status_line)?;
Ok((status_code, body))
}
fn parse_status_code(status_line: &str) -> Result<u16, String> {
let mut parts = status_line.split_whitespace();
let version = parts
.next()
.ok_or_else(|| "missing HTTP version".to_string())?;
if !version.starts_with("HTTP/") {
return Err(format!("invalid HTTP status line: {status_line}"));
}
let code = parts
.next()
.ok_or_else(|| "missing HTTP status code".to_string())?;
code.parse::<u16>()
.map_err(|_| format!("invalid HTTP status code: {code}"))
}
fn validate_payload(mode: HealthcheckMode, body: &str) -> Result<(), String> {
let payload: Value =
serde_json::from_str(body).map_err(|_| "response body is not valid JSON".to_string())?;
if payload.get("ok").and_then(Value::as_bool) != Some(true) {
return Err("response JSON has ok=false".to_string());
}
let data = payload
.get("data")
.ok_or_else(|| "response JSON has no data field".to_string())?;
match mode {
HealthcheckMode::Liveness => {
if data.get("status").and_then(Value::as_str) != Some("ok") {
return Err("liveness status is not ok".to_string());
}
}
HealthcheckMode::Ready => {
if data.get("ready").and_then(Value::as_bool) != Some(true) {
return Err("readiness flag is false".to_string());
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{HealthcheckMode, parse_status_code, split_response, validate_payload};
#[test]
fn parse_status_code_reads_http_200() {
let status = parse_status_code("HTTP/1.1 200 OK").expect("must parse status");
assert_eq!(status, 200);
}
#[test]
fn split_response_extracts_status_and_body() {
let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"ok\":true}";
let (status, body) = split_response(response).expect("must split response");
assert_eq!(status, 200);
assert_eq!(body, "{\"ok\":true}");
}
#[test]
fn validate_payload_accepts_liveness_contract() {
let body = "{\"ok\":true,\"data\":{\"status\":\"ok\"}}";
validate_payload(HealthcheckMode::Liveness, body).expect("liveness payload must pass");
}
#[test]
fn validate_payload_rejects_not_ready() {
let body = "{\"ok\":true,\"data\":{\"ready\":false}}";
let result = validate_payload(HealthcheckMode::Ready, body);
assert!(result.is_err());
}
}
+9 -32
View File
@@ -9,7 +9,7 @@ use tokio::net::UnixListener;
use tokio::sync::{Semaphore, watch};
use tracing::{debug, error, info, warn};
use crate::config::{ProxyConfig, RstOnCloseMode};
use crate::config::ProxyConfig;
use crate::crypto::SecureRandom;
use crate::ip_tracker::UserIpTracker;
use crate::proxy::ClientHandler;
@@ -21,7 +21,6 @@ use crate::stats::{ReplayChecker, Stats};
use crate::stream::BufferPool;
use crate::tls_front::TlsFrontCache;
use crate::transport::middle_proxy::MePool;
use crate::transport::socket::set_linger_zero;
use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes};
use super::helpers::{is_expected_handshake_eof, print_proxy_links};
@@ -31,19 +30,6 @@ pub(crate) struct BoundListeners {
pub(crate) has_unix_listener: bool,
}
fn listener_port_or_legacy(listener: &crate::config::ListenerConfig, config: &ProxyConfig) -> u16 {
listener.port.unwrap_or(config.server.port)
}
fn default_link_port(config: &ProxyConfig) -> u16 {
config
.server
.listeners
.first()
.and_then(|listener| listener.port)
.unwrap_or(config.server.port)
}
#[allow(clippy::too_many_arguments)]
pub(crate) async fn bind_listeners(
config: &Arc<ProxyConfig>,
@@ -76,8 +62,7 @@ pub(crate) async fn bind_listeners(
let mut listeners = Vec::new();
for listener_conf in &config.server.listeners {
let listener_port = listener_port_or_legacy(listener_conf, config);
let addr = SocketAddr::new(listener_conf.ip, listener_port);
let addr = SocketAddr::new(listener_conf.ip, config.server.port);
if addr.is_ipv4() && !decision_ipv4_dc {
warn!(%addr, "Skipping IPv4 listener: IPv4 disabled by [network]");
continue;
@@ -120,7 +105,11 @@ pub(crate) async fn bind_listeners(
if config.general.links.public_host.is_none()
&& !config.general.links.show.is_empty()
{
let link_port = config.general.links.public_port.unwrap_or(listener_port);
let link_port = config
.general
.links
.public_port
.unwrap_or(config.server.port);
print_proxy_links(&public_host, link_port, config);
}
@@ -168,7 +157,7 @@ pub(crate) async fn bind_listeners(
.general
.links
.public_port
.unwrap_or(default_link_port(config)),
.unwrap_or(config.server.port),
)
} else {
let ip = detected_ip_v4.or(detected_ip_v6).map(|ip| ip.to_string());
@@ -183,7 +172,7 @@ pub(crate) async fn bind_listeners(
.general
.links
.public_port
.unwrap_or(default_link_port(config)),
.unwrap_or(config.server.port),
)
};
@@ -391,15 +380,6 @@ pub(crate) fn spawn_tcp_accept_loops(
loop {
match listener.accept().await {
Ok((stream, peer_addr)) => {
let rst_mode = config_rx.borrow().general.rst_on_close;
#[cfg(unix)]
let raw_fd = {
use std::os::unix::io::AsRawFd;
stream.as_raw_fd()
};
if matches!(rst_mode, RstOnCloseMode::Errors | RstOnCloseMode::Always) {
let _ = set_linger_zero(&stream);
}
if !*admission_rx_tcp.borrow() {
debug!(peer = %peer_addr, "Admission gate closed, dropping connection");
drop(stream);
@@ -474,9 +454,6 @@ pub(crate) fn spawn_tcp_accept_loops(
shared,
proxy_protocol_enabled,
real_peer_report_for_handler,
#[cfg(unix)]
raw_fd,
rst_mode,
)
.run()
.await
+2 -13
View File
@@ -66,7 +66,6 @@ pub(crate) async fn initialize_me_pool(
match crate::transport::middle_proxy::fetch_proxy_secret_with_upstream(
proxy_secret_path,
config.general.proxy_secret_len_max,
config.general.proxy_secret_url.as_deref(),
Some(upstream_manager.clone()),
)
.await
@@ -127,11 +126,7 @@ pub(crate) async fn initialize_me_pool(
.set_me_status(StartupMeStatus::Initializing, COMPONENT_ME_PROXY_CONFIG_V4)
.await;
let cfg_v4 = load_startup_proxy_config_snapshot(
config
.general
.proxy_config_v4_url
.as_deref()
.unwrap_or("https://core.telegram.org/getProxyConfig"),
"https://core.telegram.org/getProxyConfig",
config.general.proxy_config_v4_cache_path.as_deref(),
me2dc_fallback,
"getProxyConfig",
@@ -163,11 +158,7 @@ pub(crate) async fn initialize_me_pool(
.set_me_status(StartupMeStatus::Initializing, COMPONENT_ME_PROXY_CONFIG_V6)
.await;
let cfg_v6 = load_startup_proxy_config_snapshot(
config
.general
.proxy_config_v6_url
.as_deref()
.unwrap_or("https://core.telegram.org/getProxyConfigV6"),
"https://core.telegram.org/getProxyConfigV6",
config.general.proxy_config_v6_cache_path.as_deref(),
me2dc_fallback,
"getProxyConfigV6",
@@ -277,8 +268,6 @@ pub(crate) async fn initialize_me_pool(
config.general.me_socks_kdf_policy,
config.general.me_writer_cmd_channel_capacity,
config.general.me_route_channel_capacity,
config.general.me_route_backpressure_enabled,
config.general.me_route_fairshare_enabled,
config.general.me_route_backpressure_base_timeout_ms,
config.general.me_route_backpressure_high_timeout_ms,
config.general.me_route_backpressure_high_watermark_pct,
+28 -48
View File
@@ -81,11 +81,23 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
}
}
// Shared maestro startup and main loop. `drop_after_bind` runs on Unix after listeners are bound
// (for privilege drop); it is a no-op on other platforms.
async fn run_telemt_core(
drop_after_bind: impl FnOnce(),
#[cfg(unix)]
async fn run_inner(
daemon_opts: DaemonOptions,
) -> std::result::Result<(), Box<dyn std::error::Error>> {
// Acquire PID file if daemonizing or if explicitly requested
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
let mut pf = PidFile::new(daemon_opts.pid_file_path());
if let Err(e) = pf.acquire() {
eprintln!("[telemt] {}", e);
std::process::exit(1);
}
Some(pf)
} else {
None
};
let process_started_at = Instant::now();
let process_started_at_epoch_secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
@@ -664,11 +676,6 @@ async fn run_telemt_core(
));
let buffer_pool = Arc::new(BufferPool::with_config(64 * 1024, 4096));
let shared_state = ProxySharedState::new();
shared_state.traffic_limiter.apply_policy(
config.access.user_rate_limits.clone(),
config.access.cidr_rate_limits.clone(),
);
connectivity::run_startup_connectivity(
&config,
@@ -700,7 +707,6 @@ async fn run_telemt_core(
beobachten.clone(),
api_config_tx.clone(),
me_pool.clone(),
shared_state.clone(),
)
.await;
let config_rx = runtime_watches.config_rx;
@@ -717,6 +723,7 @@ async fn run_telemt_core(
)
.await;
let _admission_tx_hold = admission_tx;
let shared_state = ProxySharedState::new();
conntrack_control::spawn_conntrack_controller(
config_rx.clone(),
stats.clone(),
@@ -754,8 +761,17 @@ async fn run_telemt_core(
std::process::exit(1);
}
// On Unix, caller supplies privilege drop after bind (may require root for port < 1024).
drop_after_bind();
// Drop privileges after binding sockets (which may require root for port < 1024)
if daemon_opts.user.is_some() || daemon_opts.group.is_some() {
if let Err(e) = drop_privileges(
daemon_opts.user.as_deref(),
daemon_opts.group.as_deref(),
_pid_file.as_ref(),
) {
error!(error = %e, "Failed to drop privileges");
std::process::exit(1);
}
}
runtime_tasks::apply_runtime_log_filter(
has_rust_log,
@@ -803,39 +819,3 @@ async fn run_telemt_core(
Ok(())
}
#[cfg(unix)]
async fn run_inner(
daemon_opts: DaemonOptions,
) -> std::result::Result<(), Box<dyn std::error::Error>> {
// Acquire PID file if daemonizing or if explicitly requested
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
let mut pf = PidFile::new(daemon_opts.pid_file_path());
if let Err(e) = pf.acquire() {
eprintln!("[telemt] {}", e);
std::process::exit(1);
}
Some(pf)
} else {
None
};
let user = daemon_opts.user.clone();
let group = daemon_opts.group.clone();
run_telemt_core(|| {
if user.is_some() || group.is_some() {
if let Err(e) = drop_privileges(user.as_deref(), group.as_deref(), _pid_file.as_ref()) {
error!(error = %e, "Failed to drop privileges");
std::process::exit(1);
}
}
})
.await
}
#[cfg(not(unix))]
async fn run_inner() -> std::result::Result<(), Box<dyn std::error::Error>> {
run_telemt_core(|| {}).await
}
-38
View File
@@ -51,7 +51,6 @@ pub(crate) async fn spawn_runtime_tasks(
beobachten: Arc<BeobachtenStore>,
api_config_tx: watch::Sender<Arc<ProxyConfig>>,
me_pool_for_policy: Option<Arc<MePool>>,
shared_state: Arc<ProxySharedState>,
) -> RuntimeWatches {
let um_clone = upstream_manager.clone();
let dc_overrides_for_health = config.dc_overrides.clone();
@@ -122,8 +121,6 @@ pub(crate) async fn spawn_runtime_tasks(
if let Some(pool) = &me_pool_for_policy {
pool.update_runtime_transport_policy(
cfg.general.me_socks_kdf_policy,
cfg.general.me_route_backpressure_enabled,
cfg.general.me_route_fairshare_enabled,
cfg.general.me_route_backpressure_base_timeout_ms,
cfg.general.me_route_backpressure_high_timeout_ms,
cfg.general.me_route_backpressure_high_watermark_pct,
@@ -185,41 +182,6 @@ pub(crate) async fn spawn_runtime_tasks(
}
});
let limiter = shared_state.traffic_limiter.clone();
limiter.apply_policy(
config.access.user_rate_limits.clone(),
config.access.cidr_rate_limits.clone(),
);
let mut config_rx_rate_limits = config_rx.clone();
tokio::spawn(async move {
let mut prev_user_limits = config_rx_rate_limits
.borrow()
.access
.user_rate_limits
.clone();
let mut prev_cidr_limits = config_rx_rate_limits
.borrow()
.access
.cidr_rate_limits
.clone();
loop {
if config_rx_rate_limits.changed().await.is_err() {
break;
}
let cfg = config_rx_rate_limits.borrow_and_update().clone();
if prev_user_limits != cfg.access.user_rate_limits
|| prev_cidr_limits != cfg.access.cidr_rate_limits
{
limiter.apply_policy(
cfg.access.user_rate_limits.clone(),
cfg.access.cidr_rate_limits.clone(),
);
prev_user_limits = cfg.access.user_rate_limits.clone();
prev_cidr_limits = cfg.access.cidr_rate_limits.clone();
}
}
});
let beobachten_writer = beobachten.clone();
let config_rx_beobachten = config_rx.clone();
tokio::spawn(async move {
-1
View File
@@ -8,7 +8,6 @@ mod crypto;
#[cfg(unix)]
mod daemon;
mod error;
mod healthcheck;
mod ip_tracker;
#[cfg(test)]
#[path = "tests/ip_tracker_encapsulation_adversarial_tests.rs"]
-270
View File
@@ -575,139 +575,6 @@ async fn render_metrics(
}
);
let limiter_metrics = shared_state.traffic_limiter.metrics_snapshot();
let _ = writeln!(
out,
"# HELP telemt_rate_limiter_throttle_total Traffic limiter throttle events by scope and direction"
);
let _ = writeln!(out, "# TYPE telemt_rate_limiter_throttle_total counter");
let _ = writeln!(
out,
"telemt_rate_limiter_throttle_total{{scope=\"user\",direction=\"up\"}} {}",
if core_enabled {
limiter_metrics.user_throttle_up_total
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_throttle_total{{scope=\"user\",direction=\"down\"}} {}",
if core_enabled {
limiter_metrics.user_throttle_down_total
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_throttle_total{{scope=\"cidr\",direction=\"up\"}} {}",
if core_enabled {
limiter_metrics.cidr_throttle_up_total
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_throttle_total{{scope=\"cidr\",direction=\"down\"}} {}",
if core_enabled {
limiter_metrics.cidr_throttle_down_total
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_rate_limiter_wait_ms_total Traffic limiter accumulated wait time in milliseconds by scope and direction"
);
let _ = writeln!(out, "# TYPE telemt_rate_limiter_wait_ms_total counter");
let _ = writeln!(
out,
"telemt_rate_limiter_wait_ms_total{{scope=\"user\",direction=\"up\"}} {}",
if core_enabled {
limiter_metrics.user_wait_up_ms_total
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_wait_ms_total{{scope=\"user\",direction=\"down\"}} {}",
if core_enabled {
limiter_metrics.user_wait_down_ms_total
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_wait_ms_total{{scope=\"cidr\",direction=\"up\"}} {}",
if core_enabled {
limiter_metrics.cidr_wait_up_ms_total
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_wait_ms_total{{scope=\"cidr\",direction=\"down\"}} {}",
if core_enabled {
limiter_metrics.cidr_wait_down_ms_total
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_rate_limiter_active_leases Active relay leases under rate limiting by scope"
);
let _ = writeln!(out, "# TYPE telemt_rate_limiter_active_leases gauge");
let _ = writeln!(
out,
"telemt_rate_limiter_active_leases{{scope=\"user\"}} {}",
if core_enabled {
limiter_metrics.user_active_leases
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_active_leases{{scope=\"cidr\"}} {}",
if core_enabled {
limiter_metrics.cidr_active_leases
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_rate_limiter_policy_entries Active rate-limit policy entries by scope"
);
let _ = writeln!(out, "# TYPE telemt_rate_limiter_policy_entries gauge");
let _ = writeln!(
out,
"telemt_rate_limiter_policy_entries{{scope=\"user\"}} {}",
if core_enabled {
limiter_metrics.user_policy_entries
} else {
0
}
);
let _ = writeln!(
out,
"telemt_rate_limiter_policy_entries{{scope=\"cidr\"}} {}",
if core_enabled {
limiter_metrics.cidr_policy_entries
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_upstream_connect_attempt_total Upstream connect attempts across all requests"
@@ -1310,143 +1177,6 @@ async fn render_metrics(
0
}
);
let _ = writeln!(
out,
"# HELP telemt_me_fair_pressure_state Worker-local fairness pressure state"
);
let _ = writeln!(out, "# TYPE telemt_me_fair_pressure_state gauge");
let _ = writeln!(
out,
"telemt_me_fair_pressure_state {}",
if me_allows_normal {
stats.get_me_fair_pressure_state_gauge()
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_me_fair_active_flows Fair-scheduler active flow count"
);
let _ = writeln!(out, "# TYPE telemt_me_fair_active_flows gauge");
let _ = writeln!(
out,
"telemt_me_fair_active_flows {}",
if me_allows_normal {
stats.get_me_fair_active_flows_gauge()
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_me_fair_queued_bytes Fair-scheduler queued bytes"
);
let _ = writeln!(out, "# TYPE telemt_me_fair_queued_bytes gauge");
let _ = writeln!(
out,
"telemt_me_fair_queued_bytes {}",
if me_allows_normal {
stats.get_me_fair_queued_bytes_gauge()
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_me_fair_flow_state_gauge Fair-scheduler flow health classes"
);
let _ = writeln!(out, "# TYPE telemt_me_fair_flow_state_gauge gauge");
let _ = writeln!(
out,
"telemt_me_fair_flow_state_gauge{{class=\"standing\"}} {}",
if me_allows_normal {
stats.get_me_fair_standing_flows_gauge()
} else {
0
}
);
let _ = writeln!(
out,
"telemt_me_fair_flow_state_gauge{{class=\"backpressured\"}} {}",
if me_allows_normal {
stats.get_me_fair_backpressured_flows_gauge()
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_me_fair_events_total Fair-scheduler event counters"
);
let _ = writeln!(out, "# TYPE telemt_me_fair_events_total counter");
let _ = writeln!(
out,
"telemt_me_fair_events_total{{event=\"scheduler_round\"}} {}",
if me_allows_normal {
stats.get_me_fair_scheduler_rounds_total()
} else {
0
}
);
let _ = writeln!(
out,
"telemt_me_fair_events_total{{event=\"deficit_grant\"}} {}",
if me_allows_normal {
stats.get_me_fair_deficit_grants_total()
} else {
0
}
);
let _ = writeln!(
out,
"telemt_me_fair_events_total{{event=\"deficit_skip\"}} {}",
if me_allows_normal {
stats.get_me_fair_deficit_skips_total()
} else {
0
}
);
let _ = writeln!(
out,
"telemt_me_fair_events_total{{event=\"enqueue_reject\"}} {}",
if me_allows_normal {
stats.get_me_fair_enqueue_rejects_total()
} else {
0
}
);
let _ = writeln!(
out,
"telemt_me_fair_events_total{{event=\"shed_drop\"}} {}",
if me_allows_normal {
stats.get_me_fair_shed_drops_total()
} else {
0
}
);
let _ = writeln!(
out,
"telemt_me_fair_events_total{{event=\"penalty\"}} {}",
if me_allows_normal {
stats.get_me_fair_penalties_total()
} else {
0
}
);
let _ = writeln!(
out,
"telemt_me_fair_events_total{{event=\"downstream_stall\"}} {}",
if me_allows_normal {
stats.get_me_fair_downstream_stalls_total()
} else {
0
}
);
let _ = writeln!(
out,
-1
View File
@@ -97,7 +97,6 @@ pub async fn run_probe(
let UpstreamType::Direct {
interface,
bind_addresses,
..
} = &upstream.upstream_type
else {
continue;
+1 -28
View File
@@ -804,9 +804,6 @@ pub struct RunningClientHandler {
beobachten: Arc<BeobachtenStore>,
shared: Arc<ProxySharedState>,
proxy_protocol_enabled: bool,
#[cfg(unix)]
raw_fd: std::os::unix::io::RawFd,
rst_on_close: crate::config::RstOnCloseMode,
}
impl ClientHandler {
@@ -828,11 +825,6 @@ impl ClientHandler {
proxy_protocol_enabled: bool,
real_peer_report: Arc<std::sync::Mutex<Option<SocketAddr>>>,
) -> RunningClientHandler {
#[cfg(unix)]
let raw_fd = {
use std::os::unix::io::AsRawFd;
stream.as_raw_fd()
};
Self::new_with_shared(
stream,
peer,
@@ -850,9 +842,6 @@ impl ClientHandler {
ProxySharedState::new(),
proxy_protocol_enabled,
real_peer_report,
#[cfg(unix)]
raw_fd,
crate::config::RstOnCloseMode::Off,
)
}
@@ -874,8 +863,6 @@ impl ClientHandler {
shared: Arc<ProxySharedState>,
proxy_protocol_enabled: bool,
real_peer_report: Arc<std::sync::Mutex<Option<SocketAddr>>>,
#[cfg(unix)] raw_fd: std::os::unix::io::RawFd,
rst_on_close: crate::config::RstOnCloseMode,
) -> RunningClientHandler {
let normalized_peer = normalize_ip(peer);
RunningClientHandler {
@@ -896,9 +883,6 @@ impl ClientHandler {
beobachten,
shared,
proxy_protocol_enabled,
#[cfg(unix)]
raw_fd,
rst_on_close,
}
}
}
@@ -917,10 +901,6 @@ impl RunningClientHandler {
debug!(peer = %peer, error = %e, "Failed to configure client socket");
}
#[cfg(unix)]
let raw_fd = self.raw_fd;
let rst_on_close = self.rst_on_close;
let outcome = match self.do_handshake().await? {
Some(outcome) => outcome,
None => return Ok(()),
@@ -928,14 +908,7 @@ impl RunningClientHandler {
// Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts)
match outcome {
HandshakeOutcome::NeedsRelay(fut) => {
#[cfg(unix)]
if matches!(rst_on_close, crate::config::RstOnCloseMode::Errors) {
let _ = crate::transport::socket::clear_linger_fd(raw_fd);
}
fut.await
}
HandshakeOutcome::NeedsMasking(fut) => fut.await,
HandshakeOutcome::NeedsRelay(fut) | HandshakeOutcome::NeedsMasking(fut) => fut.await,
}
}
+1 -5
View File
@@ -316,9 +316,6 @@ where
stats.increment_user_connects(user);
let _direct_connection_lease = stats.acquire_direct_connection_lease();
let traffic_lease = shared
.traffic_limiter
.acquire_lease(user, success.peer.ip());
let buffer_pool_trim = Arc::clone(&buffer_pool);
let relay_activity_timeout = if shared.conntrack_pressure_active() {
@@ -332,7 +329,7 @@ where
} else {
Duration::from_secs(1800)
};
let relay_result = crate::proxy::relay::relay_bidirectional_with_activity_timeout_and_lease(
let relay_result = crate::proxy::relay::relay_bidirectional_with_activity_timeout(
client_reader,
client_writer,
tg_reader,
@@ -343,7 +340,6 @@ where
Arc::clone(&stats),
config.access.user_data_quota.get(user).copied(),
buffer_pool,
traffic_lease,
relay_activity_timeout,
);
tokio::pin!(relay_result);
+45 -41
View File
@@ -55,6 +55,7 @@ const STICKY_HINT_MAX_ENTRIES: usize = 65_536;
const CANDIDATE_HINT_TRACK_CAP: usize = 64;
const OVERLOAD_CANDIDATE_BUDGET_HINTED: usize = 16;
const OVERLOAD_CANDIDATE_BUDGET_UNHINTED: usize = 8;
const OVERLOAD_FULL_SCAN_USER_THRESHOLD: usize = CANDIDATE_HINT_TRACK_CAP;
const RECENT_USER_RING_SCAN_LIMIT: usize = 32;
type HmacSha256 = Hmac<Sha256>;
@@ -242,6 +243,9 @@ fn budget_for_validation(total_users: usize, overload: bool, has_hint: bool) ->
if !overload {
return total_users;
}
if total_users <= OVERLOAD_FULL_SCAN_USER_THRESHOLD {
return total_users;
}
let cap = if has_hint {
OVERLOAD_CANDIDATE_BUDGET_HINTED
} else {
@@ -250,6 +254,28 @@ fn budget_for_validation(total_users: usize, overload: bool, has_hint: bool) ->
total_users.min(cap.max(1))
}
// Rotate partial overload scans across larger snapshots so one truncated
// validation window does not permanently starve the same cold users.
fn candidate_scan_start_offset_in(
shared: &ProxySharedState,
peer_ip: IpAddr,
total_users: usize,
candidate_budget: usize,
) -> usize {
if total_users == 0 || candidate_budget >= total_users {
return 0;
}
let seq = shared
.handshake
.auth_candidate_scan_seq
.fetch_add(1, Ordering::Relaxed);
let mut hasher = shared.handshake.auth_probe_eviction_hasher.build_hasher();
peer_ip.hash(&mut hasher);
seq.hash(&mut hasher);
hasher.finish() as usize % total_users
}
fn parse_tls_auth_material(
handshake: &[u8],
ignore_time_skew: bool,
@@ -1132,20 +1158,9 @@ where
"TLS handshake accepted by unknown SNI policy"
);
}
action @ (UnknownSniAction::Drop
| UnknownSniAction::Mask
| UnknownSniAction::RejectHandshake) => {
action @ (UnknownSniAction::Drop | UnknownSniAction::Mask) => {
auth_probe_record_failure_in(shared, peer.ip(), Instant::now());
// For Drop/Mask we apply the synthetic ServerHello delay so
// the fail-closed path is timing-indistinguishable from the
// success path. For RejectHandshake we deliberately skip the
// delay: a stock modern nginx with `ssl_reject_handshake on;`
// responds with the alert essentially immediately, so
// injecting 8-24ms here would itself become a distinguisher
// against the public baseline we are trying to blend into.
if !matches!(action, UnknownSniAction::RejectHandshake) {
maybe_apply_server_hello_delay(config).await;
}
maybe_apply_server_hello_delay(config).await;
let log_now = Instant::now();
if should_emit_unknown_sni_warn_in(shared, log_now) {
warn!(
@@ -1164,33 +1179,8 @@ where
"TLS handshake rejected by unknown SNI policy"
);
}
if matches!(action, UnknownSniAction::RejectHandshake) {
// TLS alert record layer:
// 0x15 ContentType.alert
// 0x03 0x03 legacy_record_version = TLS 1.2
// (matches what modern nginx emits in
// the first server -> client record,
// per RFC 8446 5.1 guidance)
// 0x00 0x02 length = 2
// Alert payload:
// 0x02 AlertLevel.fatal
// 0x70 AlertDescription.unrecognized_name (112, RFC 6066)
const TLS_ALERT_UNRECOGNIZED_NAME: [u8; 7] =
[0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x70];
if let Err(e) = writer.write_all(&TLS_ALERT_UNRECOGNIZED_NAME).await {
debug!(
peer = %peer,
error = %e,
"Failed to write unrecognized_name TLS alert"
);
} else {
let _ = writer.flush().await;
}
}
return match action {
UnknownSniAction::Drop | UnknownSniAction::RejectHandshake => {
HandshakeResult::Error(ProxyError::UnknownTlsSni)
}
UnknownSniAction::Drop => HandshakeResult::Error(ProxyError::UnknownTlsSni),
UnknownSniAction::Mask => HandshakeResult::BadClient { reader, writer },
UnknownSniAction::Accept => unreachable!(),
};
@@ -1348,7 +1338,14 @@ where
}
if !matched && !budget_exhausted {
for idx in 0..snapshot.entries().len() {
let fallback_start = candidate_scan_start_offset_in(
shared,
peer.ip(),
snapshot.entries().len(),
candidate_budget,
);
for offset in 0..snapshot.entries().len() {
let idx = (fallback_start + offset) % snapshot.entries().len();
let Some(user_id) = u32::try_from(idx).ok() else {
break;
};
@@ -1715,7 +1712,14 @@ where
}
if !matched && !budget_exhausted {
for idx in 0..snapshot.entries().len() {
let fallback_start = candidate_scan_start_offset_in(
shared,
peer.ip(),
snapshot.entries().len(),
candidate_budget,
);
for offset in 0..snapshot.entries().len() {
let idx = (fallback_start + offset) % snapshot.entries().len();
let Some(user_id) = u32::try_from(idx).ok() else {
break;
};
+141 -108
View File
@@ -28,10 +28,14 @@ use tracing::debug;
const MASK_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(test)]
const MASK_TIMEOUT: Duration = Duration::from_millis(50);
/// Maximum duration for the entire masking relay under test (replaced by config at runtime).
/// Maximum duration for the entire masking relay.
/// Limits resource consumption from slow-loris attacks and port scanners.
#[cfg(not(test))]
const MASK_RELAY_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(test)]
const MASK_RELAY_TIMEOUT: Duration = Duration::from_millis(200);
/// Per-read idle timeout for masking relay and drain paths under test (replaced by config at runtime).
#[cfg(not(test))]
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(test)]
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
const MASK_BUFFER_SIZE: usize = 8192;
@@ -51,7 +55,6 @@ async fn copy_with_idle_timeout<R, W>(
writer: &mut W,
byte_cap: usize,
shutdown_on_eof: bool,
idle_timeout: Duration,
) -> CopyOutcome
where
R: AsyncRead + Unpin,
@@ -60,19 +63,22 @@ where
let mut buf = Box::new([0u8; MASK_BUFFER_SIZE]);
let mut total = 0usize;
let mut ended_by_eof = false;
let unlimited = byte_cap == 0;
if byte_cap == 0 {
return CopyOutcome {
total,
ended_by_eof,
};
}
loop {
let read_len = if unlimited {
MASK_BUFFER_SIZE
} else {
let remaining_budget = byte_cap.saturating_sub(total);
if remaining_budget == 0 {
break;
}
remaining_budget.min(MASK_BUFFER_SIZE)
};
let read_res = timeout(idle_timeout, reader.read(&mut buf[..read_len])).await;
let remaining_budget = byte_cap.saturating_sub(total);
if remaining_budget == 0 {
break;
}
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
let read_res = timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf[..read_len])).await;
let n = match read_res {
Ok(Ok(n)) => n,
Ok(Err(_)) | Err(_) => break,
@@ -80,13 +86,13 @@ where
if n == 0 {
ended_by_eof = true;
if shutdown_on_eof {
let _ = timeout(idle_timeout, writer.shutdown()).await;
let _ = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.shutdown()).await;
}
break;
}
total = total.saturating_add(n);
let write_res = timeout(idle_timeout, writer.write_all(&buf[..n])).await;
let write_res = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.write_all(&buf[..n])).await;
match write_res {
Ok(Ok(())) => {}
Ok(Err(_)) | Err(_) => break,
@@ -224,20 +230,13 @@ where
}
}
async fn consume_client_data_with_timeout_and_cap<R>(
reader: R,
byte_cap: usize,
relay_timeout: Duration,
idle_timeout: Duration,
) where
async fn consume_client_data_with_timeout_and_cap<R>(reader: R, byte_cap: usize)
where
R: AsyncRead + Unpin,
{
if timeout(
relay_timeout,
consume_client_data(reader, byte_cap, idle_timeout),
)
.await
.is_err()
if timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, byte_cap))
.await
.is_err()
{
debug!("Timed out while consuming client data on masking fallback path");
}
@@ -507,6 +506,40 @@ fn is_mask_target_local_listener_with_interfaces(
local_addr: SocketAddr,
resolved_override: Option<SocketAddr>,
interface_ips: &[IpAddr],
) -> bool {
let resolved_candidates = resolved_override
.as_ref()
.map(std::slice::from_ref)
.unwrap_or(&[]);
is_mask_target_local_listener_candidates_with_interfaces(
mask_host,
mask_port,
local_addr,
resolved_candidates,
interface_ips,
)
}
fn mask_ip_targets_local_listener(
mask_ip: IpAddr,
local_ip: IpAddr,
interface_ips: &[IpAddr],
) -> bool {
let mask_ip = canonical_ip(mask_ip);
if mask_ip == local_ip {
return true;
}
local_ip.is_unspecified()
&& (mask_ip.is_loopback() || mask_ip.is_unspecified() || interface_ips.contains(&mask_ip))
}
fn is_mask_target_local_listener_candidates_with_interfaces(
mask_host: &str,
mask_port: u16,
local_addr: SocketAddr,
resolved_candidates: &[SocketAddr],
interface_ips: &[IpAddr],
) -> bool {
if mask_port != local_addr.port() {
return false;
@@ -515,31 +548,14 @@ fn is_mask_target_local_listener_with_interfaces(
let local_ip = canonical_ip(local_addr.ip());
let literal_mask_ip = parse_mask_host_ip_literal(mask_host).map(canonical_ip);
if let Some(addr) = resolved_override {
let resolved_ip = canonical_ip(addr.ip());
if resolved_ip == local_ip {
return true;
}
if local_ip.is_unspecified()
&& (resolved_ip.is_loopback()
|| resolved_ip.is_unspecified()
|| interface_ips.contains(&resolved_ip))
{
for addr in resolved_candidates {
if mask_ip_targets_local_listener(addr.ip(), local_ip, interface_ips) {
return true;
}
}
if let Some(mask_ip) = literal_mask_ip {
if mask_ip == local_ip {
return true;
}
if local_ip.is_unspecified()
&& (mask_ip.is_loopback()
|| mask_ip.is_unspecified()
|| interface_ips.contains(&mask_ip))
{
if mask_ip_targets_local_listener(mask_ip, local_ip, interface_ips) {
return true;
}
}
@@ -573,21 +589,67 @@ async fn is_mask_target_local_listener_async(
mask_port: u16,
local_addr: SocketAddr,
resolved_override: Option<SocketAddr>,
) -> bool {
let resolved_candidates = resolved_override
.as_ref()
.map(std::slice::from_ref)
.unwrap_or(&[]);
is_mask_target_local_listener_candidates_async(
mask_host,
mask_port,
local_addr,
resolved_candidates,
)
.await
}
async fn is_mask_target_local_listener_candidates_async(
mask_host: &str,
mask_port: u16,
local_addr: SocketAddr,
resolved_candidates: &[SocketAddr],
) -> bool {
if mask_port != local_addr.port() {
return false;
}
let interfaces = local_interface_ips_async().await;
is_mask_target_local_listener_with_interfaces(
is_mask_target_local_listener_candidates_with_interfaces(
mask_host,
mask_port,
local_addr,
resolved_override,
resolved_candidates,
&interfaces,
)
}
// Resolve hostnames through the same OS DNS path `TcpStream::connect` uses so
// self-target rejection also catches loopback and local-interface hostnames.
async fn resolve_mask_target_candidates(
mask_host: &str,
mask_port: u16,
resolved_override: Option<SocketAddr>,
) -> Vec<SocketAddr> {
if let Some(addr) = resolved_override {
return vec![addr];
}
if parse_mask_host_ip_literal(mask_host).is_some() {
return Vec::new();
}
let mut resolved = Vec::new();
if let Ok(addrs) = tokio::net::lookup_host((mask_host, mask_port)).await {
for addr in addrs {
if !resolved.contains(&addr) {
resolved.push(addr);
}
}
}
resolved
}
fn masking_beobachten_ttl(config: &ProxyConfig) -> Duration {
let minutes = config.general.beobachten_minutes;
let clamped = minutes.clamp(1, 24 * 60);
@@ -640,18 +702,10 @@ pub async fn handle_bad_client<R, W>(
beobachten.record(client_type, peer.ip(), ttl);
}
let relay_timeout = Duration::from_millis(config.censorship.mask_relay_timeout_ms);
let idle_timeout = Duration::from_millis(config.censorship.mask_relay_idle_timeout_ms);
if !config.censorship.mask {
// Masking disabled, just consume data
consume_client_data_with_timeout_and_cap(
reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
.await;
return;
}
@@ -683,7 +737,7 @@ pub async fn handle_bad_client<R, W>(
return;
}
if timeout(
relay_timeout,
MASK_RELAY_TIMEOUT,
relay_to_mask(
reader,
writer,
@@ -697,7 +751,6 @@ pub async fn handle_bad_client<R, W>(
config.censorship.mask_shape_above_cap_blur_max_bytes,
config.censorship.mask_shape_hardening_aggressive_mode,
config.censorship.mask_relay_max_bytes,
idle_timeout,
),
)
.await
@@ -713,8 +766,6 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap(
reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
wait_mask_outcome_budget(outcome_started, config).await;
@@ -724,8 +775,6 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap(
reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
wait_mask_outcome_budget(outcome_started, config).await;
@@ -745,8 +794,15 @@ pub async fn handle_bad_client<R, W>(
// Self-referential masking can create recursive proxy loops under
// misconfiguration and leak distinguishable load spikes to adversaries.
let resolved_mask_addr = resolve_socket_addr(mask_host, mask_port);
if is_mask_target_local_listener_async(mask_host, mask_port, local_addr, resolved_mask_addr)
.await
let resolved_mask_candidates =
resolve_mask_target_candidates(mask_host, mask_port, resolved_mask_addr).await;
if is_mask_target_local_listener_candidates_async(
mask_host,
mask_port,
local_addr,
&resolved_mask_candidates,
)
.await
{
let outcome_started = Instant::now();
debug!(
@@ -756,13 +812,8 @@ pub async fn handle_bad_client<R, W>(
local = %local_addr,
"Mask target resolves to local listener; refusing self-referential masking fallback"
);
consume_client_data_with_timeout_and_cap(
reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
.await;
wait_mask_outcome_budget(outcome_started, config).await;
return;
}
@@ -796,7 +847,7 @@ pub async fn handle_bad_client<R, W>(
return;
}
if timeout(
relay_timeout,
MASK_RELAY_TIMEOUT,
relay_to_mask(
reader,
writer,
@@ -810,7 +861,6 @@ pub async fn handle_bad_client<R, W>(
config.censorship.mask_shape_above_cap_blur_max_bytes,
config.censorship.mask_shape_hardening_aggressive_mode,
config.censorship.mask_relay_max_bytes,
idle_timeout,
),
)
.await
@@ -826,8 +876,6 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap(
reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
wait_mask_outcome_budget(outcome_started, config).await;
@@ -837,8 +885,6 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap(
reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
wait_mask_outcome_budget(outcome_started, config).await;
@@ -860,7 +906,6 @@ async fn relay_to_mask<R, W, MR, MW>(
shape_above_cap_blur_max_bytes: usize,
shape_hardening_aggressive_mode: bool,
mask_relay_max_bytes: usize,
idle_timeout: Duration,
) where
R: AsyncRead + Unpin + Send + 'static,
W: AsyncWrite + Unpin + Send + 'static,
@@ -882,19 +927,11 @@ async fn relay_to_mask<R, W, MR, MW>(
&mut mask_write,
mask_relay_max_bytes,
!shape_hardening_enabled,
idle_timeout,
)
.await
},
async {
copy_with_idle_timeout(
&mut mask_read,
&mut writer,
mask_relay_max_bytes,
true,
idle_timeout,
)
.await
copy_with_idle_timeout(&mut mask_read, &mut writer, mask_relay_max_bytes, true).await
}
);
@@ -922,27 +959,23 @@ async fn relay_to_mask<R, W, MR, MW>(
}
/// Just consume all data from client without responding.
async fn consume_client_data<R: AsyncRead + Unpin>(
mut reader: R,
byte_cap: usize,
idle_timeout: Duration,
) {
async fn consume_client_data<R: AsyncRead + Unpin>(mut reader: R, byte_cap: usize) {
if byte_cap == 0 {
return;
}
// Keep drain path fail-closed under slow-loris stalls.
let mut buf = Box::new([0u8; MASK_BUFFER_SIZE]);
let mut total = 0usize;
let unlimited = byte_cap == 0;
loop {
let read_len = if unlimited {
MASK_BUFFER_SIZE
} else {
let remaining_budget = byte_cap.saturating_sub(total);
if remaining_budget == 0 {
break;
}
remaining_budget.min(MASK_BUFFER_SIZE)
};
let n = match timeout(idle_timeout, reader.read(&mut buf[..read_len])).await {
let remaining_budget = byte_cap.saturating_sub(total);
if remaining_budget == 0 {
break;
}
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
let n = match timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf[..read_len])).await {
Ok(Ok(n)) => n,
Ok(Err(_)) | Err(_) => break,
};
@@ -952,7 +985,7 @@ async fn consume_client_data<R: AsyncRead + Unpin>(
}
total = total.saturating_add(n);
if !unlimited && total >= byte_cap {
if total >= byte_cap {
break;
}
}
+39 -138
View File
@@ -28,7 +28,6 @@ use crate::proxy::route_mode::{
use crate::proxy::shared_state::{
ConntrackCloseEvent, ConntrackClosePublishResult, ConntrackCloseReason, ProxySharedState,
};
use crate::proxy::traffic_limiter::{RateDirection, TrafficLease, next_refill_delay};
use crate::stats::{
MeD2cFlushReason, MeD2cQuotaRejectStage, MeD2cWriteMode, QuotaReserveError, Stats, UserStats,
};
@@ -287,10 +286,6 @@ impl RelayClientIdleState {
self.last_client_frame_at = now;
self.soft_idle_marked = false;
}
fn on_client_tiny_frame(&mut self, now: Instant) {
self.last_client_frame_at = now;
}
}
impl MeD2cFlushPolicy {
@@ -600,41 +595,6 @@ async fn reserve_user_quota_with_yield(
}
}
async fn wait_for_traffic_budget(
lease: Option<&Arc<TrafficLease>>,
direction: RateDirection,
bytes: u64,
) {
if bytes == 0 {
return;
}
let Some(lease) = lease else {
return;
};
let mut remaining = bytes;
while remaining > 0 {
let consume = lease.try_consume(direction, remaining);
if consume.granted > 0 {
remaining = remaining.saturating_sub(consume.granted);
continue;
}
let wait_started_at = Instant::now();
tokio::time::sleep(next_refill_delay()).await;
let wait_ms = wait_started_at
.elapsed()
.as_millis()
.min(u128::from(u64::MAX)) as u64;
lease.observe_wait_ms(
direction,
consume.blocked_user,
consume.blocked_cidr,
wait_ms,
);
}
}
fn classify_me_d2c_flush_reason(
flush_immediately: bool,
batch_frames: usize,
@@ -1025,7 +985,6 @@ where
let quota_limit = config.access.user_data_quota.get(&user).copied();
let quota_user_stats = quota_limit.map(|_| stats.get_or_create_user_stats_handle(&user));
let peer = success.peer;
let traffic_lease = shared.traffic_limiter.acquire_lease(&user, peer.ip());
let proto_tag = success.proto_tag;
let pool_generation = me_pool.current_generation();
@@ -1161,7 +1120,6 @@ where
let rng_clone = rng.clone();
let user_clone = user.clone();
let quota_user_stats_me_writer = quota_user_stats.clone();
let traffic_lease_me_writer = traffic_lease.clone();
let last_downstream_activity_ms_clone = last_downstream_activity_ms.clone();
let bytes_me2c_clone = bytes_me2c.clone();
let d2c_flush_policy = MeD2cFlushPolicy::from_config(&config);
@@ -1195,7 +1153,7 @@ where
let first_is_downstream_activity =
matches!(&first, MeResponse::Data { .. } | MeResponse::Ack(_));
match process_me_writer_response_with_traffic_lease(
match process_me_writer_response(
first,
&mut writer,
proto_tag,
@@ -1206,7 +1164,6 @@ where
quota_user_stats_me_writer.as_deref(),
quota_limit,
d2c_flush_policy.quota_soft_overshoot_bytes,
traffic_lease_me_writer.as_ref(),
bytes_me2c_clone.as_ref(),
conn_id,
d2c_flush_policy.ack_flush_immediate,
@@ -1256,7 +1213,7 @@ where
let next_is_downstream_activity =
matches!(&next, MeResponse::Data { .. } | MeResponse::Ack(_));
match process_me_writer_response_with_traffic_lease(
match process_me_writer_response(
next,
&mut writer,
proto_tag,
@@ -1267,7 +1224,6 @@ where
quota_user_stats_me_writer.as_deref(),
quota_limit,
d2c_flush_policy.quota_soft_overshoot_bytes,
traffic_lease_me_writer.as_ref(),
bytes_me2c_clone.as_ref(),
conn_id,
d2c_flush_policy.ack_flush_immediate,
@@ -1320,7 +1276,7 @@ where
Ok(Some(next)) => {
let next_is_downstream_activity =
matches!(&next, MeResponse::Data { .. } | MeResponse::Ack(_));
match process_me_writer_response_with_traffic_lease(
match process_me_writer_response(
next,
&mut writer,
proto_tag,
@@ -1331,7 +1287,6 @@ where
quota_user_stats_me_writer.as_deref(),
quota_limit,
d2c_flush_policy.quota_soft_overshoot_bytes,
traffic_lease_me_writer.as_ref(),
bytes_me2c_clone.as_ref(),
conn_id,
d2c_flush_policy.ack_flush_immediate,
@@ -1386,7 +1341,7 @@ where
let extra_is_downstream_activity =
matches!(&extra, MeResponse::Data { .. } | MeResponse::Ack(_));
match process_me_writer_response_with_traffic_lease(
match process_me_writer_response(
extra,
&mut writer,
proto_tag,
@@ -1397,7 +1352,6 @@ where
quota_user_stats_me_writer.as_deref(),
quota_limit,
d2c_flush_policy.quota_soft_overshoot_bytes,
traffic_lease_me_writer.as_ref(),
bytes_me2c_clone.as_ref(),
conn_id,
d2c_flush_policy.ack_flush_immediate,
@@ -1588,12 +1542,6 @@ where
match payload_result {
Ok(Some((payload, quickack))) => {
trace!(conn_id, bytes = payload.len(), "C->ME frame");
wait_for_traffic_budget(
traffic_lease.as_ref(),
RateDirection::Up,
payload.len() as u64,
)
.await;
forensics.bytes_c2me = forensics
.bytes_c2me
.saturating_add(payload.len() as u64);
@@ -1814,6 +1762,40 @@ where
let downstream_ms = last_downstream_activity_ms.load(Ordering::Relaxed);
let hard_deadline =
hard_deadline(idle_policy, idle_state, session_started_at, downstream_ms);
if now >= hard_deadline {
clear_relay_idle_candidate_in(shared, forensics.conn_id);
stats.increment_relay_idle_hard_close_total();
let client_idle_secs = now
.saturating_duration_since(idle_state.last_client_frame_at)
.as_secs();
let downstream_idle_secs = now
.saturating_duration_since(
session_started_at + Duration::from_millis(downstream_ms),
)
.as_secs();
warn!(
trace_id = format_args!("0x{:016x}", forensics.trace_id),
conn_id = forensics.conn_id,
user = %forensics.user,
read_label,
client_idle_secs,
downstream_idle_secs,
soft_idle_secs = idle_policy.soft_idle.as_secs(),
hard_idle_secs = idle_policy.hard_idle.as_secs(),
grace_secs = idle_policy.grace_after_downstream_activity.as_secs(),
"Middle-relay hard idle close"
);
return Err(ProxyError::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"middle-relay hard idle timeout while reading {read_label}: client_idle_secs={client_idle_secs}, downstream_idle_secs={downstream_idle_secs}, soft_idle_secs={}, hard_idle_secs={}, grace_secs={}",
idle_policy.soft_idle.as_secs(),
idle_policy.hard_idle.as_secs(),
idle_policy.grace_after_downstream_activity.as_secs(),
),
)));
}
if !idle_state.soft_idle_marked
&& now.saturating_duration_since(idle_state.last_client_frame_at)
>= idle_policy.soft_idle
@@ -1868,45 +1850,7 @@ where
),
)));
}
Err(_) => {
let now = Instant::now();
let downstream_ms = last_downstream_activity_ms.load(Ordering::Relaxed);
let hard_deadline =
hard_deadline(idle_policy, idle_state, session_started_at, downstream_ms);
if now >= hard_deadline {
clear_relay_idle_candidate_in(shared, forensics.conn_id);
stats.increment_relay_idle_hard_close_total();
let client_idle_secs = now
.saturating_duration_since(idle_state.last_client_frame_at)
.as_secs();
let downstream_idle_secs = now
.saturating_duration_since(
session_started_at + Duration::from_millis(downstream_ms),
)
.as_secs();
warn!(
trace_id = format_args!("0x{:016x}", forensics.trace_id),
conn_id = forensics.conn_id,
user = %forensics.user,
read_label,
client_idle_secs,
downstream_idle_secs,
soft_idle_secs = idle_policy.soft_idle.as_secs(),
hard_idle_secs = idle_policy.hard_idle.as_secs(),
grace_secs = idle_policy.grace_after_downstream_activity.as_secs(),
"Middle-relay hard idle close"
);
return Err(ProxyError::Io(std::io::Error::new(
std::io::ErrorKind::TimedOut,
format!(
"middle-relay hard idle timeout while reading {read_label}: client_idle_secs={client_idle_secs}, downstream_idle_secs={downstream_idle_secs}, soft_idle_secs={}, hard_idle_secs={}, grace_secs={}",
idle_policy.soft_idle.as_secs(),
idle_policy.hard_idle.as_secs(),
idle_policy.grace_after_downstream_activity.as_secs(),
),
)));
}
}
Err(_) => {}
}
}
@@ -1997,7 +1941,6 @@ where
};
if len == 0 {
idle_state.on_client_tiny_frame(Instant::now());
idle_state.tiny_frame_debt = idle_state
.tiny_frame_debt
.saturating_add(TINY_FRAME_DEBT_PER_TINY);
@@ -2217,46 +2160,6 @@ async fn process_me_writer_response<W>(
ack_flush_immediate: bool,
batched: bool,
) -> Result<MeWriterResponseOutcome>
where
W: AsyncWrite + Unpin + Send + 'static,
{
process_me_writer_response_with_traffic_lease(
response,
client_writer,
proto_tag,
rng,
frame_buf,
stats,
user,
quota_user_stats,
quota_limit,
quota_soft_overshoot_bytes,
None,
bytes_me2c,
conn_id,
ack_flush_immediate,
batched,
)
.await
}
async fn process_me_writer_response_with_traffic_lease<W>(
response: MeResponse,
client_writer: &mut CryptoWriter<W>,
proto_tag: ProtoTag,
rng: &SecureRandom,
frame_buf: &mut Vec<u8>,
stats: &Stats,
user: &str,
quota_user_stats: Option<&UserStats>,
quota_limit: Option<u64>,
quota_soft_overshoot_bytes: u64,
traffic_lease: Option<&Arc<TrafficLease>>,
bytes_me2c: &AtomicU64,
conn_id: u64,
ack_flush_immediate: bool,
batched: bool,
) -> Result<MeWriterResponseOutcome>
where
W: AsyncWrite + Unpin + Send + 'static,
{
@@ -2280,7 +2183,6 @@ where
});
}
}
wait_for_traffic_budget(traffic_lease, RateDirection::Down, data_len).await;
let write_mode =
match write_client_payload(client_writer, proto_tag, flags, &data, rng, frame_buf)
@@ -2318,7 +2220,6 @@ where
} else {
trace!(conn_id, confirm, "ME->C quickack");
}
wait_for_traffic_budget(traffic_lease, RateDirection::Down, 4).await;
write_client_ack(client_writer, proto_tag, confirm).await?;
stats.increment_me_d2c_ack_frames_total();
-1
View File
@@ -68,7 +68,6 @@ pub mod relay;
pub mod route_mode;
pub mod session_eviction;
pub mod shared_state;
pub mod traffic_limiter;
pub use client::ClientHandler;
#[allow(unused_imports)]
+48 -251
View File
@@ -52,7 +52,6 @@
//! - `SharedCounters` (atomics) let the watchdog read stats without locking
use crate::error::{ProxyError, Result};
use crate::proxy::traffic_limiter::{RateDirection, TrafficLease, next_refill_delay};
use crate::stats::{Stats, UserStats};
use crate::stream::BufferPool;
use std::io;
@@ -62,7 +61,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::task::{Context, Poll};
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_bidirectional_with_sizes};
use tokio::time::{Instant, Sleep};
use tokio::time::Instant;
use tracing::{debug, trace, warn};
// ============= Constants =============
@@ -211,24 +210,12 @@ struct StatsIo<S> {
stats: Arc<Stats>,
user: String,
user_stats: Arc<UserStats>,
traffic_lease: Option<Arc<TrafficLease>>,
c2s_rate_debt_bytes: u64,
c2s_wait: RateWaitState,
s2c_wait: RateWaitState,
quota_limit: Option<u64>,
quota_exceeded: Arc<AtomicBool>,
quota_bytes_since_check: u64,
epoch: Instant,
}
#[derive(Default)]
struct RateWaitState {
sleep: Option<Pin<Box<Sleep>>>,
started_at: Option<Instant>,
blocked_user: bool,
blocked_cidr: bool,
}
impl<S> StatsIo<S> {
fn new(
inner: S,
@@ -238,28 +225,6 @@ impl<S> StatsIo<S> {
quota_limit: Option<u64>,
quota_exceeded: Arc<AtomicBool>,
epoch: Instant,
) -> Self {
Self::new_with_traffic_lease(
inner,
counters,
stats,
user,
None,
quota_limit,
quota_exceeded,
epoch,
)
}
fn new_with_traffic_lease(
inner: S,
counters: Arc<SharedCounters>,
stats: Arc<Stats>,
user: String,
traffic_lease: Option<Arc<TrafficLease>>,
quota_limit: Option<u64>,
quota_exceeded: Arc<AtomicBool>,
epoch: Instant,
) -> Self {
// Mark initial activity so the watchdog doesn't fire before data flows
counters.touch(Instant::now(), epoch);
@@ -270,88 +235,12 @@ impl<S> StatsIo<S> {
stats,
user,
user_stats,
traffic_lease,
c2s_rate_debt_bytes: 0,
c2s_wait: RateWaitState::default(),
s2c_wait: RateWaitState::default(),
quota_limit,
quota_exceeded,
quota_bytes_since_check: 0,
epoch,
}
}
fn record_wait(
wait: &mut RateWaitState,
lease: Option<&Arc<TrafficLease>>,
direction: RateDirection,
) {
let Some(started_at) = wait.started_at.take() else {
return;
};
let wait_ms = started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
if let Some(lease) = lease {
lease.observe_wait_ms(direction, wait.blocked_user, wait.blocked_cidr, wait_ms);
}
wait.blocked_user = false;
wait.blocked_cidr = false;
}
fn arm_wait(wait: &mut RateWaitState, blocked_user: bool, blocked_cidr: bool) {
if wait.sleep.is_none() {
wait.sleep = Some(Box::pin(tokio::time::sleep(next_refill_delay())));
wait.started_at = Some(Instant::now());
}
wait.blocked_user |= blocked_user;
wait.blocked_cidr |= blocked_cidr;
}
fn poll_wait(
wait: &mut RateWaitState,
cx: &mut Context<'_>,
lease: Option<&Arc<TrafficLease>>,
direction: RateDirection,
) -> Poll<()> {
let Some(sleep) = wait.sleep.as_mut() else {
return Poll::Ready(());
};
if sleep.as_mut().poll(cx).is_pending() {
return Poll::Pending;
}
wait.sleep = None;
Self::record_wait(wait, lease, direction);
Poll::Ready(())
}
fn settle_c2s_rate_debt(&mut self, cx: &mut Context<'_>) -> Poll<()> {
let Some(lease) = self.traffic_lease.as_ref() else {
self.c2s_rate_debt_bytes = 0;
return Poll::Ready(());
};
while self.c2s_rate_debt_bytes > 0 {
let consume = lease.try_consume(RateDirection::Up, self.c2s_rate_debt_bytes);
if consume.granted > 0 {
self.c2s_rate_debt_bytes = self.c2s_rate_debt_bytes.saturating_sub(consume.granted);
continue;
}
Self::arm_wait(
&mut self.c2s_wait,
consume.blocked_user,
consume.blocked_cidr,
);
if Self::poll_wait(&mut self.c2s_wait, cx, Some(lease), RateDirection::Up).is_pending()
{
return Poll::Pending;
}
}
if Self::poll_wait(&mut self.c2s_wait, cx, Some(lease), RateDirection::Up).is_pending() {
return Poll::Pending;
}
Poll::Ready(())
}
}
#[derive(Debug)]
@@ -397,25 +286,6 @@ fn should_immediate_quota_check(remaining_before: u64, charge_bytes: u64) -> boo
remaining_before <= QUOTA_NEAR_LIMIT_BYTES || charge_bytes >= QUOTA_LARGE_CHARGE_BYTES
}
fn refund_reserved_quota_bytes(user_stats: &UserStats, reserved_bytes: u64) {
if reserved_bytes == 0 {
return;
}
let mut current = user_stats.quota_used.load(Ordering::Relaxed);
loop {
let next = current.saturating_sub(reserved_bytes);
match user_stats.quota_used.compare_exchange_weak(
current,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => return,
Err(observed) => current = observed,
}
}
}
impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
fn poll_read(
self: Pin<&mut Self>,
@@ -426,9 +296,6 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
if this.quota_exceeded.load(Ordering::Acquire) {
return Poll::Ready(Err(quota_io_error()));
}
if this.settle_c2s_rate_debt(cx).is_pending() {
return Poll::Pending;
}
let mut remaining_before = None;
if let Some(limit) = this.quota_limit {
@@ -510,11 +377,6 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
.add_user_octets_from_handle(this.user_stats.as_ref(), n_to_charge);
this.stats
.increment_user_msgs_from_handle(this.user_stats.as_ref());
if this.traffic_lease.is_some() {
this.c2s_rate_debt_bytes =
this.c2s_rate_debt_bytes.saturating_add(n_to_charge);
let _ = this.settle_c2s_rate_debt(cx);
}
trace!(user = %this.user, bytes = n, "C->S");
}
@@ -536,66 +398,28 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
return Poll::Ready(Err(quota_io_error()));
}
let mut shaper_reserved_bytes = 0u64;
let mut write_buf = buf;
if let Some(lease) = this.traffic_lease.as_ref() {
if !buf.is_empty() {
loop {
let consume = lease.try_consume(RateDirection::Down, buf.len() as u64);
if consume.granted > 0 {
shaper_reserved_bytes = consume.granted;
if consume.granted < buf.len() as u64 {
write_buf = &buf[..consume.granted as usize];
}
let _ = Self::poll_wait(
&mut this.s2c_wait,
cx,
Some(lease),
RateDirection::Down,
);
break;
}
Self::arm_wait(
&mut this.s2c_wait,
consume.blocked_user,
consume.blocked_cidr,
);
if Self::poll_wait(&mut this.s2c_wait, cx, Some(lease), RateDirection::Down)
.is_pending()
{
return Poll::Pending;
}
}
} else {
let _ = Self::poll_wait(&mut this.s2c_wait, cx, Some(lease), RateDirection::Down);
}
}
let mut remaining_before = None;
let mut reserved_bytes = 0u64;
let mut write_buf = buf;
if let Some(limit) = this.quota_limit {
if !write_buf.is_empty() {
if !buf.is_empty() {
let mut reserve_rounds = 0usize;
while reserved_bytes == 0 {
let used_before = this.user_stats.quota_used();
let remaining = limit.saturating_sub(used_before);
if remaining == 0 {
if let Some(lease) = this.traffic_lease.as_ref() {
lease.refund(RateDirection::Down, shaper_reserved_bytes);
}
this.quota_exceeded.store(true, Ordering::Release);
return Poll::Ready(Err(quota_io_error()));
}
remaining_before = Some(remaining);
let desired = remaining.min(write_buf.len() as u64);
let desired = remaining.min(buf.len() as u64);
let mut saw_contention = false;
for _ in 0..QUOTA_RESERVE_SPIN_RETRIES {
match this.user_stats.quota_try_reserve(desired, limit) {
Ok(_) => {
reserved_bytes = desired;
write_buf = &write_buf[..desired as usize];
write_buf = &buf[..desired as usize];
break;
}
Err(crate::stats::QuotaReserveError::LimitExceeded) => {
@@ -610,9 +434,6 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
if reserved_bytes == 0 {
reserve_rounds = reserve_rounds.saturating_add(1);
if reserve_rounds >= QUOTA_RESERVE_MAX_ROUNDS {
if let Some(lease) = this.traffic_lease.as_ref() {
lease.refund(RateDirection::Down, shaper_reserved_bytes);
}
this.quota_exceeded.store(true, Ordering::Release);
return Poll::Ready(Err(quota_io_error()));
}
@@ -625,9 +446,6 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
let used_before = this.user_stats.quota_used();
let remaining = limit.saturating_sub(used_before);
if remaining == 0 {
if let Some(lease) = this.traffic_lease.as_ref() {
lease.refund(RateDirection::Down, shaper_reserved_bytes);
}
this.quota_exceeded.store(true, Ordering::Release);
return Poll::Ready(Err(quota_io_error()));
}
@@ -638,20 +456,23 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
match Pin::new(&mut this.inner).poll_write(cx, write_buf) {
Poll::Ready(Ok(n)) => {
if reserved_bytes > n as u64 {
refund_reserved_quota_bytes(
this.user_stats.as_ref(),
reserved_bytes - n as u64,
);
}
if shaper_reserved_bytes > n as u64
&& let Some(lease) = this.traffic_lease.as_ref()
{
lease.refund(RateDirection::Down, shaper_reserved_bytes - n as u64);
}
if n > 0 {
if let Some(lease) = this.traffic_lease.as_ref() {
Self::record_wait(&mut this.s2c_wait, Some(lease), RateDirection::Down);
let refund = reserved_bytes - n as u64;
let mut current = this.user_stats.quota_used.load(Ordering::Relaxed);
loop {
let next = current.saturating_sub(refund);
match this.user_stats.quota_used.compare_exchange_weak(
current,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(observed) => current = observed,
}
}
}
if n > 0 {
let n_to_charge = n as u64;
// S→C: data written to client
@@ -691,23 +512,37 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
}
Poll::Ready(Err(err)) => {
if reserved_bytes > 0 {
refund_reserved_quota_bytes(this.user_stats.as_ref(), reserved_bytes);
}
if shaper_reserved_bytes > 0
&& let Some(lease) = this.traffic_lease.as_ref()
{
lease.refund(RateDirection::Down, shaper_reserved_bytes);
let mut current = this.user_stats.quota_used.load(Ordering::Relaxed);
loop {
let next = current.saturating_sub(reserved_bytes);
match this.user_stats.quota_used.compare_exchange_weak(
current,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(observed) => current = observed,
}
}
}
Poll::Ready(Err(err))
}
Poll::Pending => {
if reserved_bytes > 0 {
refund_reserved_quota_bytes(this.user_stats.as_ref(), reserved_bytes);
}
if shaper_reserved_bytes > 0
&& let Some(lease) = this.traffic_lease.as_ref()
{
lease.refund(RateDirection::Down, shaper_reserved_bytes);
let mut current = this.user_stats.quota_used.load(Ordering::Relaxed);
loop {
let next = current.saturating_sub(reserved_bytes);
match this.user_stats.quota_used.compare_exchange_weak(
current,
next,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(observed) => current = observed,
}
}
}
Poll::Pending
}
@@ -792,43 +627,6 @@ pub async fn relay_bidirectional_with_activity_timeout<CR, CW, SR, SW>(
_buffer_pool: Arc<BufferPool>,
activity_timeout: Duration,
) -> Result<()>
where
CR: AsyncRead + Unpin + Send + 'static,
CW: AsyncWrite + Unpin + Send + 'static,
SR: AsyncRead + Unpin + Send + 'static,
SW: AsyncWrite + Unpin + Send + 'static,
{
relay_bidirectional_with_activity_timeout_and_lease(
client_reader,
client_writer,
server_reader,
server_writer,
c2s_buf_size,
s2c_buf_size,
user,
stats,
quota_limit,
_buffer_pool,
None,
activity_timeout,
)
.await
}
pub async fn relay_bidirectional_with_activity_timeout_and_lease<CR, CW, SR, SW>(
client_reader: CR,
client_writer: CW,
server_reader: SR,
server_writer: SW,
c2s_buf_size: usize,
s2c_buf_size: usize,
user: &str,
stats: Arc<Stats>,
quota_limit: Option<u64>,
_buffer_pool: Arc<BufferPool>,
traffic_lease: Option<Arc<TrafficLease>>,
activity_timeout: Duration,
) -> Result<()>
where
CR: AsyncRead + Unpin + Send + 'static,
CW: AsyncWrite + Unpin + Send + 'static,
@@ -846,12 +644,11 @@ where
let mut server = CombinedStream::new(server_reader, server_writer);
// Wrap client with stats/activity tracking
let mut client = StatsIo::new_with_traffic_lease(
let mut client = StatsIo::new(
client_combined,
Arc::clone(&counters),
Arc::clone(&stats),
user_owned.clone(),
traffic_lease,
quota_limit,
Arc::clone(&quota_exceeded),
epoch,
+2 -3
View File
@@ -10,7 +10,6 @@ use tokio::sync::mpsc;
use crate::proxy::handshake::{AuthProbeSaturationState, AuthProbeState};
use crate::proxy::middle_relay::{DesyncDedupRotationState, RelayIdleCandidateRegistry};
use crate::proxy::traffic_limiter::TrafficLimiter;
const HANDSHAKE_RECENT_USER_RING_LEN: usize = 64;
@@ -49,6 +48,7 @@ pub(crate) struct HandshakeSharedState {
pub(crate) sticky_user_by_sni_hash: DashMap<u64, u32>,
pub(crate) recent_user_ring: Box<[AtomicU32]>,
pub(crate) recent_user_ring_seq: AtomicU64,
pub(crate) auth_candidate_scan_seq: AtomicU64,
pub(crate) auth_expensive_checks_total: AtomicU64,
pub(crate) auth_budget_exhausted_total: AtomicU64,
}
@@ -66,7 +66,6 @@ pub(crate) struct MiddleRelaySharedState {
pub(crate) struct ProxySharedState {
pub(crate) handshake: HandshakeSharedState,
pub(crate) middle_relay: MiddleRelaySharedState,
pub(crate) traffic_limiter: Arc<TrafficLimiter>,
pub(crate) conntrack_pressure_active: AtomicBool,
pub(crate) conntrack_close_tx: Mutex<Option<mpsc::Sender<ConntrackCloseEvent>>>,
}
@@ -88,6 +87,7 @@ impl ProxySharedState {
.collect::<Vec<_>>()
.into_boxed_slice(),
recent_user_ring_seq: AtomicU64::new(0),
auth_candidate_scan_seq: AtomicU64::new(0),
auth_expensive_checks_total: AtomicU64::new(0),
auth_budget_exhausted_total: AtomicU64::new(0),
},
@@ -100,7 +100,6 @@ impl ProxySharedState {
relay_idle_registry: Mutex::new(RelayIdleCandidateRegistry::default()),
relay_idle_mark_seq: AtomicU64::new(0),
},
traffic_limiter: TrafficLimiter::new(),
conntrack_pressure_active: AtomicBool::new(false),
conntrack_close_tx: Mutex::new(None),
})
@@ -31,14 +31,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -27,14 +27,11 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -25,14 +25,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -38,14 +38,11 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -16,14 +16,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -39,14 +39,11 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -232,14 +229,11 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -476,14 +470,11 @@ async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sen
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -553,14 +544,11 @@ async fn capture_forwarded_probe_len(tls_len: u16, body_sent: usize) -> usize {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -13,14 +13,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -25,14 +25,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
-81
View File
@@ -332,14 +332,11 @@ async fn relay_task_abort_releases_user_gate_and_ip_reservation() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -449,14 +446,11 @@ async fn relay_cutover_releases_user_gate_and_ip_reservation() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -576,14 +570,11 @@ async fn integration_route_cutover_and_quota_overlap_fails_closed_and_releases_s
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -749,14 +740,11 @@ async fn proxy_protocol_header_is_rejected_when_trust_list_is_empty() {
upstream_type: crate::config::UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -829,14 +817,11 @@ async fn proxy_protocol_header_from_untrusted_peer_range_is_rejected_under_load(
upstream_type: crate::config::UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -992,14 +977,11 @@ async fn short_tls_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1083,14 +1065,11 @@ async fn tls12_record_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1172,14 +1151,11 @@ async fn handle_client_stream_increments_connects_all_exactly_once() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1268,14 +1244,11 @@ async fn running_client_handler_increments_connects_all_exactly_once() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1361,14 +1334,11 @@ async fn idle_pooled_connection_closes_cleanly_in_generic_stream_path() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1435,14 +1405,11 @@ async fn idle_pooled_connection_closes_cleanly_in_client_handler_path() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1524,14 +1491,11 @@ async fn partial_tls_header_stall_triggers_handshake_timeout() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1852,14 +1816,11 @@ async fn valid_tls_path_does_not_fall_back_to_mask_backend() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1964,14 +1925,11 @@ async fn valid_tls_with_invalid_mtproto_falls_back_to_mask_backend() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -2074,14 +2032,11 @@ async fn client_handler_tls_bad_mtproto_is_forwarded_to_mask_backend() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -2199,14 +2154,11 @@ async fn alpn_mismatch_tls_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -2295,14 +2247,11 @@ async fn invalid_hmac_tls_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -2397,14 +2346,11 @@ async fn burst_invalid_tls_probes_are_masked_verbatim() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -3305,14 +3251,11 @@ async fn relay_connect_error_releases_user_and_ip_before_return() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -3869,14 +3812,11 @@ async fn untrusted_proxy_header_source_is_rejected() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -3942,14 +3882,11 @@ async fn empty_proxy_trusted_cidrs_rejects_proxy_header_by_default() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -4042,14 +3979,11 @@ async fn oversized_tls_record_is_masked_in_generic_stream_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -4148,14 +4082,11 @@ async fn oversized_tls_record_is_masked_in_client_handler_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -4268,14 +4199,11 @@ async fn tls_record_len_min_minus_1_is_rejected_in_generic_stream_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -4374,14 +4302,11 @@ async fn tls_record_len_min_minus_1_is_rejected_in_client_handler_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -4483,14 +4408,11 @@ async fn tls_record_len_16384_is_accepted_in_generic_stream_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -4587,14 +4509,11 @@ async fn tls_record_len_16384_is_accepted_in_client_handler_pipeline() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -24,14 +24,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -26,14 +26,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -27,14 +27,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -41,14 +41,11 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1293,14 +1293,11 @@ async fn direct_relay_abort_midflight_releases_route_gauge() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1403,14 +1400,11 @@ async fn direct_relay_cutover_midflight_releases_route_gauge() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1528,14 +1522,11 @@ async fn direct_relay_cutover_storm_multi_session_keeps_generic_errors_and_relea
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
@@ -1767,11 +1758,8 @@ async fn negative_direct_relay_dc_connection_refused_fails_fast() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
100,
@@ -1861,11 +1849,8 @@ async fn adversarial_direct_relay_cutover_integrity() {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
100,
+120 -51
View File
@@ -1007,55 +1007,6 @@ async fn tls_unknown_sni_mask_policy_falls_back_to_bad_client() {
assert!(matches!(result, HandshakeResult::BadClient { .. }));
}
#[tokio::test]
async fn tls_unknown_sni_reject_handshake_policy_emits_unrecognized_name_alert() {
use tokio::io::{AsyncReadExt, duplex};
let secret = [0x4Au8; 16];
let mut config = test_config_with_secret_hex("4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a");
config.censorship.unknown_sni_action = UnknownSniAction::RejectHandshake;
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let rng = SecureRandom::new();
let peer: SocketAddr = "198.51.100.192:44326".parse().unwrap();
let handshake =
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "unknown.example", &[b"h2"]);
// Wire up a duplex so we can inspect what the server writes towards the
// client. We own the "peer side" half to read from it.
let (server_side, mut peer_side) = duplex(1024);
let (server_read, server_write) = tokio::io::split(server_side);
let result = handle_tls_handshake(
&handshake,
server_read,
server_write,
peer,
&config,
&replay_checker,
&rng,
None,
)
.await;
assert!(matches!(
result,
HandshakeResult::Error(ProxyError::UnknownTlsSni)
));
// Drain what the server wrote. We expect exactly one TLS alert record:
// 0x15 0x03 0x03 0x00 0x02 0x02 0x70
// (ContentType.alert, TLS 1.2, length=2, fatal, unrecognized_name)
drop(result); // drops the server-side writer so peer_side sees EOF
let mut buf = Vec::new();
peer_side.read_to_end(&mut buf).await.unwrap();
assert_eq!(
buf,
[0x15, 0x03, 0x03, 0x00, 0x02, 0x02, 0x70],
"reject_handshake must emit a fatal unrecognized_name TLS alert"
);
}
#[tokio::test]
async fn tls_unknown_sni_accept_policy_continues_auth_path() {
let secret = [0x4Bu8; 16];
@@ -1195,9 +1146,9 @@ async fn tls_overload_budget_limits_candidate_scan_depth() {
let mut config = ProxyConfig::default();
config.access.users.clear();
config.access.ignore_time_skew = true;
for idx in 0..32u8 {
for idx in 0..96u8 {
config.access.users.insert(
format!("user-{idx}"),
format!("user-{idx:02}"),
format!("{:032x}", u128::from(idx) + 1),
);
}
@@ -1252,6 +1203,64 @@ async fn tls_overload_budget_limits_candidate_scan_depth() {
);
}
#[tokio::test]
async fn tls_overload_full_scans_small_runtime_snapshot_to_preserve_cold_user_auth() {
let mut config = ProxyConfig::default();
config.access.users.clear();
config.access.ignore_time_skew = true;
for idx in 0..32u8 {
config.access.users.insert(
format!("user-{idx:02}"),
format!("{:032x}", u128::from(idx) + 1),
);
}
config.rebuild_runtime_user_auth().unwrap();
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let rng = SecureRandom::new();
let shared = ProxySharedState::new();
let now = Instant::now();
{
let mut saturation = shared.handshake.auth_probe_saturation.lock().unwrap();
*saturation = Some(AuthProbeSaturationState {
fail_streak: AUTH_PROBE_BACKOFF_START_FAILS,
blocked_until: now + Duration::from_millis(200),
last_seen: now,
});
}
let peer: SocketAddr = "198.51.100.214:44326".parse().unwrap();
let mut secret = [0u8; 16];
secret[15] = 32;
let handshake = make_valid_tls_handshake(&secret, 0);
let result = handle_tls_handshake_with_shared(
&handshake,
tokio::io::empty(),
tokio::io::sink(),
peer,
&config,
&replay_checker,
&rng,
None,
shared.as_ref(),
)
.await;
assert!(
matches!(result, HandshakeResult::Success(_)),
"overload mode must still authenticate valid cold users when runtime snapshot stays small"
);
assert_eq!(
shared
.handshake
.auth_expensive_checks_total
.load(Ordering::Relaxed),
32,
"small saturated snapshots must remain fully scannable"
);
}
#[tokio::test]
async fn mtproto_runtime_snapshot_prefers_preferred_user_hint() {
let mut config = ProxyConfig::default();
@@ -1304,6 +1313,66 @@ async fn mtproto_runtime_snapshot_prefers_preferred_user_hint() {
);
}
#[tokio::test]
async fn mtproto_overload_full_scans_small_runtime_snapshot_to_preserve_cold_user_auth() {
let mut config = ProxyConfig::default();
config.general.modes.secure = true;
config.access.users.clear();
config.access.ignore_time_skew = true;
for idx in 0..32u8 {
config.access.users.insert(
format!("user-{idx:02}"),
format!("{:032x}", u128::from(idx) + 1),
);
}
config.rebuild_runtime_user_auth().unwrap();
let shared = ProxySharedState::new();
let now = Instant::now();
{
let mut saturation = shared.handshake.auth_probe_saturation.lock().unwrap();
*saturation = Some(AuthProbeSaturationState {
fail_streak: AUTH_PROBE_BACKOFF_START_FAILS,
blocked_until: now + Duration::from_millis(200),
last_seen: now,
});
}
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let handshake = make_valid_mtproto_handshake(
"00000000000000000000000000000020",
ProtoTag::Secure,
2,
);
let peer: SocketAddr = "198.51.100.215:44326".parse().unwrap();
let result = handle_mtproto_handshake_with_shared(
&handshake,
tokio::io::empty(),
tokio::io::sink(),
peer,
&config,
&replay_checker,
false,
None,
shared.as_ref(),
)
.await;
assert!(
matches!(result, HandshakeResult::Success(_)),
"overload mode must still authenticate valid direct MTProto users when runtime snapshot stays small"
);
assert_eq!(
shared
.handshake
.auth_expensive_checks_total
.load(Ordering::Relaxed),
32,
"small saturated MTProto snapshots must remain fully scannable"
);
}
#[tokio::test]
async fn alpn_enforce_rejects_unsupported_client_alpn() {
let secret = [0x33u8; 16];
@@ -47,7 +47,7 @@ async fn consume_client_data_stops_after_byte_cap_without_eof() {
};
let cap = 10_000usize;
consume_client_data(reader, cap, MASK_RELAY_IDLE_TIMEOUT).await;
consume_client_data(reader, cap).await;
let total = produced.load(Ordering::Relaxed);
assert!(
@@ -31,7 +31,7 @@ async fn stalling_client_terminates_at_idle_not_relay_timeout() {
let result = tokio::time::timeout(
MASK_RELAY_TIMEOUT,
consume_client_data(reader, MASK_BUFFER_SIZE * 4, MASK_RELAY_IDLE_TIMEOUT),
consume_client_data(reader, MASK_BUFFER_SIZE * 4),
)
.await;
@@ -57,12 +57,9 @@ async fn fast_reader_drains_to_eof() {
let data = vec![0xAAu8; 32 * 1024];
let reader = std::io::Cursor::new(data);
tokio::time::timeout(
MASK_RELAY_TIMEOUT,
consume_client_data(reader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
)
.await
.expect("consume_client_data did not complete for fast EOF reader");
tokio::time::timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, usize::MAX))
.await
.expect("consume_client_data did not complete for fast EOF reader");
}
#[tokio::test]
@@ -84,7 +81,7 @@ async fn io_error_terminates_cleanly() {
tokio::time::timeout(
MASK_RELAY_TIMEOUT,
consume_client_data(ErrReader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
consume_client_data(ErrReader, usize::MAX),
)
.await
.expect("consume_client_data did not return on I/O error");
@@ -34,11 +34,7 @@ async fn consume_stall_stress_finishes_within_idle_budget() {
set.spawn(async {
tokio::time::timeout(
MASK_RELAY_TIMEOUT,
consume_client_data(
OneByteThenStall { sent: false },
usize::MAX,
MASK_RELAY_IDLE_TIMEOUT,
),
consume_client_data(OneByteThenStall { sent: false }, usize::MAX),
)
.await
.expect("consume_client_data exceeded relay timeout under stall load");
@@ -58,22 +54,11 @@ async fn consume_stall_stress_finishes_within_idle_budget() {
}
#[tokio::test]
async fn consume_zero_cap_is_idle_bounded_on_stall() {
async fn consume_zero_cap_returns_immediately() {
let started = Instant::now();
tokio::time::timeout(
MASK_RELAY_TIMEOUT,
consume_client_data(OneByteThenStall { sent: false }, 0, MASK_RELAY_IDLE_TIMEOUT),
)
.await
.expect("zero-cap consume path must remain bounded by timeout guards");
let elapsed = started.elapsed();
consume_client_data(tokio::io::empty(), 0).await;
assert!(
elapsed >= (MASK_RELAY_IDLE_TIMEOUT / 2),
"zero cap must not short-circuit before idle timeout path, got {elapsed:?}"
);
assert!(
elapsed < MASK_RELAY_TIMEOUT,
"zero-cap consume path must complete before relay timeout, got {elapsed:?}"
started.elapsed() < MASK_RELAY_IDLE_TIMEOUT,
"zero byte cap must return immediately"
);
}
@@ -127,14 +127,7 @@ async fn positive_copy_with_production_cap_stops_exactly_at_budget() {
let mut reader = FinitePatternReader::new(PROD_CAP_BYTES + (256 * 1024), 4096, read_calls);
let mut writer = CountingWriter::default();
let outcome = copy_with_idle_timeout(
&mut reader,
&mut writer,
PROD_CAP_BYTES,
true,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await;
assert_eq!(
outcome.total, PROD_CAP_BYTES,
@@ -148,41 +141,16 @@ async fn positive_copy_with_production_cap_stops_exactly_at_budget() {
}
#[tokio::test]
async fn consume_with_zero_cap_drains_until_eof() {
let payload = 256 * 1024;
let total_read = Arc::new(AtomicUsize::new(0));
let reader = BudgetProbeReader::new(payload, Arc::clone(&total_read));
async fn negative_consume_with_zero_cap_performs_no_reads() {
let read_calls = Arc::new(AtomicUsize::new(0));
let reader = FinitePatternReader::new(1024, 64, Arc::clone(&read_calls));
consume_client_data_with_timeout_and_cap(
reader,
0,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
consume_client_data_with_timeout_and_cap(reader, 0).await;
assert_eq!(
total_read.load(Ordering::Relaxed),
payload,
"zero cap must disable byte budget and drain finite payload to EOF"
);
}
#[tokio::test]
async fn copy_with_zero_cap_drains_until_eof() {
let read_calls = Arc::new(AtomicUsize::new(0));
let payload = 73 * 1024;
let mut reader = FinitePatternReader::new(payload, 3072, read_calls);
let mut writer = CountingWriter::default();
let outcome =
copy_with_idle_timeout(&mut reader, &mut writer, 0, true, MASK_RELAY_IDLE_TIMEOUT).await;
assert_eq!(outcome.total, payload);
assert_eq!(writer.written, payload);
assert!(
outcome.ended_by_eof,
"zero cap must not terminate relay early on byte budget"
read_calls.load(Ordering::Relaxed),
0,
"zero cap must return before reading attacker-controlled bytes"
);
}
@@ -193,14 +161,7 @@ async fn edge_copy_below_cap_reports_eof_without_overread() {
let mut reader = FinitePatternReader::new(payload, 3072, read_calls);
let mut writer = CountingWriter::default();
let outcome = copy_with_idle_timeout(
&mut reader,
&mut writer,
PROD_CAP_BYTES,
true,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await;
assert_eq!(outcome.total, payload);
assert_eq!(writer.written, payload);
@@ -214,13 +175,7 @@ async fn edge_copy_below_cap_reports_eof_without_overread() {
async fn adversarial_blackhat_never_ready_reader_is_bounded_by_timeout_guards() {
let started = Instant::now();
consume_client_data_with_timeout_and_cap(
NeverReadyReader,
PROD_CAP_BYTES,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
consume_client_data_with_timeout_and_cap(NeverReadyReader, PROD_CAP_BYTES).await;
assert!(
started.elapsed() < Duration::from_millis(350),
@@ -235,12 +190,7 @@ async fn integration_consume_path_honors_production_cap_for_large_payload() {
let bounded = timeout(
Duration::from_millis(350),
consume_client_data_with_timeout_and_cap(
reader,
PROD_CAP_BYTES,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
),
consume_client_data_with_timeout_and_cap(reader, PROD_CAP_BYTES),
)
.await;
@@ -256,13 +206,7 @@ async fn adversarial_consume_path_never_reads_beyond_declared_byte_cap() {
let total_read = Arc::new(AtomicUsize::new(0));
let reader = BudgetProbeReader::new(256 * 1024, Arc::clone(&total_read));
consume_client_data_with_timeout_and_cap(
reader,
byte_cap,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
consume_client_data_with_timeout_and_cap(reader, byte_cap).await;
assert!(
total_read.load(Ordering::Relaxed) <= byte_cap,
@@ -287,9 +231,7 @@ async fn light_fuzz_cap_and_payload_matrix_preserves_min_budget_invariant() {
let mut reader = FinitePatternReader::new(payload, chunk, read_calls);
let mut writer = CountingWriter::default();
let outcome =
copy_with_idle_timeout(&mut reader, &mut writer, cap, true, MASK_RELAY_IDLE_TIMEOUT)
.await;
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, cap, true).await;
let expected = payload.min(cap);
assert_eq!(
@@ -319,14 +261,7 @@ async fn stress_parallel_copy_tasks_with_production_cap_complete_without_leaks()
read_calls,
);
let mut writer = CountingWriter::default();
copy_with_idle_timeout(
&mut reader,
&mut writer,
PROD_CAP_BYTES,
true,
MASK_RELAY_IDLE_TIMEOUT,
)
.await
copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await
}));
}
@@ -26,7 +26,6 @@ async fn relay_to_mask_enforces_masking_session_byte_cap() {
0,
false,
32 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
});
@@ -82,7 +81,6 @@ async fn relay_to_mask_propagates_client_half_close_without_waiting_for_other_di
0,
false,
32 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
});
@@ -1377,7 +1377,6 @@ async fn relay_to_mask_keeps_backend_to_client_flow_when_client_to_backend_stall
0,
false,
5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
});
@@ -1509,7 +1508,6 @@ async fn relay_to_mask_timeout_cancels_and_drops_all_io_endpoints() {
0,
false,
5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
),
)
.await;
@@ -88,6 +88,45 @@ async fn self_target_fallback_refuses_recursive_loopback_connect() {
);
}
#[tokio::test]
async fn self_target_fallback_refuses_recursive_hostname_connect() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let local_addr = listener.local_addr().unwrap();
let accept_task = tokio::spawn(async move {
timeout(Duration::from_millis(120), listener.accept())
.await
.is_ok()
});
let mut config = ProxyConfig::default();
config.general.beobachten = false;
config.censorship.mask = true;
config.censorship.mask_unix_sock = None;
config.censorship.mask_host = Some("localhost".to_string());
config.censorship.mask_port = local_addr.port();
config.censorship.mask_proxy_protocol = 0;
let peer: SocketAddr = "203.0.113.99:55099".parse().unwrap();
let beobachten = BeobachtenStore::new();
handle_bad_client(
tokio::io::empty(),
tokio::io::sink(),
b"GET /",
peer,
local_addr,
&config,
&beobachten,
)
.await;
let accepted = accept_task.await.unwrap();
assert!(
!accepted,
"hostname self-target masking must fail closed without connecting to local listener"
);
}
#[tokio::test]
async fn same_ip_different_port_still_forwards_to_mask_backend() {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
@@ -228,7 +267,6 @@ async fn relay_path_idle_timeout_eviction_remains_effective() {
0,
false,
5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
@@ -44,7 +44,6 @@ async fn run_relay_case(
above_cap_blur_max_bytes,
false,
5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
});
@@ -89,7 +89,6 @@ async fn relay_to_mask_applies_cap_clamped_padding_for_non_power_of_two_cap() {
0,
false,
5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
});
@@ -53,14 +53,11 @@ fn new_client_harness() -> ClientHarness {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
}],
1,
1,
-853
View File
@@ -1,853 +0,0 @@
use std::collections::{HashMap, HashSet};
use std::hash::{Hash, Hasher};
use std::net::IpAddr;
use std::sync::Arc;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
use arc_swap::ArcSwap;
use dashmap::DashMap;
use ipnetwork::IpNetwork;
use crate::config::RateLimitBps;
const REGISTRY_SHARDS: usize = 64;
const FAIR_EPOCH_MS: u64 = 20;
const MAX_BORROW_CHUNK_BYTES: u64 = 32 * 1024;
const CLEANUP_INTERVAL_SECS: u64 = 60;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RateDirection {
Up,
Down,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TrafficConsumeResult {
pub granted: u64,
pub blocked_user: bool,
pub blocked_cidr: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct TrafficLimiterMetricsSnapshot {
pub user_throttle_up_total: u64,
pub user_throttle_down_total: u64,
pub cidr_throttle_up_total: u64,
pub cidr_throttle_down_total: u64,
pub user_wait_up_ms_total: u64,
pub user_wait_down_ms_total: u64,
pub cidr_wait_up_ms_total: u64,
pub cidr_wait_down_ms_total: u64,
pub user_active_leases: u64,
pub cidr_active_leases: u64,
pub user_policy_entries: u64,
pub cidr_policy_entries: u64,
}
#[derive(Default)]
struct ScopeMetrics {
throttle_up_total: AtomicU64,
throttle_down_total: AtomicU64,
wait_up_ms_total: AtomicU64,
wait_down_ms_total: AtomicU64,
active_leases: AtomicU64,
policy_entries: AtomicU64,
}
impl ScopeMetrics {
fn throttle(&self, direction: RateDirection) {
match direction {
RateDirection::Up => {
self.throttle_up_total.fetch_add(1, Ordering::Relaxed);
}
RateDirection::Down => {
self.throttle_down_total.fetch_add(1, Ordering::Relaxed);
}
}
}
fn wait_ms(&self, direction: RateDirection, wait_ms: u64) {
match direction {
RateDirection::Up => {
self.wait_up_ms_total.fetch_add(wait_ms, Ordering::Relaxed);
}
RateDirection::Down => {
self.wait_down_ms_total
.fetch_add(wait_ms, Ordering::Relaxed);
}
}
}
}
#[derive(Default)]
struct AtomicRatePair {
up_bps: AtomicU64,
down_bps: AtomicU64,
}
impl AtomicRatePair {
fn set(&self, limits: RateLimitBps) {
self.up_bps.store(limits.up_bps, Ordering::Relaxed);
self.down_bps.store(limits.down_bps, Ordering::Relaxed);
}
fn get(&self, direction: RateDirection) -> u64 {
match direction {
RateDirection::Up => self.up_bps.load(Ordering::Relaxed),
RateDirection::Down => self.down_bps.load(Ordering::Relaxed),
}
}
}
#[derive(Default)]
struct DirectionBucket {
epoch: AtomicU64,
used: AtomicU64,
}
impl DirectionBucket {
fn sync_epoch(&self, epoch: u64) {
let current = self.epoch.load(Ordering::Relaxed);
if current == epoch {
return;
}
if current < epoch
&& self
.epoch
.compare_exchange(current, epoch, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
self.used.store(0, Ordering::Relaxed);
}
}
fn try_consume(&self, cap_bps: u64, requested: u64) -> u64 {
if requested == 0 {
return 0;
}
if cap_bps == 0 {
return requested;
}
let epoch = current_epoch();
self.sync_epoch(epoch);
let cap_epoch = bytes_per_epoch(cap_bps);
loop {
let used = self.used.load(Ordering::Relaxed);
if used >= cap_epoch {
return 0;
}
let remaining = cap_epoch.saturating_sub(used);
let grant = requested.min(remaining);
if grant == 0 {
return 0;
}
let next = used.saturating_add(grant);
if self
.used
.compare_exchange_weak(used, next, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
return grant;
}
}
}
fn refund(&self, bytes: u64) {
if bytes == 0 {
return;
}
decrement_atomic_saturating(&self.used, bytes);
}
}
struct UserBucket {
rates: AtomicRatePair,
up: DirectionBucket,
down: DirectionBucket,
active_leases: AtomicU64,
}
impl UserBucket {
fn new(limits: RateLimitBps) -> Self {
let rates = AtomicRatePair::default();
rates.set(limits);
Self {
rates,
up: DirectionBucket::default(),
down: DirectionBucket::default(),
active_leases: AtomicU64::new(0),
}
}
fn set_rates(&self, limits: RateLimitBps) {
self.rates.set(limits);
}
fn try_consume(&self, direction: RateDirection, requested: u64) -> u64 {
let cap_bps = self.rates.get(direction);
match direction {
RateDirection::Up => self.up.try_consume(cap_bps, requested),
RateDirection::Down => self.down.try_consume(cap_bps, requested),
}
}
fn refund(&self, direction: RateDirection, bytes: u64) {
match direction {
RateDirection::Up => self.up.refund(bytes),
RateDirection::Down => self.down.refund(bytes),
}
}
}
#[derive(Default)]
struct CidrDirectionBucket {
epoch: AtomicU64,
used: AtomicU64,
active_users: AtomicU64,
}
impl CidrDirectionBucket {
fn sync_epoch(&self, epoch: u64) {
let current = self.epoch.load(Ordering::Relaxed);
if current == epoch {
return;
}
if current < epoch
&& self
.epoch
.compare_exchange(current, epoch, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
self.used.store(0, Ordering::Relaxed);
self.active_users.store(0, Ordering::Relaxed);
}
}
fn try_consume(
&self,
user_state: &CidrUserDirectionState,
cap_epoch: u64,
requested: u64,
) -> u64 {
if requested == 0 || cap_epoch == 0 {
return 0;
}
let epoch = current_epoch();
self.sync_epoch(epoch);
user_state.sync_epoch_and_mark_active(epoch, &self.active_users);
let active_users = self.active_users.load(Ordering::Relaxed).max(1);
let fair_share = cap_epoch.saturating_div(active_users).max(1);
loop {
let total_used = self.used.load(Ordering::Relaxed);
if total_used >= cap_epoch {
return 0;
}
let total_remaining = cap_epoch.saturating_sub(total_used);
let user_used = user_state.used.load(Ordering::Relaxed);
let guaranteed_remaining = fair_share.saturating_sub(user_used);
let grant = if guaranteed_remaining > 0 {
requested.min(guaranteed_remaining).min(total_remaining)
} else {
requested.min(total_remaining).min(MAX_BORROW_CHUNK_BYTES)
};
if grant == 0 {
return 0;
}
let next_total = total_used.saturating_add(grant);
if self
.used
.compare_exchange_weak(total_used, next_total, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
user_state.used.fetch_add(grant, Ordering::Relaxed);
return grant;
}
}
}
fn refund(&self, bytes: u64) {
if bytes == 0 {
return;
}
decrement_atomic_saturating(&self.used, bytes);
}
}
#[derive(Default)]
struct CidrUserDirectionState {
epoch: AtomicU64,
used: AtomicU64,
}
impl CidrUserDirectionState {
fn sync_epoch_and_mark_active(&self, epoch: u64, active_users: &AtomicU64) {
let current = self.epoch.load(Ordering::Relaxed);
if current == epoch {
return;
}
if current < epoch
&& self
.epoch
.compare_exchange(current, epoch, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
self.used.store(0, Ordering::Relaxed);
active_users.fetch_add(1, Ordering::Relaxed);
}
}
fn refund(&self, bytes: u64) {
if bytes == 0 {
return;
}
decrement_atomic_saturating(&self.used, bytes);
}
}
struct CidrUserShare {
active_conns: AtomicU64,
up: CidrUserDirectionState,
down: CidrUserDirectionState,
}
impl CidrUserShare {
fn new() -> Self {
Self {
active_conns: AtomicU64::new(0),
up: CidrUserDirectionState::default(),
down: CidrUserDirectionState::default(),
}
}
}
struct CidrBucket {
rates: AtomicRatePair,
up: CidrDirectionBucket,
down: CidrDirectionBucket,
users: ShardedRegistry<CidrUserShare>,
active_leases: AtomicU64,
}
impl CidrBucket {
fn new(limits: RateLimitBps) -> Self {
let rates = AtomicRatePair::default();
rates.set(limits);
Self {
rates,
up: CidrDirectionBucket::default(),
down: CidrDirectionBucket::default(),
users: ShardedRegistry::new(REGISTRY_SHARDS),
active_leases: AtomicU64::new(0),
}
}
fn set_rates(&self, limits: RateLimitBps) {
self.rates.set(limits);
}
fn acquire_user_share(&self, user: &str) -> Arc<CidrUserShare> {
let share = self.users.get_or_insert_with(user, CidrUserShare::new);
share.active_conns.fetch_add(1, Ordering::Relaxed);
share
}
fn release_user_share(&self, user: &str, share: &Arc<CidrUserShare>) {
decrement_atomic_saturating(&share.active_conns, 1);
let share_for_remove = Arc::clone(share);
let _ = self.users.remove_if(user, |candidate| {
Arc::ptr_eq(candidate, &share_for_remove)
&& candidate.active_conns.load(Ordering::Relaxed) == 0
});
}
fn try_consume_for_user(
&self,
direction: RateDirection,
share: &CidrUserShare,
requested: u64,
) -> u64 {
let cap_bps = self.rates.get(direction);
if cap_bps == 0 {
return requested;
}
let cap_epoch = bytes_per_epoch(cap_bps);
match direction {
RateDirection::Up => self.up.try_consume(&share.up, cap_epoch, requested),
RateDirection::Down => self.down.try_consume(&share.down, cap_epoch, requested),
}
}
fn refund_for_user(&self, direction: RateDirection, share: &CidrUserShare, bytes: u64) {
match direction {
RateDirection::Up => {
self.up.refund(bytes);
share.up.refund(bytes);
}
RateDirection::Down => {
self.down.refund(bytes);
share.down.refund(bytes);
}
}
}
fn cleanup_idle_users(&self) {
self.users
.retain(|_, share| share.active_conns.load(Ordering::Relaxed) > 0);
}
}
#[derive(Clone)]
struct CidrRule {
key: String,
cidr: IpNetwork,
limits: RateLimitBps,
prefix_len: u8,
}
#[derive(Default)]
struct PolicySnapshot {
user_limits: HashMap<String, RateLimitBps>,
cidr_rules_v4: Vec<CidrRule>,
cidr_rules_v6: Vec<CidrRule>,
cidr_rule_keys: HashSet<String>,
}
impl PolicySnapshot {
fn match_cidr(&self, ip: IpAddr) -> Option<&CidrRule> {
match ip {
IpAddr::V4(_) => self
.cidr_rules_v4
.iter()
.find(|rule| rule.cidr.contains(ip)),
IpAddr::V6(_) => self
.cidr_rules_v6
.iter()
.find(|rule| rule.cidr.contains(ip)),
}
}
}
struct ShardedRegistry<T> {
shards: Box<[DashMap<String, Arc<T>>]>,
mask: usize,
}
impl<T> ShardedRegistry<T> {
fn new(shards: usize) -> Self {
let shard_count = shards.max(1).next_power_of_two();
let mut items = Vec::with_capacity(shard_count);
for _ in 0..shard_count {
items.push(DashMap::<String, Arc<T>>::new());
}
Self {
shards: items.into_boxed_slice(),
mask: shard_count.saturating_sub(1),
}
}
fn shard_index(&self, key: &str) -> usize {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
key.hash(&mut hasher);
(hasher.finish() as usize) & self.mask
}
fn get_or_insert_with<F>(&self, key: &str, make: F) -> Arc<T>
where
F: FnOnce() -> T,
{
let shard = &self.shards[self.shard_index(key)];
match shard.entry(key.to_string()) {
dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
dashmap::mapref::entry::Entry::Vacant(slot) => {
let value = Arc::new(make());
slot.insert(Arc::clone(&value));
value
}
}
}
fn retain<F>(&self, predicate: F)
where
F: Fn(&String, &Arc<T>) -> bool + Copy,
{
for shard in &*self.shards {
shard.retain(|key, value| predicate(key, value));
}
}
fn remove_if<F>(&self, key: &str, predicate: F) -> bool
where
F: Fn(&Arc<T>) -> bool,
{
let shard = &self.shards[self.shard_index(key)];
let should_remove = match shard.get(key) {
Some(entry) => predicate(entry.value()),
None => false,
};
if !should_remove {
return false;
}
shard.remove(key).is_some()
}
}
pub struct TrafficLease {
limiter: Arc<TrafficLimiter>,
user_bucket: Option<Arc<UserBucket>>,
cidr_bucket: Option<Arc<CidrBucket>>,
cidr_user_key: Option<String>,
cidr_user_share: Option<Arc<CidrUserShare>>,
}
impl TrafficLease {
pub fn try_consume(&self, direction: RateDirection, requested: u64) -> TrafficConsumeResult {
if requested == 0 {
return TrafficConsumeResult {
granted: 0,
blocked_user: false,
blocked_cidr: false,
};
}
let mut granted = requested;
if let Some(user_bucket) = self.user_bucket.as_ref() {
let user_granted = user_bucket.try_consume(direction, granted);
if user_granted == 0 {
self.limiter.observe_throttle(direction, true, false);
return TrafficConsumeResult {
granted: 0,
blocked_user: true,
blocked_cidr: false,
};
}
granted = user_granted;
}
if let (Some(cidr_bucket), Some(cidr_user_share)) =
(self.cidr_bucket.as_ref(), self.cidr_user_share.as_ref())
{
let cidr_granted =
cidr_bucket.try_consume_for_user(direction, cidr_user_share, granted);
if cidr_granted < granted
&& let Some(user_bucket) = self.user_bucket.as_ref()
{
user_bucket.refund(direction, granted.saturating_sub(cidr_granted));
}
if cidr_granted == 0 {
self.limiter.observe_throttle(direction, false, true);
return TrafficConsumeResult {
granted: 0,
blocked_user: false,
blocked_cidr: true,
};
}
granted = cidr_granted;
}
TrafficConsumeResult {
granted,
blocked_user: false,
blocked_cidr: false,
}
}
pub fn refund(&self, direction: RateDirection, bytes: u64) {
if bytes == 0 {
return;
}
if let Some(user_bucket) = self.user_bucket.as_ref() {
user_bucket.refund(direction, bytes);
}
if let (Some(cidr_bucket), Some(cidr_user_share)) =
(self.cidr_bucket.as_ref(), self.cidr_user_share.as_ref())
{
cidr_bucket.refund_for_user(direction, cidr_user_share, bytes);
}
}
pub fn observe_wait_ms(
&self,
direction: RateDirection,
blocked_user: bool,
blocked_cidr: bool,
wait_ms: u64,
) {
if wait_ms == 0 {
return;
}
self.limiter
.observe_wait(direction, blocked_user, blocked_cidr, wait_ms);
}
}
impl Drop for TrafficLease {
fn drop(&mut self) {
if let Some(bucket) = self.user_bucket.as_ref() {
decrement_atomic_saturating(&bucket.active_leases, 1);
decrement_atomic_saturating(&self.limiter.user_scope.active_leases, 1);
}
if let Some(bucket) = self.cidr_bucket.as_ref() {
if let (Some(user_key), Some(share)) =
(self.cidr_user_key.as_ref(), self.cidr_user_share.as_ref())
{
bucket.release_user_share(user_key, share);
}
decrement_atomic_saturating(&bucket.active_leases, 1);
decrement_atomic_saturating(&self.limiter.cidr_scope.active_leases, 1);
}
}
}
pub struct TrafficLimiter {
policy: ArcSwap<PolicySnapshot>,
user_buckets: ShardedRegistry<UserBucket>,
cidr_buckets: ShardedRegistry<CidrBucket>,
user_scope: ScopeMetrics,
cidr_scope: ScopeMetrics,
last_cleanup_epoch_secs: AtomicU64,
}
impl TrafficLimiter {
pub fn new() -> Arc<Self> {
Arc::new(Self {
policy: ArcSwap::from_pointee(PolicySnapshot::default()),
user_buckets: ShardedRegistry::new(REGISTRY_SHARDS),
cidr_buckets: ShardedRegistry::new(REGISTRY_SHARDS),
user_scope: ScopeMetrics::default(),
cidr_scope: ScopeMetrics::default(),
last_cleanup_epoch_secs: AtomicU64::new(0),
})
}
pub fn apply_policy(
&self,
user_limits: HashMap<String, RateLimitBps>,
cidr_limits: HashMap<IpNetwork, RateLimitBps>,
) {
let filtered_users = user_limits
.into_iter()
.filter(|(_, limit)| limit.up_bps > 0 || limit.down_bps > 0)
.collect::<HashMap<_, _>>();
let mut cidr_rules_v4 = Vec::new();
let mut cidr_rules_v6 = Vec::new();
let mut cidr_rule_keys = HashSet::new();
for (cidr, limits) in cidr_limits {
if limits.up_bps == 0 && limits.down_bps == 0 {
continue;
}
let key = cidr.to_string();
let rule = CidrRule {
key: key.clone(),
cidr,
limits,
prefix_len: cidr.prefix(),
};
cidr_rule_keys.insert(key);
match rule.cidr {
IpNetwork::V4(_) => cidr_rules_v4.push(rule),
IpNetwork::V6(_) => cidr_rules_v6.push(rule),
}
}
cidr_rules_v4.sort_by(|a, b| b.prefix_len.cmp(&a.prefix_len));
cidr_rules_v6.sort_by(|a, b| b.prefix_len.cmp(&a.prefix_len));
self.user_scope
.policy_entries
.store(filtered_users.len() as u64, Ordering::Relaxed);
self.cidr_scope
.policy_entries
.store(cidr_rule_keys.len() as u64, Ordering::Relaxed);
self.policy.store(Arc::new(PolicySnapshot {
user_limits: filtered_users,
cidr_rules_v4,
cidr_rules_v6,
cidr_rule_keys,
}));
self.maybe_cleanup();
}
pub fn acquire_lease(
self: &Arc<Self>,
user: &str,
client_ip: IpAddr,
) -> Option<Arc<TrafficLease>> {
let policy = self.policy.load_full();
let mut user_bucket = None;
if let Some(limit) = policy.user_limits.get(user).copied() {
let bucket = self
.user_buckets
.get_or_insert_with(user, || UserBucket::new(limit));
bucket.set_rates(limit);
bucket.active_leases.fetch_add(1, Ordering::Relaxed);
self.user_scope
.active_leases
.fetch_add(1, Ordering::Relaxed);
user_bucket = Some(bucket);
}
let mut cidr_bucket = None;
let mut cidr_user_key = None;
let mut cidr_user_share = None;
if let Some(rule) = policy.match_cidr(client_ip) {
let bucket = self
.cidr_buckets
.get_or_insert_with(rule.key.as_str(), || CidrBucket::new(rule.limits));
bucket.set_rates(rule.limits);
bucket.active_leases.fetch_add(1, Ordering::Relaxed);
self.cidr_scope
.active_leases
.fetch_add(1, Ordering::Relaxed);
let share = bucket.acquire_user_share(user);
cidr_user_key = Some(user.to_string());
cidr_user_share = Some(share);
cidr_bucket = Some(bucket);
}
if user_bucket.is_none() && cidr_bucket.is_none() {
return None;
}
self.maybe_cleanup();
Some(Arc::new(TrafficLease {
limiter: Arc::clone(self),
user_bucket,
cidr_bucket,
cidr_user_key,
cidr_user_share,
}))
}
pub fn metrics_snapshot(&self) -> TrafficLimiterMetricsSnapshot {
TrafficLimiterMetricsSnapshot {
user_throttle_up_total: self.user_scope.throttle_up_total.load(Ordering::Relaxed),
user_throttle_down_total: self.user_scope.throttle_down_total.load(Ordering::Relaxed),
cidr_throttle_up_total: self.cidr_scope.throttle_up_total.load(Ordering::Relaxed),
cidr_throttle_down_total: self.cidr_scope.throttle_down_total.load(Ordering::Relaxed),
user_wait_up_ms_total: self.user_scope.wait_up_ms_total.load(Ordering::Relaxed),
user_wait_down_ms_total: self.user_scope.wait_down_ms_total.load(Ordering::Relaxed),
cidr_wait_up_ms_total: self.cidr_scope.wait_up_ms_total.load(Ordering::Relaxed),
cidr_wait_down_ms_total: self.cidr_scope.wait_down_ms_total.load(Ordering::Relaxed),
user_active_leases: self.user_scope.active_leases.load(Ordering::Relaxed),
cidr_active_leases: self.cidr_scope.active_leases.load(Ordering::Relaxed),
user_policy_entries: self.user_scope.policy_entries.load(Ordering::Relaxed),
cidr_policy_entries: self.cidr_scope.policy_entries.load(Ordering::Relaxed),
}
}
fn observe_throttle(&self, direction: RateDirection, blocked_user: bool, blocked_cidr: bool) {
if blocked_user {
self.user_scope.throttle(direction);
}
if blocked_cidr {
self.cidr_scope.throttle(direction);
}
}
fn observe_wait(
&self,
direction: RateDirection,
blocked_user: bool,
blocked_cidr: bool,
wait_ms: u64,
) {
if blocked_user {
self.user_scope.wait_ms(direction, wait_ms);
}
if blocked_cidr {
self.cidr_scope.wait_ms(direction, wait_ms);
}
}
fn maybe_cleanup(&self) {
let now_epoch_secs = now_epoch_secs();
let last = self.last_cleanup_epoch_secs.load(Ordering::Relaxed);
if now_epoch_secs.saturating_sub(last) < CLEANUP_INTERVAL_SECS {
return;
}
if self
.last_cleanup_epoch_secs
.compare_exchange(last, now_epoch_secs, Ordering::Relaxed, Ordering::Relaxed)
.is_err()
{
return;
}
let policy = self.policy.load_full();
self.user_buckets.retain(|user, bucket| {
bucket.active_leases.load(Ordering::Relaxed) > 0
|| policy.user_limits.contains_key(user)
});
self.cidr_buckets.retain(|cidr_key, bucket| {
bucket.cleanup_idle_users();
bucket.active_leases.load(Ordering::Relaxed) > 0
|| policy.cidr_rule_keys.contains(cidr_key)
});
}
}
pub fn next_refill_delay() -> Duration {
let start = limiter_epoch_start();
let elapsed_ms = start.elapsed().as_millis() as u64;
let epoch_pos = elapsed_ms % FAIR_EPOCH_MS;
let wait_ms = FAIR_EPOCH_MS.saturating_sub(epoch_pos).max(1);
Duration::from_millis(wait_ms)
}
fn decrement_atomic_saturating(counter: &AtomicU64, by: u64) {
if by == 0 {
return;
}
let mut current = counter.load(Ordering::Relaxed);
loop {
if current == 0 {
return;
}
let next = current.saturating_sub(by);
match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
Ok(_) => return,
Err(actual) => current = actual,
}
}
}
fn now_epoch_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs()
}
fn bytes_per_epoch(bps: u64) -> u64 {
if bps == 0 {
return 0;
}
let numerator = bps.saturating_mul(FAIR_EPOCH_MS);
let bytes = numerator.saturating_div(8_000);
bytes.max(1)
}
fn current_epoch() -> u64 {
let start = limiter_epoch_start();
let elapsed_ms = start.elapsed().as_millis() as u64;
elapsed_ms / FAIR_EPOCH_MS
}
fn limiter_epoch_start() -> &'static Instant {
static START: OnceLock<Instant> = OnceLock::new();
START.get_or_init(Instant::now)
}
-121
View File
@@ -175,18 +175,6 @@ pub struct Stats {
me_route_drop_queue_full: AtomicU64,
me_route_drop_queue_full_base: AtomicU64,
me_route_drop_queue_full_high: AtomicU64,
me_fair_pressure_state_gauge: AtomicU64,
me_fair_active_flows_gauge: AtomicU64,
me_fair_queued_bytes_gauge: AtomicU64,
me_fair_standing_flows_gauge: AtomicU64,
me_fair_backpressured_flows_gauge: AtomicU64,
me_fair_scheduler_rounds_total: AtomicU64,
me_fair_deficit_grants_total: AtomicU64,
me_fair_deficit_skips_total: AtomicU64,
me_fair_enqueue_rejects_total: AtomicU64,
me_fair_shed_drops_total: AtomicU64,
me_fair_penalties_total: AtomicU64,
me_fair_downstream_stalls_total: AtomicU64,
me_d2c_batches_total: AtomicU64,
me_d2c_batch_frames_total: AtomicU64,
me_d2c_batch_bytes_total: AtomicU64,
@@ -868,78 +856,6 @@ impl Stats {
.fetch_add(1, Ordering::Relaxed);
}
}
pub fn set_me_fair_pressure_state_gauge(&self, value: u64) {
if self.telemetry_me_allows_normal() {
self.me_fair_pressure_state_gauge
.store(value, Ordering::Relaxed);
}
}
pub fn set_me_fair_active_flows_gauge(&self, value: u64) {
if self.telemetry_me_allows_normal() {
self.me_fair_active_flows_gauge
.store(value, Ordering::Relaxed);
}
}
pub fn set_me_fair_queued_bytes_gauge(&self, value: u64) {
if self.telemetry_me_allows_normal() {
self.me_fair_queued_bytes_gauge
.store(value, Ordering::Relaxed);
}
}
pub fn set_me_fair_standing_flows_gauge(&self, value: u64) {
if self.telemetry_me_allows_normal() {
self.me_fair_standing_flows_gauge
.store(value, Ordering::Relaxed);
}
}
pub fn set_me_fair_backpressured_flows_gauge(&self, value: u64) {
if self.telemetry_me_allows_normal() {
self.me_fair_backpressured_flows_gauge
.store(value, Ordering::Relaxed);
}
}
pub fn add_me_fair_scheduler_rounds_total(&self, value: u64) {
if self.telemetry_me_allows_normal() && value > 0 {
self.me_fair_scheduler_rounds_total
.fetch_add(value, Ordering::Relaxed);
}
}
pub fn add_me_fair_deficit_grants_total(&self, value: u64) {
if self.telemetry_me_allows_normal() && value > 0 {
self.me_fair_deficit_grants_total
.fetch_add(value, Ordering::Relaxed);
}
}
pub fn add_me_fair_deficit_skips_total(&self, value: u64) {
if self.telemetry_me_allows_normal() && value > 0 {
self.me_fair_deficit_skips_total
.fetch_add(value, Ordering::Relaxed);
}
}
pub fn add_me_fair_enqueue_rejects_total(&self, value: u64) {
if self.telemetry_me_allows_normal() && value > 0 {
self.me_fair_enqueue_rejects_total
.fetch_add(value, Ordering::Relaxed);
}
}
pub fn add_me_fair_shed_drops_total(&self, value: u64) {
if self.telemetry_me_allows_normal() && value > 0 {
self.me_fair_shed_drops_total
.fetch_add(value, Ordering::Relaxed);
}
}
pub fn add_me_fair_penalties_total(&self, value: u64) {
if self.telemetry_me_allows_normal() && value > 0 {
self.me_fair_penalties_total
.fetch_add(value, Ordering::Relaxed);
}
}
pub fn add_me_fair_downstream_stalls_total(&self, value: u64) {
if self.telemetry_me_allows_normal() && value > 0 {
self.me_fair_downstream_stalls_total
.fetch_add(value, Ordering::Relaxed);
}
}
pub fn increment_me_d2c_batches_total(&self) {
if self.telemetry_me_allows_normal() {
self.me_d2c_batches_total.fetch_add(1, Ordering::Relaxed);
@@ -1890,43 +1806,6 @@ impl Stats {
pub fn get_me_route_drop_queue_full_high(&self) -> u64 {
self.me_route_drop_queue_full_high.load(Ordering::Relaxed)
}
pub fn get_me_fair_pressure_state_gauge(&self) -> u64 {
self.me_fair_pressure_state_gauge.load(Ordering::Relaxed)
}
pub fn get_me_fair_active_flows_gauge(&self) -> u64 {
self.me_fair_active_flows_gauge.load(Ordering::Relaxed)
}
pub fn get_me_fair_queued_bytes_gauge(&self) -> u64 {
self.me_fair_queued_bytes_gauge.load(Ordering::Relaxed)
}
pub fn get_me_fair_standing_flows_gauge(&self) -> u64 {
self.me_fair_standing_flows_gauge.load(Ordering::Relaxed)
}
pub fn get_me_fair_backpressured_flows_gauge(&self) -> u64 {
self.me_fair_backpressured_flows_gauge
.load(Ordering::Relaxed)
}
pub fn get_me_fair_scheduler_rounds_total(&self) -> u64 {
self.me_fair_scheduler_rounds_total.load(Ordering::Relaxed)
}
pub fn get_me_fair_deficit_grants_total(&self) -> u64 {
self.me_fair_deficit_grants_total.load(Ordering::Relaxed)
}
pub fn get_me_fair_deficit_skips_total(&self) -> u64 {
self.me_fair_deficit_skips_total.load(Ordering::Relaxed)
}
pub fn get_me_fair_enqueue_rejects_total(&self) -> u64 {
self.me_fair_enqueue_rejects_total.load(Ordering::Relaxed)
}
pub fn get_me_fair_shed_drops_total(&self) -> u64 {
self.me_fair_shed_drops_total.load(Ordering::Relaxed)
}
pub fn get_me_fair_penalties_total(&self) -> u64 {
self.me_fair_penalties_total.load(Ordering::Relaxed)
}
pub fn get_me_fair_downstream_stalls_total(&self) -> u64 {
self.me_fair_downstream_stalls_total.load(Ordering::Relaxed)
}
pub fn get_me_d2c_batches_total(&self) -> u64 {
self.me_d2c_batches_total.load(Ordering::Relaxed)
}
+41 -93
View File
@@ -11,7 +11,6 @@ use crc32fast::Hasher;
const MIN_APP_DATA: usize = 64;
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
const MAX_TICKET_RECORDS: usize = 4;
fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> {
sizes
@@ -63,64 +62,6 @@ fn ensure_payload_capacity(mut sizes: Vec<usize>, payload_len: usize) -> Vec<usi
sizes
}
fn emulated_app_data_sizes(cached: &CachedTlsData) -> Vec<usize> {
match cached.behavior_profile.source {
TlsProfileSource::Raw | TlsProfileSource::Merged => {
return cached
.app_data_records_sizes
.first()
.copied()
.or_else(|| {
cached
.behavior_profile
.app_data_record_sizes
.first()
.copied()
})
.map(|size| vec![size])
.unwrap_or_else(|| vec![cached.total_app_data_len.max(1024)]);
}
TlsProfileSource::Default | TlsProfileSource::Rustls => {}
}
let mut sizes = cached.app_data_records_sizes.clone();
if sizes.is_empty() {
sizes.push(cached.total_app_data_len.max(1024));
}
sizes
}
fn emulated_change_cipher_spec_count(_cached: &CachedTlsData) -> usize {
1
}
fn emulated_ticket_record_sizes(
cached: &CachedTlsData,
new_session_tickets: u8,
rng: &SecureRandom,
) -> Vec<usize> {
let target_count = usize::from(new_session_tickets.min(MAX_TICKET_RECORDS as u8));
if target_count == 0 {
return Vec::new();
}
let profiled_sizes = match cached.behavior_profile.source {
TlsProfileSource::Raw | TlsProfileSource::Merged => {
cached.behavior_profile.ticket_record_sizes.as_slice()
}
TlsProfileSource::Default | TlsProfileSource::Rustls => &[],
};
let mut sizes = Vec::with_capacity(target_count);
sizes.extend(profiled_sizes.iter().copied().take(target_count));
while sizes.len() < target_count {
sizes.push(rng.range(48) + 48);
}
sizes
}
fn build_compact_cert_info_payload(cert_info: &ParsedCertificateInfo) -> Option<Vec<u8>> {
let mut fields = Vec::new();
@@ -239,32 +180,39 @@ pub fn build_emulated_server_hello(
server_hello.extend_from_slice(&message);
// --- ChangeCipherSpec ---
let change_cipher_spec_count = emulated_change_cipher_spec_count(cached);
let mut change_cipher_spec = Vec::with_capacity(change_cipher_spec_count * 6);
for _ in 0..change_cipher_spec_count {
change_cipher_spec.extend_from_slice(&[
TLS_RECORD_CHANGE_CIPHER,
TLS_VERSION[0],
TLS_VERSION[1],
0x00,
0x01,
0x01,
]);
}
let change_cipher_spec = [
TLS_RECORD_CHANGE_CIPHER,
TLS_VERSION[0],
TLS_VERSION[1],
0x00,
0x01,
0x01,
];
// --- ApplicationData (fake encrypted records) ---
let mut sizes = {
let base_sizes = emulated_app_data_sizes(cached);
match cached.behavior_profile.source {
TlsProfileSource::Raw | TlsProfileSource::Merged => base_sizes
.into_iter()
.map(|size| size.clamp(MIN_APP_DATA, MAX_APP_DATA))
.collect(),
TlsProfileSource::Default | TlsProfileSource::Rustls => {
jitter_and_clamp_sizes(&base_sizes, rng)
let sizes = match cached.behavior_profile.source {
TlsProfileSource::Raw | TlsProfileSource::Merged => cached
.app_data_records_sizes
.first()
.copied()
.or_else(|| {
cached
.behavior_profile
.app_data_record_sizes
.first()
.copied()
})
.map(|size| vec![size])
.unwrap_or_else(|| vec![cached.total_app_data_len.max(1024)]),
_ => {
let mut sizes = cached.app_data_records_sizes.clone();
if sizes.is_empty() {
sizes.push(cached.total_app_data_len.max(1024));
}
sizes
}
};
let mut sizes = jitter_and_clamp_sizes(&sizes, rng);
let compact_payload = cached
.cert_info
.as_ref()
@@ -351,13 +299,17 @@ pub fn build_emulated_server_hello(
// --- Combine ---
// Optional NewSessionTicket mimic records (opaque ApplicationData for fingerprint).
let mut tickets = Vec::new();
for ticket_len in emulated_ticket_record_sizes(cached, new_session_tickets, rng) {
let mut rec = Vec::with_capacity(5 + ticket_len);
rec.push(TLS_RECORD_APPLICATION);
rec.extend_from_slice(&TLS_VERSION);
rec.extend_from_slice(&(ticket_len as u16).to_be_bytes());
rec.extend_from_slice(&rng.bytes(ticket_len));
tickets.extend_from_slice(&rec);
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;
let mut rec = Vec::with_capacity(5 + ticket_len);
rec.push(TLS_RECORD_APPLICATION);
rec.extend_from_slice(&TLS_VERSION);
rec.extend_from_slice(&(ticket_len as u16).to_be_bytes());
rec.extend_from_slice(&rng.bytes(ticket_len));
tickets.extend_from_slice(&rec);
}
}
let mut response = Vec::with_capacity(
@@ -382,10 +334,6 @@ pub fn build_emulated_server_hello(
#[path = "tests/emulator_security_tests.rs"]
mod security_tests;
#[cfg(test)]
#[path = "tests/emulator_profile_fidelity_security_tests.rs"]
mod emulator_profile_fidelity_security_tests;
#[cfg(test)]
mod tests {
use std::time::SystemTime;
@@ -530,7 +478,7 @@ mod tests {
}
#[test]
fn test_build_emulated_server_hello_ignores_tail_records_for_profiled_tls() {
fn test_build_emulated_server_hello_ignores_tail_records_for_raw_profile() {
let mut cached = make_cached(None);
cached.app_data_records_sizes = vec![27, 3905, 537, 69];
cached.total_app_data_len = 4538;
@@ -555,8 +503,8 @@ mod tests {
let app_start = ccs_start + 6;
let app_len =
u16::from_be_bytes([response[app_start + 3], response[app_start + 4]]) as usize;
assert_eq!(response[app_start], TLS_RECORD_APPLICATION);
assert_eq!(app_len, 64);
assert_eq!(app_start + 5 + app_len, response.len());
}
}
+18 -108
View File
@@ -1,7 +1,6 @@
#![allow(clippy::too_many_arguments)]
use dashmap::DashMap;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
@@ -794,51 +793,6 @@ async fn connect_tcp_with_upstream(
))
}
fn socket_addrs_from_upstream_stream(
stream: &UpstreamStream,
) -> (Option<SocketAddr>, Option<SocketAddr>) {
match stream {
UpstreamStream::Tcp(tcp) => (tcp.local_addr().ok(), tcp.peer_addr().ok()),
UpstreamStream::Shadowsocks(_) => (None, None),
}
}
fn build_tls_fetch_proxy_header(
proxy_protocol: u8,
src_addr: Option<SocketAddr>,
dst_addr: Option<SocketAddr>,
) -> Option<Vec<u8>> {
match proxy_protocol {
0 => None,
2 => {
let header = match (src_addr, dst_addr) {
(Some(src @ SocketAddr::V4(_)), Some(dst @ SocketAddr::V4(_)))
| (Some(src @ SocketAddr::V6(_)), Some(dst @ SocketAddr::V6(_))) => {
ProxyProtocolV2Builder::new().with_addrs(src, dst).build()
}
_ => ProxyProtocolV2Builder::new().build(),
};
Some(header)
}
_ => {
let header = match (src_addr, dst_addr) {
(Some(SocketAddr::V4(src)), Some(SocketAddr::V4(dst))) => {
ProxyProtocolV1Builder::new()
.tcp4(src.into(), dst.into())
.build()
}
(Some(SocketAddr::V6(src)), Some(SocketAddr::V6(dst))) => {
ProxyProtocolV1Builder::new()
.tcp6(src.into(), dst.into())
.build()
}
_ => ProxyProtocolV1Builder::new().build(),
};
Some(header)
}
}
}
fn encode_tls13_certificate_message(cert_chain_der: &[Vec<u8>]) -> Option<Vec<u8>> {
if cert_chain_der.is_empty() {
return None;
@@ -870,7 +824,7 @@ async fn fetch_via_raw_tls_stream<S>(
mut stream: S,
sni: &str,
connect_timeout: Duration,
proxy_header: Option<Vec<u8>>,
proxy_protocol: u8,
profile: TlsFetchProfile,
grease_enabled: bool,
deterministic: bool,
@@ -881,7 +835,11 @@ where
let rng = SecureRandom::new();
let client_hello = build_client_hello(sni, &rng, profile, grease_enabled, deterministic);
timeout(connect_timeout, async {
if let Some(header) = proxy_header.as_ref() {
if proxy_protocol > 0 {
let header = match proxy_protocol {
2 => ProxyProtocolV2Builder::new().build(),
_ => ProxyProtocolV1Builder::new().build(),
};
stream.write_all(&header).await?;
}
stream.write_all(&client_hello).await?;
@@ -963,12 +921,11 @@ async fn fetch_via_raw_tls(
sock = %sock_path,
"Raw TLS fetch using mask unix socket"
);
let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, None, None);
return fetch_via_raw_tls_stream(
stream,
sni,
connect_timeout,
proxy_header,
proxy_protocol,
profile,
grease_enabled,
deterministic,
@@ -999,13 +956,11 @@ async fn fetch_via_raw_tls(
let stream =
connect_tcp_with_upstream(host, port, connect_timeout, upstream, scope, strict_route)
.await?;
let (src_addr, dst_addr) = socket_addrs_from_upstream_stream(&stream);
let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, src_addr, dst_addr);
fetch_via_raw_tls_stream(
stream,
sni,
connect_timeout,
proxy_header,
proxy_protocol,
profile,
grease_enabled,
deterministic,
@@ -1017,13 +972,17 @@ async fn fetch_via_rustls_stream<S>(
mut stream: S,
host: &str,
sni: &str,
proxy_header: Option<Vec<u8>>,
proxy_protocol: u8,
) -> Result<TlsFetchResult>
where
S: AsyncRead + AsyncWrite + Unpin,
{
// rustls handshake path for certificate and basic negotiated metadata.
if let Some(header) = proxy_header.as_ref() {
if proxy_protocol > 0 {
let header = match proxy_protocol {
2 => ProxyProtocolV2Builder::new().build(),
_ => ProxyProtocolV1Builder::new().build(),
};
stream.write_all(&header).await?;
stream.flush().await?;
}
@@ -1123,8 +1082,7 @@ async fn fetch_via_rustls(
sock = %sock_path,
"Rustls fetch using mask unix socket"
);
let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, None, None);
return fetch_via_rustls_stream(stream, host, sni, proxy_header).await;
return fetch_via_rustls_stream(stream, host, sni, proxy_protocol).await;
}
Ok(Err(e)) => {
warn!(
@@ -1150,9 +1108,7 @@ async fn fetch_via_rustls(
let stream =
connect_tcp_with_upstream(host, port, connect_timeout, upstream, scope, strict_route)
.await?;
let (src_addr, dst_addr) = socket_addrs_from_upstream_stream(&stream);
let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, src_addr, dst_addr);
fetch_via_rustls_stream(stream, host, sni, proxy_header).await
fetch_via_rustls_stream(stream, host, sni, proxy_protocol).await
}
/// Fetch real TLS metadata with an adaptive multi-profile strategy.
@@ -1322,13 +1278,11 @@ pub async fn fetch_real_tls(
#[cfg(test)]
mod tests {
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use super::{
ProfileCacheValue, TlsFetchStrategy, build_client_hello, build_tls_fetch_proxy_header,
derive_behavior_profile, encode_tls13_certificate_message, order_profiles, profile_cache,
profile_cache_key,
ProfileCacheValue, TlsFetchStrategy, build_client_hello, derive_behavior_profile,
encode_tls13_certificate_message, order_profiles, profile_cache, profile_cache_key,
};
use crate::config::TlsFetchProfile;
use crate::crypto::SecureRandom;
@@ -1469,48 +1423,4 @@ mod tests {
assert_eq!(first, second);
}
#[test]
fn test_build_tls_fetch_proxy_header_v2_with_tcp_addrs() {
let src: SocketAddr = "198.51.100.10:42000".parse().expect("valid src");
let dst: SocketAddr = "203.0.113.20:443".parse().expect("valid dst");
let header = build_tls_fetch_proxy_header(2, Some(src), Some(dst)).expect("header");
assert_eq!(
&header[..12],
&[
0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a
]
);
assert_eq!(header[12], 0x21);
assert_eq!(header[13], 0x11);
assert_eq!(u16::from_be_bytes([header[14], header[15]]), 12);
assert_eq!(&header[16..20], &[198, 51, 100, 10]);
assert_eq!(&header[20..24], &[203, 0, 113, 20]);
assert_eq!(u16::from_be_bytes([header[24], header[25]]), 42000);
assert_eq!(u16::from_be_bytes([header[26], header[27]]), 443);
}
#[test]
fn test_build_tls_fetch_proxy_header_v2_mixed_family_falls_back_to_local_command() {
let src: SocketAddr = "198.51.100.10:42000".parse().expect("valid src");
let dst: SocketAddr = "[2001:db8::20]:443".parse().expect("valid dst");
let header = build_tls_fetch_proxy_header(2, Some(src), Some(dst)).expect("header");
assert_eq!(header[12], 0x20);
assert_eq!(header[13], 0x00);
assert_eq!(u16::from_be_bytes([header[14], header[15]]), 0);
}
#[test]
fn test_build_tls_fetch_proxy_header_v1_with_tcp_addrs() {
let src: SocketAddr = "198.51.100.10:42000".parse().expect("valid src");
let dst: SocketAddr = "203.0.113.20:443".parse().expect("valid dst");
let header = build_tls_fetch_proxy_header(1, Some(src), Some(dst)).expect("header");
assert_eq!(
header,
b"PROXY TCP4 198.51.100.10 203.0.113.20 42000 443\r\n"
);
}
}
@@ -1,114 +0,0 @@
use std::time::SystemTime;
use crate::crypto::SecureRandom;
use crate::protocol::constants::{
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
};
use crate::tls_front::emulator::build_emulated_server_hello;
use crate::tls_front::types::{
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsProfileSource,
};
fn make_cached() -> CachedTlsData {
CachedTlsData {
server_hello_template: ParsedServerHello {
version: [0x03, 0x03],
random: [0u8; 32],
session_id: Vec::new(),
cipher_suite: [0x13, 0x01],
compression: 0,
extensions: Vec::new(),
},
cert_info: None,
cert_payload: None,
app_data_records_sizes: vec![1200, 900, 220, 180],
total_app_data_len: 2500,
behavior_profile: TlsBehaviorProfile {
change_cipher_spec_count: 2,
app_data_record_sizes: vec![1200, 900],
ticket_record_sizes: vec![220, 180],
source: TlsProfileSource::Merged,
},
fetched_at: SystemTime::now(),
domain: "example.com".to_string(),
}
}
fn record_lengths_by_type(response: &[u8], wanted_type: u8) -> Vec<usize> {
let mut out = Vec::new();
let mut pos = 0usize;
while pos + 5 <= response.len() {
let record_type = response[pos];
let record_len = u16::from_be_bytes([response[pos + 3], response[pos + 4]]) as usize;
if pos + 5 + record_len > response.len() {
break;
}
if record_type == wanted_type {
out.push(record_len);
}
pos += 5 + record_len;
}
out
}
#[test]
fn emulated_server_hello_keeps_single_change_cipher_spec_for_client_compatibility() {
let cached = make_cached();
let rng = SecureRandom::new();
let response = build_emulated_server_hello(
b"secret",
&[0x71; 32],
&[0x72; 16],
&cached,
false,
&rng,
None,
0,
);
assert_eq!(response[0], TLS_RECORD_HANDSHAKE);
let ccs_records = record_lengths_by_type(&response, TLS_RECORD_CHANGE_CIPHER);
assert_eq!(ccs_records.len(), 1);
assert!(ccs_records.iter().all(|len| *len == 1));
}
#[test]
fn emulated_server_hello_does_not_emit_profile_ticket_tail_when_disabled() {
let cached = make_cached();
let rng = SecureRandom::new();
let response = build_emulated_server_hello(
b"secret",
&[0x81; 32],
&[0x82; 16],
&cached,
false,
&rng,
None,
0,
);
let app_records = record_lengths_by_type(&response, TLS_RECORD_APPLICATION);
assert_eq!(app_records, vec![1200]);
}
#[test]
fn emulated_server_hello_uses_profile_ticket_lengths_when_enabled() {
let cached = make_cached();
let rng = SecureRandom::new();
let response = build_emulated_server_hello(
b"secret",
&[0x91; 32],
&[0x92; 16],
&cached,
false,
&rng,
None,
2,
);
let app_records = record_lengths_by_type(&response, TLS_RECORD_APPLICATION);
assert_eq!(app_records, vec![1200, 220, 180]);
}
+2 -13
View File
@@ -321,14 +321,7 @@ async fn run_update_cycle(
let mut maps_changed = false;
let mut ready_v4: Option<(ProxyConfigData, u64)> = None;
let cfg_v4 = retry_fetch(
cfg.general
.proxy_config_v4_url
.as_deref()
.unwrap_or("https://core.telegram.org/getProxyConfig"),
upstream.clone(),
)
.await;
let cfg_v4 = retry_fetch("https://core.telegram.org/getProxyConfig", upstream.clone()).await;
if let Some(cfg_v4) = cfg_v4
&& snapshot_passes_guards(cfg, &cfg_v4, "getProxyConfig")
{
@@ -353,10 +346,7 @@ async fn run_update_cycle(
let mut ready_v6: Option<(ProxyConfigData, u64)> = None;
let cfg_v6 = retry_fetch(
cfg.general
.proxy_config_v6_url
.as_deref()
.unwrap_or("https://core.telegram.org/getProxyConfigV6"),
"https://core.telegram.org/getProxyConfigV6",
upstream.clone(),
)
.await;
@@ -440,7 +430,6 @@ async fn run_update_cycle(
match download_proxy_secret_with_max_len_via_upstream(
cfg.general.proxy_secret_len_max,
upstream,
cfg.general.proxy_secret_url.as_deref(),
)
.await
{
@@ -1,12 +0,0 @@
//! Backpressure-driven fairness control for ME reader routing.
//!
//! This module keeps fairness decisions worker-local:
//! each reader loop owns one scheduler instance and mutates it without locks.
mod model;
mod pressure;
mod scheduler;
pub(crate) use model::PressureState;
pub(crate) use model::{AdmissionDecision, DispatchAction, DispatchFeedback, SchedulerDecision};
pub(crate) use scheduler::{WorkerFairnessConfig, WorkerFairnessSnapshot, WorkerFairnessState};
@@ -1,142 +0,0 @@
use std::time::Instant;
use bytes::Bytes;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub(crate) enum PressureState {
Normal = 0,
Pressured = 1,
Shedding = 2,
Saturated = 3,
}
impl PressureState {
pub(crate) fn as_u8(self) -> u8 {
self as u8
}
}
impl Default for PressureState {
fn default() -> Self {
Self::Normal
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FlowPressureClass {
Healthy,
Bursty,
Backpressured,
Standing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StandingQueueState {
Transient,
Standing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum FlowSchedulerState {
Idle,
Active,
Backpressured,
Penalized,
SheddingCandidate,
}
#[derive(Debug, Clone)]
pub(crate) struct QueuedFrame {
pub(crate) conn_id: u64,
pub(crate) flags: u32,
pub(crate) data: Bytes,
pub(crate) enqueued_at: Instant,
}
impl QueuedFrame {
#[inline]
pub(crate) fn queued_bytes(&self) -> u64 {
self.data.len() as u64
}
}
#[derive(Debug, Clone)]
pub(crate) struct FlowFairnessState {
pub(crate) _flow_id: u64,
pub(crate) _worker_id: u16,
pub(crate) pending_bytes: u64,
pub(crate) deficit_bytes: i64,
pub(crate) queue_started_at: Option<Instant>,
pub(crate) last_drain_at: Option<Instant>,
pub(crate) recent_drain_bytes: u64,
pub(crate) consecutive_stalls: u8,
pub(crate) consecutive_skips: u8,
pub(crate) penalty_score: u16,
pub(crate) pressure_class: FlowPressureClass,
pub(crate) standing_state: StandingQueueState,
pub(crate) scheduler_state: FlowSchedulerState,
pub(crate) bucket_id: usize,
pub(crate) weight_quanta: u8,
pub(crate) in_active_ring: bool,
}
impl FlowFairnessState {
pub(crate) fn new(flow_id: u64, worker_id: u16, bucket_id: usize, weight_quanta: u8) -> Self {
Self {
_flow_id: flow_id,
_worker_id: worker_id,
pending_bytes: 0,
deficit_bytes: 0,
queue_started_at: None,
last_drain_at: None,
recent_drain_bytes: 0,
consecutive_stalls: 0,
consecutive_skips: 0,
penalty_score: 0,
pressure_class: FlowPressureClass::Healthy,
standing_state: StandingQueueState::Transient,
scheduler_state: FlowSchedulerState::Idle,
bucket_id,
weight_quanta: weight_quanta.max(1),
in_active_ring: false,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AdmissionDecision {
Admit,
RejectWorkerCap,
RejectFlowCap,
RejectBucketCap,
RejectSaturated,
RejectStandingFlow,
}
#[derive(Debug, Clone)]
pub(crate) enum SchedulerDecision {
Idle,
Dispatch(DispatchCandidate),
}
#[derive(Debug, Clone)]
pub(crate) struct DispatchCandidate {
pub(crate) frame: QueuedFrame,
pub(crate) pressure_state: PressureState,
pub(crate) flow_class: FlowPressureClass,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DispatchFeedback {
Routed,
QueueFull,
ChannelClosed,
NoConn,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DispatchAction {
Continue,
CloseFlow,
}
@@ -1,227 +0,0 @@
use std::time::{Duration, Instant};
use super::model::PressureState;
#[derive(Debug, Clone, Copy)]
pub(crate) struct PressureSignals {
pub(crate) active_flows: usize,
pub(crate) total_queued_bytes: u64,
pub(crate) standing_flows: usize,
pub(crate) backpressured_flows: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct PressureConfig {
pub(crate) backpressure_enabled: bool,
pub(crate) evaluate_every_rounds: u32,
pub(crate) transition_hysteresis_rounds: u8,
pub(crate) standing_ratio_pressured_pct: u8,
pub(crate) standing_ratio_shedding_pct: u8,
pub(crate) standing_ratio_saturated_pct: u8,
pub(crate) queue_ratio_pressured_pct: u8,
pub(crate) queue_ratio_shedding_pct: u8,
pub(crate) queue_ratio_saturated_pct: u8,
pub(crate) reject_window: Duration,
pub(crate) rejects_pressured: u32,
pub(crate) rejects_shedding: u32,
pub(crate) rejects_saturated: u32,
pub(crate) stalls_pressured: u32,
pub(crate) stalls_shedding: u32,
pub(crate) stalls_saturated: u32,
}
impl Default for PressureConfig {
fn default() -> Self {
Self {
backpressure_enabled: true,
evaluate_every_rounds: 8,
transition_hysteresis_rounds: 3,
standing_ratio_pressured_pct: 20,
standing_ratio_shedding_pct: 35,
standing_ratio_saturated_pct: 50,
queue_ratio_pressured_pct: 65,
queue_ratio_shedding_pct: 82,
queue_ratio_saturated_pct: 94,
reject_window: Duration::from_secs(2),
rejects_pressured: 32,
rejects_shedding: 96,
rejects_saturated: 256,
stalls_pressured: 32,
stalls_shedding: 96,
stalls_saturated: 256,
}
}
}
#[derive(Debug)]
pub(crate) struct PressureEvaluator {
state: PressureState,
candidate_state: PressureState,
candidate_hits: u8,
rounds_since_eval: u32,
window_started_at: Instant,
admission_rejects_window: u32,
route_stalls_window: u32,
}
impl PressureEvaluator {
pub(crate) fn new(now: Instant) -> Self {
Self {
state: PressureState::Normal,
candidate_state: PressureState::Normal,
candidate_hits: 0,
rounds_since_eval: 0,
window_started_at: now,
admission_rejects_window: 0,
route_stalls_window: 0,
}
}
#[inline]
pub(crate) fn state(&self) -> PressureState {
self.state
}
pub(crate) fn note_admission_reject(&mut self, now: Instant, cfg: &PressureConfig) {
self.rotate_window_if_needed(now, cfg);
self.admission_rejects_window = self.admission_rejects_window.saturating_add(1);
}
pub(crate) fn note_route_stall(&mut self, now: Instant, cfg: &PressureConfig) {
self.rotate_window_if_needed(now, cfg);
self.route_stalls_window = self.route_stalls_window.saturating_add(1);
}
pub(crate) fn maybe_evaluate(
&mut self,
now: Instant,
cfg: &PressureConfig,
max_total_queued_bytes: u64,
signals: PressureSignals,
force: bool,
) -> PressureState {
self.rotate_window_if_needed(now, cfg);
if !cfg.backpressure_enabled {
self.state = PressureState::Normal;
self.candidate_state = PressureState::Normal;
self.candidate_hits = 0;
self.rounds_since_eval = 0;
return self.state;
}
self.rounds_since_eval = self.rounds_since_eval.saturating_add(1);
if !force && self.rounds_since_eval < cfg.evaluate_every_rounds.max(1) {
return self.state;
}
self.rounds_since_eval = 0;
let target = self.derive_target_state(cfg, max_total_queued_bytes, signals);
if target == self.state {
self.candidate_state = target;
self.candidate_hits = 0;
return self.state;
}
if self.candidate_state == target {
self.candidate_hits = self.candidate_hits.saturating_add(1);
} else {
self.candidate_state = target;
self.candidate_hits = 1;
}
if self.candidate_hits >= cfg.transition_hysteresis_rounds.max(1) {
self.state = target;
self.candidate_hits = 0;
}
self.state
}
fn derive_target_state(
&self,
cfg: &PressureConfig,
max_total_queued_bytes: u64,
signals: PressureSignals,
) -> PressureState {
if !cfg.backpressure_enabled {
return PressureState::Normal;
}
let queue_ratio_pct = if max_total_queued_bytes == 0 {
100
} else {
((signals.total_queued_bytes.saturating_mul(100)) / max_total_queued_bytes).min(100)
as u8
};
let standing_ratio_pct = if signals.active_flows == 0 {
0
} else {
((signals.standing_flows.saturating_mul(100)) / signals.active_flows).min(100) as u8
};
let mut pressure_score = 0u8;
if queue_ratio_pct >= cfg.queue_ratio_pressured_pct {
pressure_score = pressure_score.max(1);
}
if queue_ratio_pct >= cfg.queue_ratio_shedding_pct {
pressure_score = pressure_score.max(2);
}
if queue_ratio_pct >= cfg.queue_ratio_saturated_pct {
pressure_score = pressure_score.max(3);
}
if standing_ratio_pct >= cfg.standing_ratio_pressured_pct {
pressure_score = pressure_score.max(1);
}
if standing_ratio_pct >= cfg.standing_ratio_shedding_pct {
pressure_score = pressure_score.max(2);
}
if standing_ratio_pct >= cfg.standing_ratio_saturated_pct {
pressure_score = pressure_score.max(3);
}
if self.admission_rejects_window >= cfg.rejects_pressured {
pressure_score = pressure_score.max(1);
}
if self.admission_rejects_window >= cfg.rejects_shedding {
pressure_score = pressure_score.max(2);
}
if self.admission_rejects_window >= cfg.rejects_saturated {
pressure_score = pressure_score.max(3);
}
if self.route_stalls_window >= cfg.stalls_pressured {
pressure_score = pressure_score.max(1);
}
if self.route_stalls_window >= cfg.stalls_shedding {
pressure_score = pressure_score.max(2);
}
if self.route_stalls_window >= cfg.stalls_saturated {
pressure_score = pressure_score.max(3);
}
if signals.backpressured_flows > signals.active_flows.saturating_div(2)
&& signals.active_flows > 0
{
pressure_score = pressure_score.max(2);
}
match pressure_score {
0 => PressureState::Normal,
1 => PressureState::Pressured,
2 => PressureState::Shedding,
_ => PressureState::Saturated,
}
}
fn rotate_window_if_needed(&mut self, now: Instant, cfg: &PressureConfig) {
if now.saturating_duration_since(self.window_started_at) < cfg.reject_window {
return;
}
self.window_started_at = now;
self.admission_rejects_window = 0;
self.route_stalls_window = 0;
}
}
@@ -1,806 +0,0 @@
use std::collections::{HashMap, HashSet, VecDeque};
use std::time::{Duration, Instant};
use crate::protocol::constants::RPC_FLAG_QUICKACK;
use bytes::Bytes;
use super::model::{
AdmissionDecision, DispatchAction, DispatchCandidate, DispatchFeedback, FlowFairnessState,
FlowPressureClass, FlowSchedulerState, PressureState, QueuedFrame, SchedulerDecision,
StandingQueueState,
};
use super::pressure::{PressureConfig, PressureEvaluator, PressureSignals};
#[derive(Debug, Clone)]
pub(crate) struct WorkerFairnessConfig {
pub(crate) worker_id: u16,
pub(crate) backpressure_enabled: bool,
pub(crate) max_active_flows: usize,
pub(crate) max_total_queued_bytes: u64,
pub(crate) max_flow_queued_bytes: u64,
pub(crate) base_quantum_bytes: u32,
pub(crate) pressured_quantum_bytes: u32,
pub(crate) penalized_quantum_bytes: u32,
pub(crate) standing_queue_min_age: Duration,
pub(crate) standing_queue_min_backlog_bytes: u64,
pub(crate) standing_stall_threshold: u8,
pub(crate) max_consecutive_stalls_before_shed: u8,
pub(crate) max_consecutive_stalls_before_close: u8,
pub(crate) soft_bucket_count: usize,
pub(crate) soft_bucket_share_pct: u8,
pub(crate) default_flow_weight: u8,
pub(crate) quickack_flow_weight: u8,
pub(crate) pressure: PressureConfig,
}
impl Default for WorkerFairnessConfig {
fn default() -> Self {
Self {
worker_id: 0,
backpressure_enabled: true,
max_active_flows: 4096,
max_total_queued_bytes: 16 * 1024 * 1024,
max_flow_queued_bytes: 512 * 1024,
base_quantum_bytes: 32 * 1024,
pressured_quantum_bytes: 16 * 1024,
penalized_quantum_bytes: 8 * 1024,
standing_queue_min_age: Duration::from_millis(250),
standing_queue_min_backlog_bytes: 64 * 1024,
standing_stall_threshold: 3,
max_consecutive_stalls_before_shed: 4,
max_consecutive_stalls_before_close: 16,
soft_bucket_count: 64,
soft_bucket_share_pct: 25,
default_flow_weight: 1,
quickack_flow_weight: 4,
pressure: PressureConfig::default(),
}
}
}
struct FlowEntry {
fairness: FlowFairnessState,
queue: VecDeque<QueuedFrame>,
}
impl FlowEntry {
fn new(flow_id: u64, worker_id: u16, bucket_id: usize, weight_quanta: u8) -> Self {
Self {
fairness: FlowFairnessState::new(flow_id, worker_id, bucket_id, weight_quanta),
queue: VecDeque::new(),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct WorkerFairnessSnapshot {
pub(crate) pressure_state: PressureState,
pub(crate) active_flows: usize,
pub(crate) total_queued_bytes: u64,
pub(crate) standing_flows: usize,
pub(crate) backpressured_flows: usize,
pub(crate) scheduler_rounds: u64,
pub(crate) deficit_grants: u64,
pub(crate) deficit_skips: u64,
pub(crate) enqueue_rejects: u64,
pub(crate) shed_drops: u64,
pub(crate) fairness_penalties: u64,
pub(crate) downstream_stalls: u64,
}
pub(crate) struct WorkerFairnessState {
config: WorkerFairnessConfig,
pressure: PressureEvaluator,
flows: HashMap<u64, FlowEntry>,
active_ring: VecDeque<u64>,
active_ring_members: HashSet<u64>,
total_queued_bytes: u64,
bucket_queued_bytes: Vec<u64>,
bucket_active_flows: Vec<usize>,
standing_flow_count: usize,
backpressured_flow_count: usize,
scheduler_rounds: u64,
deficit_grants: u64,
deficit_skips: u64,
enqueue_rejects: u64,
shed_drops: u64,
fairness_penalties: u64,
downstream_stalls: u64,
}
impl WorkerFairnessState {
pub(crate) fn new(mut config: WorkerFairnessConfig, now: Instant) -> Self {
config.pressure.backpressure_enabled = config.backpressure_enabled;
let bucket_count = config.soft_bucket_count.max(1);
Self {
config,
pressure: PressureEvaluator::new(now),
flows: HashMap::new(),
active_ring: VecDeque::new(),
active_ring_members: HashSet::new(),
total_queued_bytes: 0,
bucket_queued_bytes: vec![0; bucket_count],
bucket_active_flows: vec![0; bucket_count],
standing_flow_count: 0,
backpressured_flow_count: 0,
scheduler_rounds: 0,
deficit_grants: 0,
deficit_skips: 0,
enqueue_rejects: 0,
shed_drops: 0,
fairness_penalties: 0,
downstream_stalls: 0,
}
}
pub(crate) fn pressure_state(&self) -> PressureState {
self.pressure.state()
}
pub(crate) fn set_backpressure_enabled(&mut self, enabled: bool) {
if self.config.backpressure_enabled == enabled {
return;
}
self.config.backpressure_enabled = enabled;
self.config.pressure.backpressure_enabled = enabled;
self.evaluate_pressure(Instant::now(), true);
}
pub(crate) fn snapshot(&self) -> WorkerFairnessSnapshot {
WorkerFairnessSnapshot {
pressure_state: self.pressure.state(),
active_flows: self.flows.len(),
total_queued_bytes: self.total_queued_bytes,
standing_flows: self.standing_flow_count,
backpressured_flows: self.backpressured_flow_count,
scheduler_rounds: self.scheduler_rounds,
deficit_grants: self.deficit_grants,
deficit_skips: self.deficit_skips,
enqueue_rejects: self.enqueue_rejects,
shed_drops: self.shed_drops,
fairness_penalties: self.fairness_penalties,
downstream_stalls: self.downstream_stalls,
}
}
pub(crate) fn enqueue_data(
&mut self,
conn_id: u64,
flags: u32,
data: Bytes,
now: Instant,
) -> AdmissionDecision {
let frame = QueuedFrame {
conn_id,
flags,
data,
enqueued_at: now,
};
let frame_bytes = frame.queued_bytes();
if self.config.backpressure_enabled && self.pressure.state() == PressureState::Saturated {
self.pressure
.note_admission_reject(now, &self.config.pressure);
self.enqueue_rejects = self.enqueue_rejects.saturating_add(1);
return AdmissionDecision::RejectSaturated;
}
if self.total_queued_bytes.saturating_add(frame_bytes) > self.config.max_total_queued_bytes
{
self.pressure
.note_admission_reject(now, &self.config.pressure);
self.enqueue_rejects = self.enqueue_rejects.saturating_add(1);
self.evaluate_pressure(now, true);
return AdmissionDecision::RejectWorkerCap;
}
if !self.flows.contains_key(&conn_id) && self.flows.len() >= self.config.max_active_flows {
self.pressure
.note_admission_reject(now, &self.config.pressure);
self.enqueue_rejects = self.enqueue_rejects.saturating_add(1);
self.evaluate_pressure(now, true);
return AdmissionDecision::RejectWorkerCap;
}
let bucket_id = self.bucket_for(conn_id);
let frame_weight = Self::weight_for_flags(&self.config, flags);
let bucket_cap = self
.config
.max_total_queued_bytes
.saturating_mul(self.config.soft_bucket_share_pct.max(1) as u64)
.saturating_div(100)
.max(self.config.max_flow_queued_bytes);
if self.bucket_queued_bytes[bucket_id].saturating_add(frame_bytes) > bucket_cap {
self.pressure
.note_admission_reject(now, &self.config.pressure);
self.enqueue_rejects = self.enqueue_rejects.saturating_add(1);
self.evaluate_pressure(now, true);
return AdmissionDecision::RejectBucketCap;
}
let entry = if let Some(flow) = self.flows.get_mut(&conn_id) {
flow
} else {
self.bucket_active_flows[bucket_id] =
self.bucket_active_flows[bucket_id].saturating_add(1);
self.flows.insert(
conn_id,
FlowEntry::new(conn_id, self.config.worker_id, bucket_id, frame_weight),
);
self.flows
.get_mut(&conn_id)
.expect("flow inserted must be retrievable")
};
entry.fairness.weight_quanta = entry.fairness.weight_quanta.max(frame_weight);
if entry.fairness.pending_bytes.saturating_add(frame_bytes)
> self.config.max_flow_queued_bytes
{
self.pressure
.note_admission_reject(now, &self.config.pressure);
self.enqueue_rejects = self.enqueue_rejects.saturating_add(1);
self.evaluate_pressure(now, true);
return AdmissionDecision::RejectFlowCap;
}
if self.config.backpressure_enabled
&& self.pressure.state() >= PressureState::Shedding
&& entry.fairness.standing_state == StandingQueueState::Standing
{
self.pressure
.note_admission_reject(now, &self.config.pressure);
self.enqueue_rejects = self.enqueue_rejects.saturating_add(1);
self.evaluate_pressure(now, true);
return AdmissionDecision::RejectStandingFlow;
}
entry.fairness.pending_bytes = entry.fairness.pending_bytes.saturating_add(frame_bytes);
if entry.fairness.queue_started_at.is_none() {
entry.fairness.queue_started_at = Some(now);
}
entry.queue.push_back(frame);
self.total_queued_bytes = self.total_queued_bytes.saturating_add(frame_bytes);
self.bucket_queued_bytes[bucket_id] =
self.bucket_queued_bytes[bucket_id].saturating_add(frame_bytes);
let mut enqueue_active = false;
if !entry.fairness.in_active_ring {
entry.fairness.in_active_ring = true;
enqueue_active = true;
}
let pressure_state = self.pressure.state();
let (before_membership, after_membership) = {
let before = Self::flow_membership(&entry.fairness);
Self::classify_flow(&self.config, pressure_state, now, &mut entry.fairness);
let after = Self::flow_membership(&entry.fairness);
(before, after)
};
if enqueue_active {
self.enqueue_active_conn(conn_id);
}
self.apply_flow_membership_delta(before_membership, after_membership);
self.evaluate_pressure(now, true);
AdmissionDecision::Admit
}
pub(crate) fn next_decision(&mut self, now: Instant) -> SchedulerDecision {
self.scheduler_rounds = self.scheduler_rounds.saturating_add(1);
self.evaluate_pressure(now, false);
let active_len = self.active_ring.len();
for _ in 0..active_len {
let Some(conn_id) = self.active_ring.pop_front() else {
break;
};
if !self.active_ring_members.remove(&conn_id) {
continue;
}
let mut candidate = None;
let mut requeue_active = false;
let mut drained_bytes = 0u64;
let mut bucket_id = 0usize;
let mut should_continue = false;
let mut enqueue_active = false;
let mut membership_delta = None;
let pressure_state = self.pressure.state();
if let Some(flow) = self.flows.get_mut(&conn_id) {
bucket_id = flow.fairness.bucket_id;
flow.fairness.in_active_ring = false;
let before_membership = Self::flow_membership(&flow.fairness);
if flow.queue.is_empty() {
flow.fairness.in_active_ring = false;
flow.fairness.scheduler_state = FlowSchedulerState::Idle;
flow.fairness.pending_bytes = 0;
flow.fairness.deficit_bytes = 0;
flow.fairness.queue_started_at = None;
should_continue = true;
} else {
Self::classify_flow(&self.config, pressure_state, now, &mut flow.fairness);
let quantum =
Self::effective_quantum_bytes(&self.config, pressure_state, &flow.fairness);
flow.fairness.deficit_bytes = flow
.fairness
.deficit_bytes
.saturating_add(i64::from(quantum));
Self::clamp_deficit_bytes(&self.config, &mut flow.fairness);
self.deficit_grants = self.deficit_grants.saturating_add(1);
let front_len = flow.queue.front().map_or(0, |front| front.queued_bytes());
if flow.fairness.deficit_bytes < front_len as i64 {
flow.fairness.consecutive_skips =
flow.fairness.consecutive_skips.saturating_add(1);
self.deficit_skips = self.deficit_skips.saturating_add(1);
requeue_active = true;
flow.fairness.in_active_ring = true;
enqueue_active = true;
} else if let Some(frame) = flow.queue.pop_front() {
drained_bytes = frame.queued_bytes();
flow.fairness.pending_bytes =
flow.fairness.pending_bytes.saturating_sub(drained_bytes);
flow.fairness.deficit_bytes = flow
.fairness
.deficit_bytes
.saturating_sub(drained_bytes as i64);
Self::clamp_deficit_bytes(&self.config, &mut flow.fairness);
flow.fairness.consecutive_skips = 0;
flow.fairness.queue_started_at =
flow.queue.front().map(|front| front.enqueued_at);
requeue_active = !flow.queue.is_empty();
if !requeue_active {
flow.fairness.scheduler_state = FlowSchedulerState::Idle;
flow.fairness.in_active_ring = false;
flow.fairness.deficit_bytes = 0;
} else {
flow.fairness.in_active_ring = true;
enqueue_active = true;
}
candidate = Some(DispatchCandidate {
pressure_state,
flow_class: flow.fairness.pressure_class,
frame,
});
}
}
membership_delta = Some((before_membership, Self::flow_membership(&flow.fairness)));
}
if let Some((before_membership, after_membership)) = membership_delta {
self.apply_flow_membership_delta(before_membership, after_membership);
}
if should_continue {
continue;
}
if drained_bytes > 0 {
self.total_queued_bytes = self.total_queued_bytes.saturating_sub(drained_bytes);
self.bucket_queued_bytes[bucket_id] =
self.bucket_queued_bytes[bucket_id].saturating_sub(drained_bytes);
}
if requeue_active && enqueue_active {
self.enqueue_active_conn(conn_id);
}
if let Some(candidate) = candidate {
return SchedulerDecision::Dispatch(candidate);
}
}
SchedulerDecision::Idle
}
pub(crate) fn apply_dispatch_feedback(
&mut self,
conn_id: u64,
candidate: DispatchCandidate,
feedback: DispatchFeedback,
now: Instant,
) -> DispatchAction {
match feedback {
DispatchFeedback::Routed => {
let mut membership_delta = None;
if let Some(flow) = self.flows.get_mut(&conn_id) {
let before_membership = Self::flow_membership(&flow.fairness);
flow.fairness.last_drain_at = Some(now);
flow.fairness.recent_drain_bytes = flow
.fairness
.recent_drain_bytes
.saturating_add(candidate.frame.queued_bytes());
flow.fairness.consecutive_stalls = 0;
if flow.fairness.scheduler_state != FlowSchedulerState::Idle {
flow.fairness.scheduler_state = FlowSchedulerState::Active;
}
Self::classify_flow(
&self.config,
self.pressure.state(),
now,
&mut flow.fairness,
);
membership_delta =
Some((before_membership, Self::flow_membership(&flow.fairness)));
}
if let Some((before_membership, after_membership)) = membership_delta {
self.apply_flow_membership_delta(before_membership, after_membership);
}
self.evaluate_pressure(now, false);
DispatchAction::Continue
}
DispatchFeedback::QueueFull => {
if self.config.backpressure_enabled {
self.pressure.note_route_stall(now, &self.config.pressure);
self.downstream_stalls = self.downstream_stalls.saturating_add(1);
}
let state = self.pressure.state();
let Some(flow) = self.flows.get_mut(&conn_id) else {
self.evaluate_pressure(now, true);
return DispatchAction::Continue;
};
let (before_membership, after_membership, should_close_flow, enqueue_active) = {
let before_membership = Self::flow_membership(&flow.fairness);
let mut enqueue_active = false;
if self.config.backpressure_enabled {
flow.fairness.consecutive_stalls =
flow.fairness.consecutive_stalls.saturating_add(1);
flow.fairness.scheduler_state = FlowSchedulerState::Backpressured;
flow.fairness.pressure_class = FlowPressureClass::Backpressured;
}
let should_shed_frame = self.config.backpressure_enabled
&& (matches!(state, PressureState::Saturated)
|| (matches!(state, PressureState::Shedding)
&& flow.fairness.standing_state == StandingQueueState::Standing
&& flow.fairness.consecutive_stalls
>= self.config.max_consecutive_stalls_before_shed));
if should_shed_frame {
self.shed_drops = self.shed_drops.saturating_add(1);
self.fairness_penalties = self.fairness_penalties.saturating_add(1);
} else {
let frame_bytes = candidate.frame.queued_bytes();
flow.queue.push_front(candidate.frame);
flow.fairness.pending_bytes =
flow.fairness.pending_bytes.saturating_add(frame_bytes);
flow.fairness.queue_started_at =
flow.queue.front().map(|front| front.enqueued_at);
self.total_queued_bytes =
self.total_queued_bytes.saturating_add(frame_bytes);
self.bucket_queued_bytes[flow.fairness.bucket_id] = self
.bucket_queued_bytes[flow.fairness.bucket_id]
.saturating_add(frame_bytes);
if !flow.fairness.in_active_ring {
flow.fairness.in_active_ring = true;
enqueue_active = true;
}
}
Self::classify_flow(&self.config, state, now, &mut flow.fairness);
let after_membership = Self::flow_membership(&flow.fairness);
let should_close_flow = self.config.backpressure_enabled
&& flow.fairness.consecutive_stalls
>= self.config.max_consecutive_stalls_before_close
&& self.pressure.state() == PressureState::Saturated;
(
before_membership,
after_membership,
should_close_flow,
enqueue_active,
)
};
if enqueue_active {
self.enqueue_active_conn(conn_id);
}
self.apply_flow_membership_delta(before_membership, after_membership);
if should_close_flow {
self.remove_flow(conn_id);
self.evaluate_pressure(now, true);
return DispatchAction::CloseFlow;
}
self.evaluate_pressure(now, true);
DispatchAction::Continue
}
DispatchFeedback::ChannelClosed | DispatchFeedback::NoConn => {
self.remove_flow(conn_id);
self.evaluate_pressure(now, true);
DispatchAction::CloseFlow
}
}
}
pub(crate) fn remove_flow(&mut self, conn_id: u64) {
let Some(entry) = self.flows.remove(&conn_id) else {
return;
};
self.active_ring_members.remove(&conn_id);
self.active_ring
.retain(|queued_conn_id| *queued_conn_id != conn_id);
let (was_standing, was_backpressured) = Self::flow_membership(&entry.fairness);
if was_standing {
self.standing_flow_count = self.standing_flow_count.saturating_sub(1);
}
if was_backpressured {
self.backpressured_flow_count = self.backpressured_flow_count.saturating_sub(1);
}
self.bucket_active_flows[entry.fairness.bucket_id] =
self.bucket_active_flows[entry.fairness.bucket_id].saturating_sub(1);
let mut reclaimed = 0u64;
for frame in entry.queue {
reclaimed = reclaimed.saturating_add(frame.queued_bytes());
}
self.total_queued_bytes = self.total_queued_bytes.saturating_sub(reclaimed);
self.bucket_queued_bytes[entry.fairness.bucket_id] =
self.bucket_queued_bytes[entry.fairness.bucket_id].saturating_sub(reclaimed);
}
fn evaluate_pressure(&mut self, now: Instant, force: bool) {
let _ = self.pressure.maybe_evaluate(
now,
&self.config.pressure,
self.config.max_total_queued_bytes,
PressureSignals {
active_flows: self.flows.len(),
total_queued_bytes: self.total_queued_bytes,
standing_flows: self.standing_flow_count,
backpressured_flows: self.backpressured_flow_count,
},
force,
);
}
fn classify_flow(
config: &WorkerFairnessConfig,
pressure_state: PressureState,
now: Instant,
fairness: &mut FlowFairnessState,
) {
let (pressure_class, standing_state, scheduler_state, standing) =
Self::derive_flow_classification(config, pressure_state, now, fairness);
fairness.pressure_class = pressure_class;
fairness.standing_state = standing_state;
fairness.scheduler_state = scheduler_state;
if scheduler_state == FlowSchedulerState::Idle {
fairness.deficit_bytes = 0;
}
if standing {
fairness.penalty_score = fairness.penalty_score.saturating_add(1);
} else {
fairness.penalty_score = fairness.penalty_score.saturating_sub(1);
}
}
fn derive_flow_classification(
config: &WorkerFairnessConfig,
pressure_state: PressureState,
now: Instant,
fairness: &FlowFairnessState,
) -> (
FlowPressureClass,
StandingQueueState,
FlowSchedulerState,
bool,
) {
if fairness.pending_bytes == 0 {
return (
FlowPressureClass::Healthy,
StandingQueueState::Transient,
FlowSchedulerState::Idle,
false,
);
}
let queue_age = fairness
.queue_started_at
.map(|ts| now.saturating_duration_since(ts))
.unwrap_or_default();
let drain_stalled = fairness
.last_drain_at
.map(|ts| now.saturating_duration_since(ts) >= config.standing_queue_min_age)
.unwrap_or(true);
let standing = fairness.pending_bytes >= config.standing_queue_min_backlog_bytes
&& queue_age >= config.standing_queue_min_age
&& (fairness.consecutive_stalls >= config.standing_stall_threshold || drain_stalled);
if standing {
let scheduler_state = if pressure_state >= PressureState::Shedding {
FlowSchedulerState::SheddingCandidate
} else {
FlowSchedulerState::Penalized
};
return (
FlowPressureClass::Standing,
StandingQueueState::Standing,
scheduler_state,
true,
);
}
if fairness.consecutive_stalls > 0 {
return (
FlowPressureClass::Backpressured,
StandingQueueState::Transient,
FlowSchedulerState::Backpressured,
false,
);
}
if fairness.pending_bytes >= config.standing_queue_min_backlog_bytes {
return (
FlowPressureClass::Bursty,
StandingQueueState::Transient,
FlowSchedulerState::Active,
false,
);
}
(
FlowPressureClass::Healthy,
StandingQueueState::Transient,
FlowSchedulerState::Active,
false,
)
}
#[inline]
fn flow_membership(fairness: &FlowFairnessState) -> (bool, bool) {
(
fairness.standing_state == StandingQueueState::Standing,
Self::scheduler_state_is_backpressured(fairness.scheduler_state),
)
}
#[inline]
fn scheduler_state_is_backpressured(state: FlowSchedulerState) -> bool {
matches!(
state,
FlowSchedulerState::Backpressured
| FlowSchedulerState::Penalized
| FlowSchedulerState::SheddingCandidate
)
}
fn apply_flow_membership_delta(
&mut self,
before_membership: (bool, bool),
after_membership: (bool, bool),
) {
if before_membership.0 != after_membership.0 {
if after_membership.0 {
self.standing_flow_count = self.standing_flow_count.saturating_add(1);
} else {
self.standing_flow_count = self.standing_flow_count.saturating_sub(1);
}
}
if before_membership.1 != after_membership.1 {
if after_membership.1 {
self.backpressured_flow_count = self.backpressured_flow_count.saturating_add(1);
} else {
self.backpressured_flow_count = self.backpressured_flow_count.saturating_sub(1);
}
}
}
#[inline]
fn clamp_deficit_bytes(config: &WorkerFairnessConfig, fairness: &mut FlowFairnessState) {
let max_deficit = config.max_flow_queued_bytes.min(i64::MAX as u64) as i64;
fairness.deficit_bytes = fairness.deficit_bytes.clamp(0, max_deficit);
}
#[inline]
fn enqueue_active_conn(&mut self, conn_id: u64) {
if self.active_ring_members.insert(conn_id) {
self.active_ring.push_back(conn_id);
}
}
#[inline]
fn weight_for_flags(config: &WorkerFairnessConfig, flags: u32) -> u8 {
if (flags & RPC_FLAG_QUICKACK) != 0 {
return config.quickack_flow_weight.max(1);
}
config.default_flow_weight.max(1)
}
#[cfg(test)]
pub(crate) fn debug_recompute_flow_counters(&self, now: Instant) -> (usize, usize) {
let pressure_state = self.pressure.state();
let mut standing = 0usize;
let mut backpressured = 0usize;
for flow in self.flows.values() {
let (_, standing_state, scheduler_state, _) =
Self::derive_flow_classification(&self.config, pressure_state, now, &flow.fairness);
if standing_state == StandingQueueState::Standing {
standing = standing.saturating_add(1);
}
if Self::scheduler_state_is_backpressured(scheduler_state) {
backpressured = backpressured.saturating_add(1);
}
}
(standing, backpressured)
}
#[cfg(test)]
pub(crate) fn debug_check_active_ring_consistency(&self) -> bool {
if self.active_ring.len() != self.active_ring_members.len() {
return false;
}
let mut seen = HashSet::with_capacity(self.active_ring.len());
for conn_id in self.active_ring.iter().copied() {
if !seen.insert(conn_id) {
return false;
}
if !self.active_ring_members.contains(&conn_id) {
return false;
}
let Some(flow) = self.flows.get(&conn_id) else {
return false;
};
if !flow.fairness.in_active_ring || flow.queue.is_empty() {
return false;
}
}
for (conn_id, flow) in self.flows.iter() {
let in_ring = self.active_ring_members.contains(conn_id);
if flow.fairness.in_active_ring != in_ring {
return false;
}
if in_ring && flow.queue.is_empty() {
return false;
}
}
true
}
#[cfg(test)]
pub(crate) fn debug_max_deficit_bytes(&self) -> i64 {
self.flows
.values()
.map(|entry| entry.fairness.deficit_bytes)
.max()
.unwrap_or(0)
}
fn effective_quantum_bytes(
config: &WorkerFairnessConfig,
pressure_state: PressureState,
fairness: &FlowFairnessState,
) -> u32 {
let penalized = matches!(
fairness.scheduler_state,
FlowSchedulerState::Penalized | FlowSchedulerState::SheddingCandidate
);
if penalized {
return config.penalized_quantum_bytes.max(1);
}
let base_quantum = match pressure_state {
PressureState::Normal => config.base_quantum_bytes.max(1),
PressureState::Pressured => config.pressured_quantum_bytes.max(1),
PressureState::Shedding => config.pressured_quantum_bytes.max(1),
PressureState::Saturated => config.penalized_quantum_bytes.max(1),
};
let weighted_quantum = base_quantum.saturating_mul(fairness.weight_quanta.max(1) as u32);
weighted_quantum.max(1)
}
fn bucket_for(&self, conn_id: u64) -> usize {
(conn_id as usize) % self.bucket_queued_bytes.len().max(1)
}
}
+95 -59
View File
@@ -67,8 +67,10 @@ struct FamilyReconnectOutcome {
key: (i32, IpFamily),
dc: i32,
family: IpFamily,
alive: usize,
required: usize,
endpoint_count: usize,
restored: usize,
}
pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_connections: usize) {
@@ -80,6 +82,8 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
let mut single_endpoint_outage: HashSet<(i32, IpFamily)> = HashSet::new();
let mut shadow_rotate_deadline: HashMap<(i32, IpFamily), Instant> = HashMap::new();
let mut idle_refresh_next_attempt: HashMap<(i32, IpFamily), Instant> = HashMap::new();
let mut adaptive_idle_since: HashMap<(i32, IpFamily), Instant> = HashMap::new();
let mut adaptive_recover_until: HashMap<(i32, IpFamily), Instant> = HashMap::new();
let mut floor_warn_next_allowed: HashMap<(i32, IpFamily), Instant> = HashMap::new();
let mut drain_warn_next_allowed: HashMap<u64, Instant> = HashMap::new();
let mut degraded_interval = true;
@@ -105,6 +109,8 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
&mut single_endpoint_outage,
&mut shadow_rotate_deadline,
&mut idle_refresh_next_attempt,
&mut adaptive_idle_since,
&mut adaptive_recover_until,
&mut floor_warn_next_allowed,
)
.await;
@@ -120,6 +126,8 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
&mut single_endpoint_outage,
&mut shadow_rotate_deadline,
&mut idle_refresh_next_attempt,
&mut adaptive_idle_since,
&mut adaptive_recover_until,
&mut floor_warn_next_allowed,
)
.await;
@@ -352,6 +360,8 @@ async fn check_family(
single_endpoint_outage: &mut HashSet<(i32, IpFamily)>,
shadow_rotate_deadline: &mut HashMap<(i32, IpFamily), Instant>,
idle_refresh_next_attempt: &mut HashMap<(i32, IpFamily), Instant>,
adaptive_idle_since: &mut HashMap<(i32, IpFamily), Instant>,
adaptive_recover_until: &mut HashMap<(i32, IpFamily), Instant>,
floor_warn_next_allowed: &mut HashMap<(i32, IpFamily), Instant>,
) -> bool {
let enabled = match family {
@@ -383,7 +393,10 @@ async fn check_family(
let reconnect_budget = health_reconnect_budget(pool, dc_endpoints.len());
let reconnect_sem = Arc::new(Semaphore::new(reconnect_budget));
if pool.floor_mode() == MeFloorMode::Static {}
if pool.floor_mode() == MeFloorMode::Static {
adaptive_idle_since.clear();
adaptive_recover_until.clear();
}
let mut live_addr_counts = HashMap::<(i32, SocketAddr), usize>::new();
let mut live_writer_ids_by_addr = HashMap::<(i32, SocketAddr), Vec<u64>>::new();
@@ -422,6 +435,8 @@ async fn check_family(
&live_addr_counts,
&live_writer_ids_by_addr,
&bound_clients_by_writer,
adaptive_idle_since,
adaptive_recover_until,
)
.await;
pool.set_adaptive_floor_runtime_caps(
@@ -488,6 +503,8 @@ async fn check_family(
outage_next_attempt.remove(&key);
shadow_rotate_deadline.remove(&key);
idle_refresh_next_attempt.remove(&key);
adaptive_idle_since.remove(&key);
adaptive_recover_until.remove(&key);
info!(
dc = %dc,
?family,
@@ -615,28 +632,22 @@ async fn check_family(
restored += 1;
continue;
}
let base_req = pool_for_reconnect
.required_writers_for_dc_with_floor_mode(endpoints_for_dc.len(), false);
if alive + restored >= base_req {
pool_for_reconnect
.stats
.increment_me_floor_cap_block_total();
pool_for_reconnect
.stats
.increment_me_floor_swap_idle_failed_total();
debug!(
dc = %dc,
?family,
alive,
required,
active_cap_effective_total,
"Adaptive floor cap reached, reconnect attempt blocked"
);
break;
}
pool_for_reconnect
.stats
.increment_me_floor_cap_block_total();
pool_for_reconnect
.stats
.increment_me_floor_swap_idle_failed_total();
debug!(
dc = %dc,
?family,
alive,
required,
active_cap_effective_total,
"Adaptive floor cap reached, reconnect attempt blocked"
);
break;
}
pool_for_reconnect.stats.increment_me_reconnect_attempt();
let res = tokio::time::timeout(
pool_for_reconnect.reconnect_runtime.me_one_timeout,
pool_for_reconnect.connect_endpoints_round_robin(
@@ -652,9 +663,11 @@ async fn check_family(
pool_for_reconnect.stats.increment_me_reconnect_success();
}
Ok(false) => {
pool_for_reconnect.stats.increment_me_reconnect_attempt();
debug!(dc = %dc, ?family, "ME round-robin reconnect failed")
}
Err(_) => {
pool_for_reconnect.stats.increment_me_reconnect_attempt();
debug!(dc = %dc, ?family, "ME reconnect timed out");
}
}
@@ -665,8 +678,10 @@ async fn check_family(
key,
dc,
family,
alive,
required,
endpoint_count: endpoints_for_dc.len(),
restored,
}
});
}
@@ -680,7 +695,7 @@ async fn check_family(
}
};
let now = Instant::now();
let now_alive = live_active_writers_for_dc_family(pool, outcome.dc, outcome.family).await;
let now_alive = outcome.alive + outcome.restored;
if now_alive >= outcome.required {
info!(
dc = %outcome.dc,
@@ -836,33 +851,6 @@ fn should_emit_rate_limited_warn(
false
}
async fn live_active_writers_for_dc_family(pool: &Arc<MePool>, dc: i32, family: IpFamily) -> usize {
let writers = pool.writers.read().await;
writers
.iter()
.filter(|writer| {
if writer.draining.load(std::sync::atomic::Ordering::Relaxed) {
return false;
}
if writer.writer_dc != dc {
return false;
}
if !matches!(
super::pool::WriterContour::from_u8(
writer.contour.load(std::sync::atomic::Ordering::Relaxed),
),
super::pool::WriterContour::Active
) {
return false;
}
match family {
IpFamily::V4 => writer.addr.is_ipv4(),
IpFamily::V6 => writer.addr.is_ipv6(),
}
})
.count()
}
fn adaptive_floor_class_min(
pool: &Arc<MePool>,
endpoint_count: usize,
@@ -916,6 +904,8 @@ async fn build_family_floor_plan(
live_addr_counts: &HashMap<(i32, SocketAddr), usize>,
live_writer_ids_by_addr: &HashMap<(i32, SocketAddr), Vec<u64>>,
bound_clients_by_writer: &HashMap<u64, usize>,
adaptive_idle_since: &mut HashMap<(i32, IpFamily), Instant>,
adaptive_recover_until: &mut HashMap<(i32, IpFamily), Instant>,
) -> FamilyFloorPlan {
let mut entries = Vec::<DcFloorPlanEntry>::new();
let mut by_dc = HashMap::<i32, DcFloorPlanEntry>::new();
@@ -931,7 +921,18 @@ async fn build_family_floor_plan(
if endpoints.is_empty() {
continue;
}
let _key = (*dc, family);
let key = (*dc, family);
let reduce_for_idle = should_reduce_floor_for_idle(
pool,
key,
*dc,
endpoints,
live_writer_ids_by_addr,
bound_clients_by_writer,
adaptive_idle_since,
adaptive_recover_until,
)
.await;
let base_required = pool.required_writers_for_dc(endpoints.len()).max(1);
let min_required = if is_adaptive {
adaptive_floor_class_min(pool, endpoints.len(), base_required)
@@ -946,11 +947,11 @@ async fn build_family_floor_plan(
if max_required < min_required {
max_required = min_required;
}
// We initialize target_required at base_required to prevent 0-writer blackouts
// caused by proactively dropping an idle DC to a single fragile connection.
// The Adaptive Floor constraint loop below will gracefully compress idle DCs
// (prioritized via has_bound_clients = false) to min_required only when global capacity is reached.
let desired_raw = base_required;
let desired_raw = if is_adaptive && reduce_for_idle {
min_required
} else {
base_required
};
let target_required = desired_raw.clamp(min_required, max_required);
let alive = endpoints
.iter()
@@ -1277,6 +1278,43 @@ async fn maybe_refresh_idle_writer_for_dc(
);
}
async fn should_reduce_floor_for_idle(
pool: &Arc<MePool>,
key: (i32, IpFamily),
dc: i32,
endpoints: &[SocketAddr],
live_writer_ids_by_addr: &HashMap<(i32, SocketAddr), Vec<u64>>,
bound_clients_by_writer: &HashMap<u64, usize>,
adaptive_idle_since: &mut HashMap<(i32, IpFamily), Instant>,
adaptive_recover_until: &mut HashMap<(i32, IpFamily), Instant>,
) -> bool {
if pool.floor_mode() != MeFloorMode::Adaptive {
adaptive_idle_since.remove(&key);
adaptive_recover_until.remove(&key);
return false;
}
let now = Instant::now();
let writer_ids = list_writer_ids_for_endpoints(dc, endpoints, live_writer_ids_by_addr);
let has_bound_clients = has_bound_clients_on_endpoint(&writer_ids, bound_clients_by_writer);
if has_bound_clients {
adaptive_idle_since.remove(&key);
adaptive_recover_until.insert(key, now + pool.adaptive_floor_recover_grace_duration());
return false;
}
if let Some(recover_until) = adaptive_recover_until.get(&key)
&& now < *recover_until
{
adaptive_idle_since.remove(&key);
return false;
}
adaptive_recover_until.remove(&key);
let idle_since = adaptive_idle_since.entry(key).or_insert(now);
now.saturating_duration_since(*idle_since) >= pool.adaptive_floor_idle_duration()
}
fn has_bound_clients_on_endpoint(
writer_ids: &[u64],
bound_clients_by_writer: &HashMap<u64, usize>,
@@ -1326,7 +1364,6 @@ async fn recover_single_endpoint_outage(
);
return;
};
pool.stats.increment_me_reconnect_attempt();
pool.stats
.increment_me_single_endpoint_outage_reconnect_attempt_total();
@@ -1402,6 +1439,7 @@ async fn recover_single_endpoint_outage(
return;
}
pool.stats.increment_me_reconnect_attempt();
let current_ms = *outage_backoff.get(&key).unwrap_or(&min_backoff_ms);
let next_ms = current_ms.saturating_mul(2).min(max_backoff_ms);
outage_backoff.insert(key, next_ms);
@@ -1794,8 +1832,6 @@ mod tests {
MeSocksKdfPolicy::default(),
general.me_writer_cmd_channel_capacity,
general.me_route_channel_capacity,
general.me_route_backpressure_enabled,
general.me_route_fairshare_enabled,
general.me_route_backpressure_base_timeout_ms,
general.me_route_backpressure_high_timeout_ms,
general.me_route_backpressure_high_watermark_pct,
-4
View File
@@ -2,10 +2,6 @@
mod codec;
mod config_updater;
mod fairness;
#[cfg(test)]
#[path = "tests/fairness_security_tests.rs"]
mod fairness_security_tests;
mod handshake;
mod health;
#[cfg(test)]
+1 -8
View File
@@ -67,7 +67,6 @@ pub fn format_sample_line(sample: &MePingSample) -> String {
fn format_direct_with_config(
interface: &Option<String>,
bind_addresses: &Option<Vec<String>>,
bindtodevice: &Option<String>,
) -> Option<String> {
let mut direct_parts: Vec<String> = Vec::new();
if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) {
@@ -76,9 +75,6 @@ fn format_direct_with_config(
if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
direct_parts.push(format!("src={}", src.join(",")));
}
if let Some(device) = bindtodevice.as_deref().filter(|v| !v.is_empty()) {
direct_parts.push(format!("bindtodevice={device}"));
}
if direct_parts.is_empty() {
None
} else {
@@ -235,11 +231,8 @@ pub async fn format_me_route(
UpstreamType::Direct {
interface,
bind_addresses,
bindtodevice,
} => {
if let Some(route) =
format_direct_with_config(interface, bind_addresses, bindtodevice)
{
if let Some(route) = format_direct_with_config(interface, bind_addresses) {
route
} else {
detect_direct_route_details(reports, prefer_ipv6, v4_ok, v6_ok)
+16 -54
View File
@@ -396,8 +396,6 @@ pub(super) struct WriterSelectionPolicyCore {
pub(super) struct TransportPolicyCore {
pub(super) me_socks_kdf_policy: AtomicU8,
pub(super) me_route_backpressure_enabled: Arc<AtomicBool>,
pub(super) me_route_fairshare_enabled: Arc<AtomicBool>,
pub(super) me_reader_route_data_wait_ms: Arc<AtomicU64>,
}
@@ -550,8 +548,6 @@ impl MePool {
me_socks_kdf_policy: MeSocksKdfPolicy,
me_writer_cmd_channel_capacity: usize,
me_route_channel_capacity: usize,
me_route_backpressure_enabled: bool,
me_route_fairshare_enabled: bool,
me_route_backpressure_base_timeout_ms: u64,
me_route_backpressure_high_timeout_ms: u64,
me_route_backpressure_high_watermark_pct: u8,
@@ -787,10 +783,6 @@ impl MePool {
}),
transport_policy: Arc::new(TransportPolicyCore {
me_socks_kdf_policy: AtomicU8::new(me_socks_kdf_policy.as_u8()),
me_route_backpressure_enabled: Arc::new(AtomicBool::new(
me_route_backpressure_enabled,
)),
me_route_fairshare_enabled: Arc::new(AtomicBool::new(me_route_fairshare_enabled)),
me_reader_route_data_wait_ms: Arc::new(AtomicU64::new(
me_reader_route_data_wait_ms,
)),
@@ -1253,8 +1245,6 @@ impl MePool {
pub fn update_runtime_transport_policy(
&self,
socks_kdf_policy: MeSocksKdfPolicy,
route_backpressure_enabled: bool,
route_fairshare_enabled: bool,
route_backpressure_base_timeout_ms: u64,
route_backpressure_high_timeout_ms: u64,
route_backpressure_high_watermark_pct: u8,
@@ -1263,12 +1253,6 @@ impl MePool {
self.transport_policy
.me_socks_kdf_policy
.store(socks_kdf_policy.as_u8(), Ordering::Relaxed);
self.transport_policy
.me_route_backpressure_enabled
.store(route_backpressure_enabled, Ordering::Relaxed);
self.transport_policy
.me_route_fairshare_enabled
.store(route_fairshare_enabled, Ordering::Relaxed);
self.transport_policy
.me_reader_route_data_wait_ms
.store(reader_route_data_wait_ms, Ordering::Relaxed);
@@ -1438,6 +1422,22 @@ impl MePool {
MeFloorMode::from_u8(self.floor_runtime.me_floor_mode.load(Ordering::Relaxed))
}
pub(super) fn adaptive_floor_idle_duration(&self) -> Duration {
Duration::from_secs(
self.floor_runtime
.me_adaptive_floor_idle_secs
.load(Ordering::Relaxed),
)
}
pub(super) fn adaptive_floor_recover_grace_duration(&self) -> Duration {
Duration::from_secs(
self.floor_runtime
.me_adaptive_floor_recover_grace_secs
.load(Ordering::Relaxed),
)
}
pub(super) fn adaptive_floor_min_writers_multi_endpoint(&self) -> usize {
(self
.floor_runtime
@@ -1659,7 +1659,6 @@ impl MePool {
&self,
contour: WriterContour,
allow_coverage_override: bool,
writer_dc: i32,
) -> bool {
let (active_writers, warm_writers, _) = self.non_draining_writer_counts_by_contour().await;
match contour {
@@ -1671,43 +1670,6 @@ impl MePool {
if !allow_coverage_override {
return false;
}
let mut endpoints_len = 0;
let now_epoch = Self::now_epoch_secs();
if self.family_enabled_for_drain_coverage(IpFamily::V4, now_epoch) {
if let Some(addrs) = self.proxy_map_v4.read().await.get(&writer_dc) {
endpoints_len += addrs.len();
}
}
if self.family_enabled_for_drain_coverage(IpFamily::V6, now_epoch) {
if let Some(addrs) = self.proxy_map_v6.read().await.get(&writer_dc) {
endpoints_len += addrs.len();
}
}
if endpoints_len > 0 {
let base_req =
self.required_writers_for_dc_with_floor_mode(endpoints_len, false);
let active_for_dc = {
let ws = self.writers.read().await;
ws.iter()
.filter(|w| {
!w.draining.load(std::sync::atomic::Ordering::Relaxed)
&& w.writer_dc == writer_dc
&& matches!(
WriterContour::from_u8(
w.contour.load(std::sync::atomic::Ordering::Relaxed),
),
WriterContour::Active
)
})
.count()
};
if active_for_dc < base_req {
return true;
}
}
let coverage_required = self.active_coverage_required_total().await;
active_writers < coverage_required
}

Some files were not shown because too many files have changed in this diff Show More