Compare commits

...

98 Commits

Author SHA1 Message Date
Alexey 32d5cee01c Bump 2026-04-15 02:18:44 +03:00
Alexey 3a17901e83 Reconnect logic for single-endpoint DC + Handling single-endpoint outages + Windows build + Mask timeouts + BINDTODEVICE + Gray Action for API + Beobachten Path + Server.Listeners + Upstream V4/V6 + Server.Listeners + Upstream V4/V6: merge pull request #705 from telemt/flow
Reconnect logic for single-endpoint DC + Handling single-endpoint outages + Windows build + Mask timeouts + BINDTODEVICE + Gray Action for API + Beobachten Path + Server.Listeners + Upstream V4/V6 + Server.Listeners + Upstream V4/V6
2026-04-15 02:02:51 +03:00
Alexey 902a4e83cf Specific scopes for Connectivity by #699 and #700 2026-04-15 01:56:49 +03:00
Alexey 696316f919 Rustfmt 2026-04-15 01:39:47 +03:00
Alexey d7a0319696 Server.Listeners + Upstream V4/V6
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-15 01:32:49 +03:00
Alexey 3fefcdd11f Fix for beobachten path by #664
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-14 20:09:31 +03:00
Alexey 57dca639f0 Gray Action for API by #630
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-14 19:19:06 +03:00
Alexey 13f86062f4 BINDTODEVICE for Direct Upstreams by #683
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-14 18:32:06 +03:00
Alexey 9303c7854a Merge pull request #701 from groozchique/main
[FAQ] Updated info + section about Telegram DC interaction
2026-04-14 18:05:47 +03:00
Alexey 8267149b53 Merge pull request #695 from vladon/fix/windows-run-inner-issue-690
fix(maestro): restore Windows build after cfg split (fixes #690)
2026-04-14 18:04:56 +03:00
Alexey 30fab00bfd Merge branch 'flow' into fix/windows-run-inner-issue-690 2026-04-14 18:01:18 +03:00
Nick Parfyonov afc07345f5 [docs] fix typo in FAQ.en.md 2026-04-14 15:07:44 +03:00
Nick Parfyonov a965b38bd4 [docs] add section about client interaction with Telegram DCs 2026-04-14 14:59:04 +03:00
Nick Parfyonov f0ebbac338 [docs] update information about TLS fingerprint in FAQ
Updated information about TLS fingerprint issue and notice for users to update their clients
2026-04-14 14:26:12 +03:00
Alexey 286662fc51 Merge pull request #697 from TWRoman/main
[docs] Updated QUICK START GUIDEs and READMEs
2026-04-13 19:39:05 +03:00
TWRoman c5390baaf1 Merge branch 'main' of github.com:TWRoman/telemt_docs 2026-04-13 11:15:49 +03:00
TWRoman 1cd1e96079 Fixed server.listeners and upstreams description 2026-04-13 11:14:02 +03:00
Roman 2b995c31b0 Update README.md
Fixed the link for README.ru
2026-04-13 10:20:25 +03:00
Roman 442320302d Update QUICK_START_GUIDE.ru.md 2026-04-13 10:14:39 +03:00
Roman ac0dde567b Update README.ru.md 2026-04-13 10:07:50 +03:00
TWRoman b2fe9b78d8 [docs] Updated READMEs 2026-04-13 10:05:55 +03:00
TWRoman f039ce1827 [docs] Updated QUICK START GUIDES 2026-04-13 09:56:44 +03:00
Vladislav Yaroslavlev abff2fd7fe fix(maestro): restore Windows build (missing run_inner)
The full runtime entry was gated with #[cfg(unix)] while run() still called
run_inner() on non-Unix targets, causing E0425 on Windows (issue #690).

Extract shared pipeline into run_telemt_core with a post-bind hook for Unix
privilege dropping; provide cfg-split run_inner wrappers.

Fixes https://github.com/telemt/telemt/issues/690

Made-with: Cursor
2026-04-13 00:21:19 +03:00
Alexey 0b580eccd3 Merge pull request #693 from telemt/flow-timeouts
Configureable mask timeouts
2026-04-12 19:51:59 +03:00
Alexey 70b63e4e0b Merge pull request #623 from Batmaev/feat-config-mask-timeouts
Configure mask timeouts
2026-04-12 19:16:33 +03:00
Alexey 5f5a3e3fa0 Merge pull request #673 from Artymediys/main
docs: align LTO notes, API docs, and Fake-TLS guidance
2026-04-12 19:15:45 +03:00
Alexey f9e54ee739 Merge pull request #688 from TWRoman/main
[docs]Update CONFIG-PARAMS
2026-04-12 15:48:43 +03:00
Roman d477d6ee29 Update CONFIG_PARAMS.ru.md
Corrected override_dc and default_dc descriptions.
2026-04-12 13:54:22 +03:00
TWRoman 1383dfcbb1 [docs]Update CONFIG-PARAMS 2026-04-12 12:37:38 +03:00
Artymediys 107a7cc758 Merge branch 'main' into main 2026-04-12 12:11:07 +03:00
Artymediys 4f3193fdaa Merge branch 'main' into main 2026-04-12 12:11:07 +03:00
Artymediys d6be691c67 Merge branch 'main' into main 2026-04-12 12:10:26 +03:00
Artymediys 0b0be07a9c docs: align LTO notes, API docs, and Fake-TLS guidance 2026-04-12 12:02:14 +03:00
Batmaev 26c40092f3 rm hardcoded mask timeouts 2026-04-12 10:46:18 +03:00
Alexey 192a852034 Merge pull request #687 from telemt/flow-flap
Endpoint handling during single endpoint outages
2026-04-12 10:43:46 +03:00
Alexey 16c7a63fbc Fix test for single-endpoint DC 2026-04-12 10:38:22 +03:00
Alexey 69a73d5fec Merge pull request #647 from miniusercoder/flow
fix(me): stabilize single-endpoint DC writer recovery and floor behavior
2026-04-12 10:19:25 +03:00
Alexey 7b1aa46753 Deleting API and CONFIG_PARAMS 2026-04-12 10:19:06 +03:00
Alexey a728c727bc Merge pull request #669 from mammuthus/chore/update-grafana-dashboard-json
Updated and extended grafana dashboard
2026-04-11 20:12:28 +03:00
Alexey d23ce4a184 Merge pull request #671 from miniusercoder/xray-double-hop
add documentation for Xray double hop setup
2026-04-11 20:12:00 +03:00
Alexey e48e1b141d Merge pull request #686 from Misha20062006/patch-1
Rename TememtAPI to TelemtAPI (fix typo)
2026-04-11 20:09:24 +03:00
Misha20062006 82da541f9c Rename TememtAPI to TelemtAPI (fix typo)
Fixed a typo in class names and exceptions where 'Tememt' was used instead of 'Telemt'.
2026-04-11 17:35:25 +03:00
Alexey 6d5a1a29df Merge pull request #677 from xaosproxy/feat/rst-on-close
feat: add configurable RST-on-close mode for client sockets
2026-04-11 10:35:16 +03:00
Alexey 026ca5cc1d Merge pull request #678 from avbor/main
Fixed link to quick start guide
2026-04-11 10:34:05 +03:00
Alexey b11dec7f91 Update FUNDING.yml 2026-04-10 20:37:09 +03:00
Alexey edd1405562 Update FUNDING.yml 2026-04-10 20:34:43 +03:00
brekotis 45dd7485a9 Create FUNDING.yml 2026-04-10 15:49:29 +03:00
brekotis 901cf11c51 Add donation section to README.md 2026-04-10 15:48:24 +03:00
miniusercoder 7acc76b422 fix quick start link in xray double hop 2026-04-10 13:45:53 +03:00
Alexey 227a64ef06 Update CODE_OF_CONDUCT.md 2026-04-10 13:17:51 +03:00
Alexander 6748ed920e Update VPS_DOUBLE_HOP.ru.md 2026-04-10 11:53:35 +03:00
Alexander 303b273c77 Update VPS_DOUBLE_HOP.en.md 2026-04-10 11:52:58 +03:00
Alexander 3bcc129b8d Fix link in quick start 2026-04-10 11:17:17 +03:00
Alexander 3ffbd294d2 Fix link to quick start 2026-04-10 11:16:41 +03:00
sintanial ddeda8d914 feat: add configurable RST-on-close mode for client sockets
Add `rst_on_close` config option (off/errors/always) to control
SO_LINGER(0) behaviour on accepted TCP connections.

- `off` (default): normal FIN on all closes, no behaviour change.
- `errors`: SO_LINGER(0) set on accept, cleared after successful
  handshake auth. Pre-handshake failures (scanners, DPI probes,
  timeouts) send RST instead of FIN, eliminating FIN-WAIT-1 and
  orphan socket accumulation. Authenticated relay sessions still
  close gracefully with FIN.
- `always`: SO_LINGER(0) on accept, never cleared — all closes
  send RST regardless of handshake outcome.
2026-04-10 05:01:38 +03:00
Alexey 17fd01a2c4 Update CODE_OF_CONDUCT.md 2026-04-09 23:27:16 +03:00
Alexey 8ed43a562c Update CODE_OF_CONDUCT.md 2026-04-09 23:25:19 +03:00
Alexey fd6243b6cc Update CODE_OF_CONDUCT.md 2026-04-09 23:21:37 +03:00
Alexey 44127c6f96 Update CODE_OF_CONDUCT.md 2026-04-09 23:21:21 +03:00
Alexey a0c7a9e62c Update CODE_OF_CONDUCT.md 2026-04-09 23:17:06 +03:00
Alexey d7af1cc206 Update CODE_OF_CONDUCT.md 2026-04-09 23:07:58 +03:00
Alexey f8e22970c1 Merge pull request #670 from TWRoman/main
[docs] Update CONFIG-PARAMS and README
2026-04-09 21:55:47 +03:00
Roman 792f626336 Update README.ru.md 2026-04-09 21:53:08 +03:00
Roman c5c98bb7fa Update README.ru.md 2026-04-09 21:46:33 +03:00
Roman 6102280345 Update README.ru.md 2026-04-09 21:45:30 +03:00
Roman 177f0f0325 Update README.ru.md 2026-04-09 21:30:34 +03:00
Roman abcce12368 Merge branch 'main' into main 2026-04-09 21:26:40 +03:00
Alexey 31cbf31491 Update README.md 2026-04-09 21:18:52 +03:00
Alexey f479ecd1ad Update README.md 2026-04-09 21:14:42 +03:00
Alexey 5c953eb4ba Update README.md 2026-04-09 21:13:50 +03:00
Alexey 3771eb4ab2 Merge pull request #674 from agvol/main
Dashboards: add grafana dashboard by user
2026-04-09 21:07:27 +03:00
miniusercoder b246f0ed99 do not use haproxy in xray double hop configuration 2026-04-09 19:51:35 +03:00
Roman 07d19027f6 Merge branch 'main' into main 2026-04-09 19:21:28 +03:00
Alexey 877d16659e Merge pull request #666 from miniusercoder/highload-docs
Add High-Load Configuration & Tuning Guide
2026-04-09 18:58:13 +03:00
miniusercoder 1265234491 xray with xhttp configuration 2026-04-09 18:48:37 +03:00
mamuthus 07b53785c5 fix: set dashboard metadata name to Telemt MtProto proxy 2026-04-09 13:17:21 +00:00
mamuthus 1e3522652c chore: sync grafana dashboard json 2026-04-09 13:17:11 +00:00
Andrey Voloshin 79f4ff4eec Dashboards: add grafana dashboard by user 2026-04-09 15:55:35 +03:00
Roman e6c64525e3 Merge branch 'main' into main 2026-04-09 13:02:03 +03:00
Roman 59df74e341 Update README.ru.md 2026-04-09 11:58:29 +03:00
TWRoman 21a33e4d2a New button for README 2026-04-09 10:15:46 +03:00
Roman 73bf23eb61 Update README.md
Lost dot in README ^-^
2026-04-09 09:20:10 +03:00
TWRoman 4a904568da Minor changes in README 2026-04-09 09:04:54 +03:00
miniusercoder a526fee728 fix documentation for Xray double hop setup 2026-04-08 22:24:51 +03:00
TWRoman 265478b9ca [docs] Update CONFIG-PARAMS.en, ru 2026-04-08 19:37:03 +03:00
Roman 038f688e75 Update CONFIG_PARAMS.ru.md 2026-04-08 19:28:28 +03:00
Roman fa3a1b4dbc Update CONFIG_PARAMS.ru.md 2026-04-08 19:25:26 +03:00
TWRoman e2e8b54f87 [docs] Update CONFIG-PARAMS.en 2026-04-08 19:21:44 +03:00
miniusercoder 970313edcb add documentation for Xray double hop setup 2026-04-08 19:17:09 +03:00
TWRoman 45c66bc823 [docs] Update CONFIG-PARAMS.en 2026-04-08 19:10:26 +03:00
miniusercoder 5e38a72add Remove maxconn and nbthread settings from high load configuration examples 2026-04-08 18:29:04 +03:00
miniusercoder 7ba02ea3d5 fix double-hop highload config example 2026-04-08 16:01:36 +03:00
miniusercoder 38c5f73d6a Add High-Load Configuration & Tuning Guide 2026-04-08 15:52:21 +03:00
miniusercoder 185e0081d7 fix(pool): improve endpoint handling during single endpoint outages 2026-04-07 18:57:22 +03:00
miniusercoder b6a30c1b51 refactor: cargo fmt fixes 2026-04-07 13:52:35 +03:00
miniusercoder 19f9eb36ac docs(api): update descriptions for outage mode parameters in API documentation 2026-04-06 21:38:19 +03:00
miniusercoder 2b8159a65e fix(pool): enhance reconnect logic for single-endpoint data centers 2026-04-06 21:06:53 +03:00
miniusercoder 86be0d53fe fix(me-pool): resolve 0-writer blackouts with zero-allocation constraints
- Converts adaptive floor logic from proactive idle drops to reactive
  global capacity constraints, fixing sudden drops to 0 active writers.
- Implements `base_req` override gateway via `can_open_writer_for_contour`,
  retaining critical connections for starved datacenters during bursts.
- Applies zero-allocation performance optimization via direct inner lock iter,
  avoiding `HashSet` generation and deep `RwLock` checks in writer validation paths.
- Scrubs now-dead variables/evaluations (`adaptive_idle_since`,
  `adaptive_recover_until`) to fulfill strict memory & hot-path constraints.
2026-04-06 20:27:17 +03:00
78 changed files with 11765 additions and 4908 deletions
+16
View File
@@ -0,0 +1,16 @@
# 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
+45 -56
View File
@@ -3,50 +3,39 @@
## Purpose ## Purpose
**Telemt exists to solve technical problems.** **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.
Telemt is open to contributors who want to learn, improve and build meaningful systems together. > **Absicht bestimmt die Form**
It is a place for building, testing, reasoning, documenting, and improving systems. > Design follows intent
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 ## Principles
* **Technical over emotional** * **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** * **Clarity over noise**
- Communication is structured, concise, and relevant.
Communication is structured, concise, and relevant.
* **Openness with standards** * **Openness with standards**
- Participation is open. The work remains disciplined.
Participation is open. The work remains disciplined.
* **Independence of judgment** * **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** * **Responsibility over capability**
- Capability does not justify careless use.
Capability does not justify careless use.
* **Cooperation over friction** * **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** * **Good intent, rigorous method**
- Assume good intent, but require rigor.
Assume good intent, but require rigor.
> **Aussagen gelten nach ihrer Begründung.** > **Aussagen gelten nach ihrer Begründung.**
@@ -68,7 +57,9 @@ Participants are expected to:
Precision is learned. 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.** > **Wer behauptet, belegt.**
@@ -112,7 +103,7 @@ Security is both technical and behavioral.
--- ---
## 6. Openness ## Openness
Telemt is open to contributors of different backgrounds, experience levels, and working styles. Telemt is open to contributors of different backgrounds, experience levels, and working styles.
@@ -148,10 +139,9 @@ Judgment should be exercised with restraint, consistency, and institutional resp
All decisions are expected to serve the durability, clarity, and integrity of Telemt. All decisions are expected to serve the durability, clarity, and integrity of Telemt.
> **Ordnung ist Voraussetzung der Funktion.** > **Klarheit vor Zustimmung - Bestand vor Beifall**
> Order is the precondition of function.
> Clarity above approval - substantiality before success
--- ---
## Enforcement ## Enforcement
@@ -171,42 +161,41 @@ Actions are taken to maintain function, continuity, and signal quality.
## Final ## Final
Telemt is built on discipline, structure, and shared intent. **Telemt is built on discipline, structure, and shared intent**
- Signal over noise. - Signal over noise
- Facts over opinion. - Facts over opinion
- Systems over rhetoric. - Systems over rhetoric
- Work is collective
- Outcomes are shared
- Responsibility is distributed
- Precision is learned
- Rigor is expected
- Help is part of the work
- Work is collective. > **Ordnung ist Voraussetzung der Freiheit**
- Outcomes are shared.
- Responsibility is distributed.
- Precision is learned. - If you contribute — contribute with care
- Rigor is expected. - If you speak — speak with substance
- Help is part of the work. - If you engage — engage constructively
> **Ordnung ist Voraussetzung der Freiheit.**
- If you contribute — contribute with care.
- If you speak — speak with substance.
- If you engage — engage constructively.
--- ---
## After All ## After All
Systems outlive intentions. Systems outlive intentions
- What is built will be used. - What is built will be used
- What is released will propagate. - What is released will propagate
- What is maintained will define the future state. - 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
> **Jedes System trägt Verantwortung.** > **Ordnung → Umsetzung → Ergebnis**
> Every system carries responsibility. > Order → Implementation → Result
- Stability requires discipline. - Stability requires discipline
- Freedom requires structure. - Freedom requires structure
- Trust requires honesty. - Trust requires honesty
In the end: the system reflects its contributors
In the end: the system reflects its contributors.
Generated
+1 -1
View File
@@ -2780,7 +2780,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]] [[package]]
name = "telemt" name = "telemt"
version = "3.3.39" version = "3.4.0"
dependencies = [ dependencies = [
"aes", "aes",
"anyhow", "anyhow",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "telemt" name = "telemt"
version = "3.3.39" version = "3.4.0"
edition = "2024" edition = "2024"
[features] [features]
+47 -40
View File
@@ -1,31 +1,33 @@
# Telemt - MTProxy on Rust + Tokio # Telemt - MTProxy on Rust + Tokio
![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon) ![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social) ![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social) [![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*** ***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist***
> [!NOTE] > [!NOTE]
> >
> Fixed TLS ClientHello is now available: > Fixed TLS ClientHello is now available in official clients for Desktop / Android / iOS
> - 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! > To work with EE-MTProxy, please update your client!
<p align="center"> <p align="center">
<a href="https://t.me/telemtrs"> <a href="https://t.me/telemtrs">
<img src="/docs/assets/telegram_button.svg" width="200"/> <img src="/docs/assets/telegram_button.svg" width="150"/>
</a> </a>
</p> </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 such as: **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
- [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.
![telemt_scheme](docs/assets/telemt.png) ### 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)
## 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 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 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
@@ -40,29 +42,17 @@ Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared t
- Graceful shutdown on Ctrl+C; - Graceful shutdown on Ctrl+C;
- Extensive logging via `trace` and `debug` with `RUST_LOG` method. - 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
- [FAQ RU](docs/FAQ.ru.md) - [FAQ RU](docs/FAQ.ru.md)
- [FAQ EN](docs/FAQ.en.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 ## Build
```bash ```bash
# Cloning repo # Cloning repo
@@ -72,9 +62,8 @@ cd telemt
# Starting Release Build # Starting Release Build
cargo build --release cargo build --release
# Low-RAM devices (1 GB, e.g. NanoPi Neo3 / Raspberry Pi Zero 2): # Current release profile uses lto = "fat" for maximum optimization (see Cargo.toml).
# release profile uses lto = "thin" to reduce peak linker memory. # On low-RAM systems (~1 GB) you can override it to "thin".
# If your custom toolchain overrides profiles, avoid enabling fat LTO.
# Move to /bin # Move to /bin
mv ./target/release/telemt /bin mv ./target/release/telemt /bin
@@ -84,15 +73,33 @@ chmod +x /bin/telemt
telemt config.toml 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? ## Why Rust?
- Long-running reliability and idempotent behavior - Long-running reliability and idempotent behavior
- Rust's deterministic resource management - RAII - Rust's deterministic resource management - RAII
- No garbage collector - No garbage collector
- Memory safety and reduced attack surface - Memory safety and reduced attack surface
- Tokio's asynchronous architecture - 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)
+38 -39
View File
@@ -1,30 +1,31 @@
# Telemt — MTProxy на Rust + Tokio # Telemt — MTProxy на Rust + Tokio
![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon) ![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social) ![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs)
***Решает проблемы раньше, чем другие узнают об их существовании*** ***Решает проблемы раньше, чем другие узнают об их существовании***
> [!NOTE] > [!NOTE]
> >
> Исправленный TLS ClientHello доступен в **Telegram Desktop** начиная с версии **6.7.2**: для работы с EE-MTProxy обновите клиент. > Исправленный TLS ClientHello доступен в Telegram для настольных ПК, Android и iOS.
> >
> Исправленный TLS ClientHello доступен в **Telegram Android** начиная с версии **12.6.4**; **официальный релиз для iOS находится в процессе разработки**. > Пожалуйста, обновите клиентское приложение для работы с EE-MTProxy.
<p align="center"> <p align="center">
<a href="https://t.me/telemtrs"> <a href="https://t.me/telemtrs">
<img src="/docs/assets/telegram_button.svg" width="200"/> <img src="/docs/assets/telegram_button.svg" width="150"/>
</a> </a>
</p> </p>
**Telemt** — это быстрый, безопасный и функциональный сервер, написанный на Rust. Он полностью реализует официальный алгоритм прокси Telegram и добавляет множество улучшений для продакшена: **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.
![telemt_scheme](docs/assets/telemt.png) ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
## Особенности - [Инструкция по быстрому запуску](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-и-сканеров)). Реализация **TLS-fronting** максимально приближена к поведению реального HTTPS-трафика (подробнее - [FAQ](docs/FAQ.ru.md#распознаваемость-для-dpi-и-сканеров)).
@@ -40,35 +41,19 @@
- Корректное завершение работы (Ctrl+C); - Корректное завершение работы (Ctrl+C);
- Подробное логирование через `trace` и `debug`. - Подробное логирование через `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) - [FAQ](#faq)
- [Архитектура](docs/Architecture) - [Архитектура](docs/Architecture)
- [Быстрый старт](#quick-start-guide)
- [Параметры конфигурационного файла](docs/Config_params) - [Параметры конфигурационного файла](docs/Config_params)
- [Сборка](#build) - [Сборка](#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) - [Почему 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
- [FAQ RU](docs/FAQ.ru.md) - [FAQ RU](docs/FAQ.ru.md)
- [FAQ EN](docs/FAQ.en.md) - [FAQ EN](docs/FAQ.en.md)
## Сборка ## Сборка
```bash ```bash
# Клонируйте репозиторий # Клонируйте репозиторий
git clone https://github.com/telemt/telemt git clone https://github.com/telemt/telemt
@@ -77,9 +62,8 @@ cd telemt
# Начните процесс сборки # Начните процесс сборки
cargo build --release cargo build --release
# Устройства с небольшим объёмом оперативной памяти (1 ГБ, например NanoPi Neo3 / Raspberry Pi Zero 2): # В текущем release-профиле используется lto = "fat" для максимальной оптимизации (см. Cargo.toml).
# используется параметр lto = «thin» для уменьшения пикового потребления памяти. # На системах с малым объёмом RAM (~1 ГБ) можно переопределить это значение на "thin".
# Если ваш пользовательский набор инструментов переопределяет профили, не используйте Fat LTO.
# Перейдите в каталог /bin # Перейдите в каталог /bin
mv ./target/release/telemt /bin mv ./target/release/telemt /bin
@@ -89,22 +73,37 @@ chmod +x /bin/telemt
telemt config.toml telemt config.toml
``` ```
### Устройства с малым объемом RAM ## Установка на BSD
Для устройств с ~1 ГБ RAM (например Raspberry Pi):
- используется облегчённая оптимизация линковщика (thin LTO);
- не рекомендуется включать fat LTO.
## OpenBSD
- Руководство по сборке и настройке на английском языке [OpenBSD Guide (EN)](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md); - Руководство по сборке и настройке на английском языке [OpenBSD Guide (EN)](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md);
- Пример rc.d скрипта: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd); - Пример rc.d скрипта: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd);
- Поддержка sandbox с `pledge(2)` и `unveil(2)` пока не реализована. - Поддержка sandbox с `pledge(2)` и `unveil(2)` пока не реализована.
## Почему Rust? ## Почему Rust?
- Надёжность для долгоживущих процессов; - Надёжность для долгоживущих процессов;
- Детерминированное управление ресурсами (RAII); - Детерминированное управление ресурсами (RAII);
- Отсутствие сборщика мусора; - Отсутствие сборщика мусора;
- Безопасность памяти; - Безопасность памяти;
- Асинхронная архитектура Tokio. - Асинхронная архитектура 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)
+8 -5
View File
@@ -32,13 +32,13 @@ show = "*"
port = 443 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_port = 9090
# metrics_listen = "0.0.0.0:9090" # Listen address for metrics (overrides metrics_port) # metrics_listen = "127.0.0.1:9090" # Listen address for metrics (overrides metrics_port)
# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"] # metrics_whitelist = ["127.0.0.1/32", "::1/128"]
[server.api] [server.api]
enabled = true enabled = true
listen = "0.0.0.0:9091" listen = "127.0.0.1:9091"
whitelist = ["127.0.0.0/8"] whitelist = ["127.0.0.1/32", "::1/128"]
minimal_runtime_enabled = false minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000 minimal_runtime_cache_ttl_ms = 1000
@@ -48,9 +48,12 @@ ip = "0.0.0.0"
# === Anti-Censorship & Masking === # === Anti-Censorship & Masking ===
[censorship] [censorship]
# Fake-TLS / SNI masking domain used in generated ee-links.
# Changing tls_domain invalidates previously generated TLS links.
tls_domain = "petrovich.ru" tls_domain = "petrovich.ru"
mask = true 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 tls_front_dir = "tlsfront" # Cache directory for TLS emulation
[access.users] [access.users]
+141
View File
@@ -0,0 +1,141 @@
# High-Load Configuration & Tuning Guide
When deploying Telemt under high-traffic load (tens or hundreds of thousands of concurrent connections), the standard OS network stack limits can lead to packet drops, high CPU context switching, and connection failures. This guide covers Linux kernel tuning, hardware configuration, and architecture optimizations required to prepare the server for high-load scenarios.
---
## 1. System Limits & File Descriptors
Every TCP connection requires a file descriptor. At 100k connections, standard Linux limits (often 1024 or 65535) will be exhausted immediately.
### System-Wide Limits (`sysctl`)
Increase the global file descriptor limit in `/etc/sysctl.conf`:
```ini
fs.file-max = 2097152
fs.nr_open = 2097152
```
### User-Level Limits (`limits.conf`)
Edit `/etc/security/limits.conf` to allow the telemt (or proxy) user to allocate them:
```conf
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
```
### Systemd / Docker Overrides
If using **Systemd**, add to your `telemt.service`:
```ini
[Service]
LimitNOFILE=1048576
LimitNPROC=65535
TasksMax=infinity
```
If using **Docker**, configure `ulimits` in `docker-compose.yaml`:
```yaml
services:
telemt:
ulimits:
nofile:
soft: 1048576
hard: 1048576
```
---
## 2. Kernel Network Stack Tuning (`sysctl`)
Create a dedicated file `/etc/sysctl.d/99-telemt-highload.conf` and apply it via `sysctl -p /etc/sysctl.d/99-telemt-highload.conf`.
### 2.1 Connection Queues & SYN Flood Protection
Increase the size of accept queues to absorb sudden connection spikes (bursts) and mitigate SYN floods:
```ini
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_syncookies = 1
```
### 2.2 Port Exhaustion & TIME-WAIT Sockets
High churn rates lead to ephemeral port exhaustion. Expand the range and rapidly recycle closed sockets:
```ini
net.ipv4.ip_local_port_range = 10000 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_tw_buckets = 2000000
```
### 2.3 TCP Keepalive (Aggressive Dead Connection Culling)
By default, Linux keeps silent, dropped connections open for over 2 hours. This consumes memory at scale. Configure the system to detect and drop them in < 5 minutes:
```ini
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
```
### 2.4 TCP Buffers & Congestion Control
Optimize memory usage per socket and switch to BBR (Bottleneck Bandwidth and Round-trip propagation time) to improve latency on lossy networks:
```ini
# Core buffer sizes
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# TCP specific buffers (min, default, max)
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Enable BBR
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
```
---
## 3. Conntrack (Netfilter) Tuning
If your server uses `iptables`, `ufw`, or `firewalld`, the Linux kernel tracks every connection state in a table (`nf_conntrack`). When this table fills up, Linux drops new packets.
Check your current limit and usage:
```bash
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count
```
If it gets close to the limit, tune it up, and reduce the time established connections linger in the tracker:
```ini
# In /etc/sysctl.d/99-telemt-highload.conf
net.netfilter.nf_conntrack_max = 2097152
# Reduce timeout from default 5 days to 1 hour
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 12
```
*Note: Depending on your OS, you may need to run `modprobe nf_conntrack` before setting these parameters.*
---
## 4. Multi-Tier Architecture: HAProxy Setup
For massive traffic loads, buffering Telemt behind a reverse proxy like HAProxy can help absorb connection spikes and handle basic TCP connections before handing them off.
### HAProxy High-Load `haproxy.cfg`
```haproxy
global
# Disable detailed logging under load
log stdout format raw local0 err
# maxconn 250000
# Buffer tuning
tune.bufsize 16384
tune.maxaccept 64
defaults
log global
mode tcp
option clitcpka
option srvtcpka
timeout connect 5s
timeout client 1h
timeout server 1h
# Quick purge for dead peers
timeout client-fin 10s
timeout server-fin 10s
frontend proxy_in
bind *:443
maxconn 250000
option tcp-smart-accept
default_backend telemt_backend
backend telemt_backend
option tcp-smart-connect
# Send-Proxy-V2 to preserve Client IP for Telemt's internal logic
server telemt_core 10.10.10.1:443 maxconn 250000 send-proxy-v2 check inter 5s
```
**Important**: Telemt must be configured to process the `PROXY` protocol on port `443` for this chain to work and preserve client IPs.
---
## 5. Diagnostics & Monitoring
When operating under load, these commands are useful for diagnostics:
* **Checking dropped connections (Queues full)**: `netstat -s | grep "times the listen queue of a socket overflowed"`
* **Checking Conntrack drops**: `dmesg | grep conntrack`
* **Checking File Descriptor usage**: `cat /proc/sys/fs/file-nr`
* **Real-time connection states**: `ss -s` (Avoid using `netstat` on heavy loads).
+139
View File
@@ -0,0 +1,139 @@
# Руководство по High-Load конфигурации и тюнингу
При развертывании Telemt под высокой нагрузкой (десятки и сотни тысяч одновременных подключений), стандартные ограничения сетевого стека ОС могут приводить к потерям пакетов, переключениям контекста CPU и отказам в соединениях. В данном руководстве описана настройка ядра Linux, системных лимитов и аппаратной конфигурации для работы в подобных сценариях.
---
## 1. Системные лимиты и файловые дескрипторы
Каждое TCP-сосоединение требует файлового дескриптора. При 100 тысячах соединений стандартные лимиты Linux (зачастую 1024 или 65535) будут исчерпаны немедленно.
### Общесистемные лимиты (`sysctl`)
Увеличьте глобальный лимит файловых дескрипторов в `/etc/sysctl.conf`:
```ini
fs.file-max = 2097152
fs.nr_open = 2097152
```
### На уровне пользователя (`limits.conf`)
Отредактируйте `/etc/security/limits.conf`, чтобы разрешить пользователю (от которого запущен telemt) резервировать дескрипторы:
```conf
* soft nofile 1048576
* hard nofile 1048576
root soft nofile 1048576
root hard nofile 1048576
```
### Переопределения для Systemd / Docker
Если используется **Systemd**, добавьте в ваш `telemt.service`:
```ini
[Service]
LimitNOFILE=1048576
LimitNPROC=65535
TasksMax=infinity
```
Если используется **Docker**, задайте `ulimits` в `docker-compose.yaml`:
```yaml
services:
telemt:
ulimits:
nofile:
soft: 1048576
hard: 1048576
```
---
## 2. Тонкая настройка сетевого стека ядра (`sysctl`)
Создайте выделенный файл `/etc/sysctl.d/99-telemt-highload.conf` и примените его через `sysctl -p /etc/sysctl.d/99-telemt-highload.conf`.
### 2.1 Очереди соединений и защита от SYN-флуда
Увеличьте размеры очередей, чтобы поглощать внезапные всплески соединений и смягчить атаки типа SYN flood:
```ini
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 65535
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_syncookies = 1
```
### 2.2 Исчерпание портов и TIME-WAIT сокеты
Высокая текучесть приводит к нехватке временных (ephemeral) портов. Расширьте диапазон портов и позвольте ядру быстро переиспользовать закрытые сокеты:
```ini
net.ipv4.ip_local_port_range = 10000 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_max_tw_buckets = 2000000
```
### 2.3 TCP Keepalive (Агрессивная очистка мертвых соединений)
По умолчанию Linux держит "оборванные" TCP-сессии более 2 часов. Задайте параметры для обнаружения и сброса мертвых соединений за менее чем 5 минут:
```ini
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_keepalive_probes = 5
```
### 2.4 Буферы TCP и управление перегрузками (Congestion Control)
Оптимизируйте использование памяти на сокет и переключитесь на алгоритм BBR (Bottleneck Bandwidth and Round-trip propagation time) для улучшения задержки на плохих сетях:
```ini
# Размеры буферов ядра (по умолчанию и макс)
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
# Специфичные TCP буферы (min, default, max)
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
# Включение BBR
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
```
---
## 3. Тюнинг Conntrack (Netfilter)
Если ваш сервер использует `iptables`, `ufw` или `firewalld`, ядро вынуждено отслеживать каждое соединение в таблице состояний (`nf_conntrack`). Когда эта таблица переполняется, Linux отбрасывает новые пакеты без уведомления приложения.
Проверьте текущие лимиты и использование:
```bash
sysctl net.netfilter.nf_conntrack_max
sysctl net.netfilter.nf_conntrack_count
```
Если вы близки к пределу, увеличьте таблицу и заставьте ядро быстрее удалять установленные соединения. Добавьте в `/etc/sysctl.d/99-telemt-highload.conf`:
```ini
net.netfilter.nf_conntrack_max = 2097152
# Снижаем таймаут с дефолтных 5 дней до 1 часа
net.netfilter.nf_conntrack_tcp_timeout_established = 3600
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 12
```
*Внимание: в зависимости от ОС, вам может потребоваться выполнить `modprobe nf_conntrack` перед установкой этих параметров.*
---
## 4. Архитектура: Развертывание за HAProxy
Для максимальных нагрузок выставление Telemt напрямую в интернет менее эффективно, чем использование оптимизированного L4-балансировщика. HAProxy эффективен в поглощении TCP атак, обработке рукопожатий и сглаживании всплесков подключений.
### Оптимизация `haproxy.cfg` для High-Load
```haproxy
global
# Отключить детальные логи соединений под нагрузкой
log stdout format raw local0 err
maxconn 250000
# Тюнинг буферов и приема сокетов
tune.bufsize 16384
tune.maxaccept 64
defaults
log global
mode tcp
option clitcpka
option srvtcpka
timeout connect 5s
timeout client 1h
timeout server 1h
# Быстрая очистка мертвых пиров
timeout client-fin 10s
timeout server-fin 10s
frontend proxy_in
bind *:443
maxconn 250000
option tcp-smart-accept
default_backend telemt_backend
backend telemt_backend
option tcp-smart-connect
# Send-Proxy-V2 обязателен для сохранения IP клиента внутри внутренней логики Telemt
server telemt_core 10.10.10.1:443 maxconn 250000 send-proxy-v2 check inter 5s
```
**Важно**: Telemt должен быть настроен на обработку протокола `PROXY` на порту `443`, чтобы получать оригинальные IP-адреса клиентов.
---
## 5. Диагностика
Команды для выявления узких мест:
* **Проверка дропов TCP (переполнение очередей)**: `netstat -s | grep "times the listen queue of a socket overflowed"`
* **Контроль отбрасывания пакетов Conntrack**: `dmesg | grep conntrack`
* **Проверка использования файловых дескрипторов**: `cat /proc/sys/fs/file-nr`
* **Отображение состояния сокетов**: `ss -s` (Избегайте использования `netstat` под высокой нагрузкой).
+4 -4
View File
@@ -9,12 +9,12 @@ API runtime is configured in `[server.api]`.
| Field | Type | Default | Description | | Field | Type | Default | Description |
| --- | --- | --- | --- | | --- | --- | --- | --- |
| `enabled` | `bool` | `false` | Enables REST API listener. | | `enabled` | `bool` | `true` | Enables REST API listener. |
| `listen` | `string` (`IP:PORT`) | `127.0.0.1:9091` | API bind address. | | `listen` | `string` (`IP:PORT`) | `0.0.0.0:9091` | API bind address. |
| `whitelist` | `CIDR[]` | `127.0.0.1/32, ::1/128` | Source IP allowlist. Empty list means allow all. | | `whitelist` | `CIDR[]` | `127.0.0.0/8` | Source IP allowlist. Empty list means allow all. |
| `auth_header` | `string` | `""` | Exact value for `Authorization` header. Empty disables header auth. | | `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`. | | `request_body_limit_bytes` | `usize` | `65536` | Maximum request body size. Must be `> 0`. |
| `minimal_runtime_enabled` | `bool` | `false` | Enables runtime snapshot endpoints requiring ME pool read-lock aggregation. | | `minimal_runtime_enabled` | `bool` | `true` | 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]`. | | `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_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. | | `runtime_edge_cache_ttl_ms` | `u64` | `1000` | Cache TTL for runtime edge summary payloads. `0` disables cache. |
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+25 -3
View File
@@ -36,8 +36,11 @@ hello2 = "ad_tag2"
On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS, 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, based on the ECH extension and the ordering of cipher suites,
as well as an overall unique JA3/JA4 fingerprint as well as an overall unique JA3/JA4 fingerprint
that does not occur in modern browsers: 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.
> [!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.
- We consider this a breakthrough aspect, which has no stable analogues today - 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 - Based on this: if `telemt` configured correctly, **TLS mode is completely identical to real-life handshake + communication** with a specified host
@@ -154,6 +157,24 @@ Keep-Alive: timeout=60
### Why do you need a middle proxy (ME) ### Why do you need a middle proxy (ME)
https://github.com/telemt/telemt/discussions/167 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 ### How many people can use one link
By default, an unlimited number of people can use a single 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: However, you can limit the number of unique IP addresses for each user:
@@ -161,7 +182,8 @@ However, you can limit the number of unique IP addresses for each user:
[access.user_max_unique_ips] [access.user_max_unique_ips]
hello = 1 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 ### How to create multiple different links
1. Generate the required number of secrets using the command: `openssl rand -hex 16`. 1. Generate the required number of secrets using the command: `openssl rand -hex 16`.
+22 -2
View File
@@ -33,9 +33,12 @@ hello = "ad_tag"
hello2 = "ad_tag2" hello2 = "ad_tag2"
``` ```
## Распознаваемость для DPI и сканеров ## Распознаваемость для DPI и сканеров
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров, 1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах: мы уже отправили первоначальные изменения разработчикам Telegram Desktop и работаем над обновлениями для других клиентов. а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах.
> [!IMPORTANT]
> Проблема с TLS отпечатком исправлена в последних версиях клиентов Telegram для Desktop / Android / iOS.
> Обновите свой клиент для корректной работы с MTProxy Fake-TLS!
- Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов; - Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов;
- Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом; - Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом;
@@ -152,6 +155,23 @@ Keep-Alive: timeout=60
## Зачем нужен middle proxy (ME) ## Зачем нужен middle proxy (ME)
https://github.com/telemt/telemt/discussions/167 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? ## Что такое dd и ee в контексте MTProxy?
@@ -27,7 +27,8 @@ cargo build --release
./target/release/telemt --version ./target/release/telemt --version
``` ```
For low-RAM systems, this repository already uses `lto = "thin"` in release profile. 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.
## 3. Install binary and config ## 3. Install binary and config
+37 -10
View File
@@ -1,9 +1,36 @@
# 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 # Very quick start
### One-command installation / update on re-run ### One-command installation / update on re-run
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh 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 ### Installing a specific version
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39 curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
@@ -110,15 +137,15 @@ show = "*"
# === Server Binding === # === Server Binding ===
[server] [server]
port = 443 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_port = 9090
# metrics_listen = "0.0.0.0:9090" # Listen address for metrics (overrides metrics_port) # metrics_listen = "127.0.0.1:9090" # Listen address for metrics (overrides metrics_port)
# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"] # metrics_whitelist = ["127.0.0.1/32", "::1/128"]
[server.api] [server.api]
enabled = true enabled = true
listen = "0.0.0.0:9091" listen = "127.0.0.1:9091"
whitelist = ["127.0.0.0/8"] whitelist = ["127.0.0.1/32", "::1/128"]
minimal_runtime_enabled = false minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000 minimal_runtime_cache_ttl_ms = 1000
@@ -128,9 +155,9 @@ ip = "0.0.0.0"
# === Anti-Censorship & Masking === # === Anti-Censorship & Masking ===
[censorship] [censorship]
tls_domain = "petrovich.ru" tls_domain = "petrovich.ru" # Fake-TLS / SNI masking domain used in generated ee-links
mask = true 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 tls_front_dir = "tlsfront" # Cache directory for TLS emulation
[access.users] [access.users]
@@ -141,9 +168,9 @@ hello = "00000000000000000000000000000000"
then Ctrl+S -> Ctrl+X to save then Ctrl+S -> Ctrl+X to save
> [!WARNING] > [!WARNING]
> Replace the value of the hello parameter with the value you obtained in step 0. > 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. > 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! > Changing the `tls_domain` parameter will break all links that use the old domain!
--- ---
+39 -13
View File
@@ -1,9 +1,35 @@
# Варианты установки
Имеется три варианта установки Telemt:
- [Автоматизированная установка с помощью скрипта](#очень-быстрый-старт).
- [Ручная установка Telemt в качестве службы](#telemt-через-systemd-вручную).
- [Установка через Docker Compose](#telemt-через-docker-compose).
# Очень быстрый старт # Очень быстрый старт
### Установка одной командой / обновление при повторном запуске ### Установка одной командой / обновление при повторном запуске
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh 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 ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39 curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
@@ -103,22 +129,22 @@ tls = true
[general.links] [general.links]
show = "*" show = "*"
# show = ["alice", "bob"] # Показывать ссылки только для alice и bob # show = ["alice", "bob"] # Показывать ссылки только для alice и bob
# show = "*"              # Показывать ссылки для всех пользователей # show = "*" # Показывать ссылки для всех пользователей
# public_host = "proxy.example.com"  # Хост (IP-адрес или домен) для ссылок tg:// # public_host = "proxy.example.com" # Хост (IP-адрес или домен) для ссылок tg://
# public_port = 443                  # Порт для ссылок tg:// (по умолчанию: server.port) # public_port = 443 # Порт для ссылок tg:// (по умолчанию: server.port)
# === Привязка сервера === # === Привязка сервера ===
[server] [server]
port = 443 port = 443
# proxy_protocol = false           # Включите, если сервер находится за HAProxy/nginx с протоколом PROXY # proxy_protocol = false # Включите, если сервер находится за HAProxy/nginx с протоколом PROXY
# metrics_port = 9090 # metrics_port = 9090
# metrics_listen = "0.0.0.0:9090"  # Адрес прослушивания для метрик (переопределяет metrics_port) # metrics_listen = "127.0.0.1:9090" # Адрес прослушивания для метрик (переопределяет metrics_port)
# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"] # metrics_whitelist = ["127.0.0.1/32", "::1/128"]
[server.api] [server.api]
enabled = true enabled = true
listen = "0.0.0.0:9091" listen = "127.0.0.1:9091"
whitelist = ["127.0.0.0/8"] whitelist = ["127.0.0.1/32", "::1/128"]
minimal_runtime_enabled = false minimal_runtime_enabled = false
minimal_runtime_cache_ttl_ms = 1000 minimal_runtime_cache_ttl_ms = 1000
@@ -128,9 +154,9 @@ ip = "0.0.0.0"
# === Обход блокировок и маскировка === # === Обход блокировок и маскировка ===
[censorship] [censorship]
tls_domain = "petrovich.ru" tls_domain = "petrovich.ru" # Домен Fake-TLS / SNI, который будет использоваться в сгенерированных ee-ссылках
mask = true mask = true
tls_emulation = true # Получить реальную длину сертификата и эмулировать запись TLS tls_emulation = true # Получить реальную длину сертификата и эмулировать запись TLS
tls_front_dir = "tlsfront" # Директория кэша для эмуляции TLS tls_front_dir = "tlsfront" # Директория кэша для эмуляции TLS
[access.users] [access.users]
@@ -141,9 +167,9 @@ hello = "00000000000000000000000000000000"
Затем нажмите Ctrl+S -> Ctrl+X, чтобы сохранить Затем нажмите Ctrl+S -> Ctrl+X, чтобы сохранить
> [!WARNING] > [!WARNING]
> Замените значение параметра hello на значение, которое вы получили в пункте 0. > Замените значение параметра `hello` на значение, которое вы получили в пункте 0.
> Так же замените значение параметра tls_domain на другой сайт. > Так же замените значение параметра `tls_domain` на другой сайт.
> Изменение параметра 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) ## 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_GUIDE.ru.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/QUICK_START_GUIDE.en.md) or [here](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
It is assumed that telemt expects connections on port `443\tcp`. 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. 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 (_условно Нидерланды_) ## Шаг 2. Установка telemt на Сервере B (_условно Нидерланды_)
Установка и настройка описаны [здесь](https://github.com/telemt/telemt/blob/main/docs/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/QUICK_START_GUIDE.ru.md) или [здесь](https://gitlab.com/An0nX/telemt-docker#-quick-start-docker-compose).\
Подразумевается что telemt ожидает подключения на порту `443\tcp`. Подразумевается что telemt ожидает подключения на порту `443\tcp`.
В конфиге telemt необходимо включить протокол `Proxy` и ограничить подключения к нему только через туннель. В конфиге telemt необходимо включить протокол `Proxy` и ограничить подключения к нему только через туннель.
+273
View File
@@ -0,0 +1,273 @@
<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 16
# Save the output (e.g.: 0123456789abcdef0123456789abcdef) — this is <SHORT_ID>
```
4. **Random Path (for xhttp):**
```bash
openssl rand -hex 8
# Save the output (e.g., abc123def456) 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
@@ -0,0 +1,272 @@
<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 16
# Сохраните вывод (например: 0123456789abcdef0123456789abcdef) — это <SHORT_ID>
```
4. **Random Path (путь для xhttp):**
```bash
openssl rand -hex 8
# Сохраните вывод (например, abc123def456), чтобы заменить <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`, и клиенты смогут подключаться по выданным ссылкам.
Executable → Regular
+1 -11
View File
@@ -1,11 +1 @@
<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"> <?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>
<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: 2.5 KiB

After

Width:  |  Height:  |  Size: 804 B

+38 -14
View File
@@ -1,6 +1,6 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use std::convert::Infallible; use std::io::{Error as IoError, ErrorKind};
use std::net::{IpAddr, SocketAddr}; use std::net::{IpAddr, SocketAddr};
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::Arc; use std::sync::Arc;
@@ -16,7 +16,7 @@ use tokio::net::TcpListener;
use tokio::sync::{Mutex, RwLock, watch}; use tokio::sync::{Mutex, RwLock, watch};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
use crate::config::ProxyConfig; use crate::config::{ApiGrayAction, ProxyConfig};
use crate::ip_tracker::UserIpTracker; use crate::ip_tracker::UserIpTracker;
use crate::proxy::route_mode::RouteRuntimeController; use crate::proxy::route_mode::RouteRuntimeController;
use crate::startup::StartupTracker; use crate::startup::StartupTracker;
@@ -184,7 +184,9 @@ pub async fn serve(
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc) .serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
.await .await
{ {
debug!(error = %error, "API connection error"); if !error.is_user() {
debug!(error = %error, "API connection error");
}
} }
}); });
} }
@@ -195,7 +197,7 @@ async fn handle(
peer: SocketAddr, peer: SocketAddr,
shared: Arc<ApiShared>, shared: Arc<ApiShared>,
config_rx: watch::Receiver<Arc<ProxyConfig>>, config_rx: watch::Receiver<Arc<ProxyConfig>>,
) -> Result<Response<Full<Bytes>>, Infallible> { ) -> Result<Response<Full<Bytes>>, IoError> {
let request_id = shared.next_request_id(); let request_id = shared.next_request_id();
let cfg = config_rx.borrow().clone(); let cfg = config_rx.borrow().clone();
let api_cfg = &cfg.server.api; let api_cfg = &cfg.server.api;
@@ -213,14 +215,25 @@ async fn handle(
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip())) if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
{ {
return Ok(error_response( return match api_cfg.gray_action {
request_id, ApiGrayAction::Api => Ok(error_response(
ApiFailure::new( request_id,
StatusCode::FORBIDDEN, ApiFailure::new(
"forbidden", StatusCode::FORBIDDEN,
"Source IP is not allowed", "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",
)),
};
} }
if !api_cfg.auth_header.is_empty() { if !api_cfg.auth_header.is_empty() {
@@ -244,11 +257,16 @@ async fn handle(
let method = req.method().clone(); let method = req.method().clone();
let path = req.uri().path().to_string(); 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 query = req.uri().query().map(str::to_string);
let body_limit = api_cfg.request_body_limit_bytes; let body_limit = api_cfg.request_body_limit_bytes;
let result: Result<Response<Full<Bytes>>, ApiFailure> = async { let result: Result<Response<Full<Bytes>>, ApiFailure> = async {
match (method.as_str(), path.as_str()) { match (method.as_str(), normalized_path) {
("GET", "/v1/health") => { ("GET", "/v1/health") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let data = HealthData { let data = HealthData {
@@ -431,7 +449,7 @@ async fn handle(
Ok(success_response(status, data, revision)) Ok(success_response(status, data, revision))
} }
_ => { _ => {
if let Some(user) = path.strip_prefix("/v1/users/") if let Some(user) = normalized_path.strip_prefix("/v1/users/")
&& !user.is_empty() && !user.is_empty()
&& !user.contains('/') && !user.contains('/')
{ {
@@ -600,6 +618,12 @@ async fn handle(
), ),
)); ));
} }
debug!(
method = method.as_str(),
path = %path,
normalized_path = %normalized_path,
"API route not found"
);
Ok(error_response( Ok(error_response(
request_id, request_id,
ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"), ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"),
+13 -1
View File
@@ -452,7 +452,11 @@ fn build_user_links(
startup_detected_ip_v6: Option<IpAddr>, startup_detected_ip_v6: Option<IpAddr>,
) -> UserLinks { ) -> UserLinks {
let hosts = resolve_link_hosts(cfg, startup_detected_ip_v4, startup_detected_ip_v6); let hosts = resolve_link_hosts(cfg, startup_detected_ip_v4, startup_detected_ip_v6);
let port = cfg.general.links.public_port.unwrap_or(cfg.server.port); let port = cfg
.general
.links
.public_port
.unwrap_or(resolve_default_link_port(cfg));
let tls_domains = resolve_tls_domains(cfg); let tls_domains = resolve_tls_domains(cfg);
let mut classic = Vec::new(); let mut classic = Vec::new();
@@ -490,6 +494,14 @@ 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( fn resolve_link_hosts(
cfg: &ProxyConfig, cfg: &ProxyConfig,
startup_detected_ip_v4: Option<IpAddr>, startup_detected_ip_v4: Option<IpAddr>,
+2 -1
View File
@@ -598,16 +598,17 @@ secure = false
tls = true tls = true
[server] [server]
port = {port}
listen_addr_ipv4 = "0.0.0.0" listen_addr_ipv4 = "0.0.0.0"
listen_addr_ipv6 = "::" listen_addr_ipv6 = "::"
[[server.listeners]] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
port = {port}
# reuse_allow = false # Set true only when intentionally running multiple telemt instances on same port # reuse_allow = false # Set true only when intentionally running multiple telemt instances on same port
[[server.listeners]] [[server.listeners]]
ip = "::" ip = "::"
port = {port}
[timeouts] [timeouts]
client_first_byte_idle_secs = 300 client_first_byte_idle_secs = 300
+20
View File
@@ -615,6 +615,26 @@ pub(crate) fn default_mask_relay_max_bytes() -> usize {
32 * 1024 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 { pub(crate) fn default_mask_classifier_prefetch_timeout_ms() -> u64 {
5 5
} }
+16 -3
View File
@@ -17,8 +17,9 @@
//! | `network` | `dns_overrides` | Applied immediately | //! | `network` | `dns_overrides` | Applied immediately |
//! | `access` | All user/quota fields | Effective immediately | //! | `access` | All user/quota fields | Effective immediately |
//! //!
//! Fields that require re-binding sockets (`server.port`, `censorship.*`, //! Fields that require re-binding sockets (`server.listeners`, legacy
//! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted. //! `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. //! Non-hot changes are never mixed into the runtime config snapshot.
use std::collections::BTreeSet; use std::collections::BTreeSet;
@@ -299,6 +300,7 @@ fn listeners_equal(
} }
lhs.iter().zip(rhs.iter()).all(|(a, b)| { lhs.iter().zip(rhs.iter()).all(|(a, b)| {
a.ip == b.ip a.ip == b.ip
&& a.port == b.port
&& a.announce == b.announce && a.announce == b.announce
&& a.announce_ip == b.announce_ip && a.announce_ip == b.announce_ip
&& a.proxy_protocol == b.proxy_protocol && a.proxy_protocol == b.proxy_protocol
@@ -306,6 +308,14 @@ 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)] #[derive(Debug, Clone, Default, PartialEq, Eq)]
struct WatchManifest { struct WatchManifest {
files: BTreeSet<PathBuf>, files: BTreeSet<PathBuf>,
@@ -560,6 +570,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
if old.server.api.enabled != new.server.api.enabled if old.server.api.enabled != new.server.api.enabled
|| old.server.api.listen != new.server.api.listen || old.server.api.listen != new.server.api.listen
|| old.server.api.whitelist != new.server.api.whitelist || 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.auth_header != new.server.api.auth_header
|| old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes || 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 || old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
@@ -611,6 +622,8 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|| old.censorship.mask_shape_above_cap_blur_max_bytes || old.censorship.mask_shape_above_cap_blur_max_bytes
!= new.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_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 || old.censorship.mask_classifier_prefetch_timeout_ms
!= new.censorship.mask_classifier_prefetch_timeout_ms != new.censorship.mask_classifier_prefetch_timeout_ms
|| old.censorship.mask_timing_normalization_enabled || old.censorship.mask_timing_normalization_enabled
@@ -1117,7 +1130,7 @@ fn log_changes(
.general .general
.links .links
.public_port .public_port
.unwrap_or(new_cfg.server.port); .unwrap_or(resolve_default_link_port(new_cfg));
for user in &added { for user in &added {
if let Some(secret) = new_hot.users.get(*user) { if let Some(secret) = new_hot.users.get(*user) {
print_user_links(user, secret, &host, port, new_cfg); print_user_links(user, secret, &host, port, new_cfg);
+208
View File
@@ -253,6 +253,12 @@ fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
} }
for upstream in &config.upstreams { 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 { if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type {
let parsed = ShadowsocksServerConfig::from_url(url) let parsed = ShadowsocksServerConfig::from_url(url)
.map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?; .map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?;
@@ -340,12 +346,29 @@ impl ProxyConfig {
let update_every_is_explicit = general_table let update_every_is_explicit = general_table
.map(|table| table.contains_key("update_every")) .map(|table| table.contains_key("update_every"))
.unwrap_or(false); .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 let legacy_secret_is_explicit = general_table
.map(|table| table.contains_key("proxy_secret_auto_reload_secs")) .map(|table| table.contains_key("proxy_secret_auto_reload_secs"))
.unwrap_or(false); .unwrap_or(false);
let legacy_config_is_explicit = general_table let legacy_config_is_explicit = general_table
.map(|table| table.contains_key("proxy_config_auto_reload_secs")) .map(|table| table.contains_key("proxy_config_auto_reload_secs"))
.unwrap_or(false); .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 let stun_servers_is_explicit = network_table
.map(|table| table.contains_key("stun_servers")) .map(|table| table.contains_key("stun_servers"))
.unwrap_or(false); .unwrap_or(false);
@@ -358,6 +381,59 @@ impl ProxyConfig {
config.general.update_every = None; 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 = config.general.middle_proxy_nat_stun.take();
let legacy_nat_stun_servers = let legacy_nat_stun_servers =
std::mem::take(&mut config.general.middle_proxy_nat_stun_servers); std::mem::take(&mut config.general.middle_proxy_nat_stun_servers);
@@ -1250,6 +1326,7 @@ impl ProxyConfig {
if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() { if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() {
config.server.listeners.push(ListenerConfig { config.server.listeners.push(ListenerConfig {
ip: ipv4, ip: ipv4,
port: Some(config.server.port),
announce: None, announce: None,
announce_ip: None, announce_ip: None,
proxy_protocol: None, proxy_protocol: None,
@@ -1261,6 +1338,7 @@ impl ProxyConfig {
{ {
config.server.listeners.push(ListenerConfig { config.server.listeners.push(ListenerConfig {
ip: ipv6, ip: ipv6,
port: Some(config.server.port),
announce: None, announce: None,
announce_ip: None, announce_ip: None,
proxy_protocol: None, proxy_protocol: None,
@@ -1269,6 +1347,13 @@ 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. // Migration: announce_ip → announce for each listener.
for listener in &mut config.server.listeners { for listener in &mut config.server.listeners {
if listener.announce.is_none() if listener.announce.is_none()
@@ -1289,11 +1374,14 @@ impl ProxyConfig {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}); });
} }
@@ -1385,6 +1473,21 @@ mod tests {
const TEST_SHADOWSOCKS_URL: &str = const TEST_SHADOWSOCKS_URL: &str =
"ss://2022-blake3-aes-256-gcm:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=@127.0.0.1:8388"; "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] #[test]
fn serde_defaults_remain_unchanged_for_present_sections() { fn serde_defaults_remain_unchanged_for_present_sections() {
let toml = r#" let toml = r#"
@@ -1481,6 +1584,7 @@ mod tests {
cfg.general.rpc_proxy_req_every, cfg.general.rpc_proxy_req_every,
default_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.general.update_every, default_update_every());
assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4()); assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4());
assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt()); assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt());
@@ -1491,6 +1595,7 @@ mod tests {
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop); assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
assert_eq!(cfg.server.api.listen, default_api_listen()); assert_eq!(cfg.server.api.listen, default_api_listen());
assert_eq!(cfg.server.api.whitelist, default_api_whitelist()); assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
assert_eq!(cfg.server.api.gray_action, ApiGrayAction::Drop);
assert_eq!( assert_eq!(
cfg.server.api.request_body_limit_bytes, cfg.server.api.request_body_limit_bytes,
default_api_request_body_limit_bytes() default_api_request_body_limit_bytes()
@@ -1647,6 +1752,7 @@ mod tests {
default_upstream_connect_failfast_hard_errors() default_upstream_connect_failfast_hard_errors()
); );
assert_eq!(general.rpc_proxy_req_every, default_rpc_proxy_req_every()); 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()); assert_eq!(general.update_every, default_update_every());
let server = ServerConfig::default(); let server = ServerConfig::default();
@@ -1661,6 +1767,7 @@ mod tests {
); );
assert_eq!(server.api.listen, default_api_listen()); assert_eq!(server.api.listen, default_api_listen());
assert_eq!(server.api.whitelist, default_api_whitelist()); assert_eq!(server.api.whitelist, default_api_whitelist());
assert_eq!(server.api.gray_action, ApiGrayAction::Drop);
assert_eq!( assert_eq!(
server.api.request_body_limit_bytes, server.api.request_body_limit_bytes,
default_api_request_body_limit_bytes() default_api_request_body_limit_bytes()
@@ -1808,6 +1915,107 @@ mod tests {
); );
} }
#[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] #[test]
fn dc_overrides_allow_string_and_array() { fn dc_overrides_allow_string_and_array() {
let toml = r#" let toml = r#"
+79 -1
View File
@@ -159,6 +159,21 @@ 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. /// Middle-End writer floor policy mode.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
@@ -925,6 +940,14 @@ pub struct GeneralConfig {
/// Minimum unavailable ME DC groups before degrading. /// Minimum unavailable ME DC groups before degrading.
#[serde(default = "default_degradation_min_unavailable_dc_groups")] #[serde(default = "default_degradation_min_unavailable_dc_groups")]
pub degradation_min_unavailable_dc_groups: u8, 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 { impl Default for GeneralConfig {
@@ -1086,6 +1109,7 @@ impl Default for GeneralConfig {
ntp_servers: default_ntp_servers(), ntp_servers: default_ntp_servers(),
auto_degradation_enabled: default_true(), auto_degradation_enabled: default_true(),
degradation_min_unavailable_dc_groups: default_degradation_min_unavailable_dc_groups(), degradation_min_unavailable_dc_groups: default_degradation_min_unavailable_dc_groups(),
rst_on_close: RstOnCloseMode::default(),
} }
} }
} }
@@ -1129,7 +1153,8 @@ pub struct LinksConfig {
#[serde(default)] #[serde(default)]
pub public_host: Option<String>, pub public_host: Option<String>,
/// Public port for tg:// link generation (overrides server.port). /// Public port for tg:// link generation.
/// Overrides listener ports and legacy `server.port`.
#[serde(default)] #[serde(default)]
pub public_port: Option<u16>, pub public_port: Option<u16>,
} }
@@ -1159,6 +1184,13 @@ pub struct ApiConfig {
#[serde(default = "default_api_whitelist")] #[serde(default = "default_api_whitelist")]
pub whitelist: Vec<IpNetwork>, 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. /// Optional static value for `Authorization` header validation.
/// Empty string disables header auth. /// Empty string disables header auth.
#[serde(default)] #[serde(default)]
@@ -1203,6 +1235,7 @@ impl Default for ApiConfig {
enabled: default_true(), enabled: default_true(),
listen: default_api_listen(), listen: default_api_listen(),
whitelist: default_api_whitelist(), whitelist: default_api_whitelist(),
gray_action: ApiGrayAction::default(),
auth_header: String::new(), auth_header: String::new(),
request_body_limit_bytes: default_api_request_body_limit_bytes(), request_body_limit_bytes: default_api_request_body_limit_bytes(),
minimal_runtime_enabled: default_api_minimal_runtime_enabled(), minimal_runtime_enabled: default_api_minimal_runtime_enabled(),
@@ -1216,6 +1249,19 @@ 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)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ConntrackMode { pub enum ConntrackMode {
@@ -1330,6 +1376,8 @@ impl Default for ConntrackControlConfig {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig { pub struct ServerConfig {
/// Legacy listener port used for backward compatibility.
/// For new configs prefer `[[server.listeners]].port`.
#[serde(default = "default_port")] #[serde(default = "default_port")]
pub port: u16, pub port: u16,
@@ -1686,6 +1734,19 @@ pub struct AntiCensorshipConfig {
#[serde(default = "default_mask_relay_max_bytes")] #[serde(default = "default_mask_relay_max_bytes")]
pub mask_relay_max_bytes: usize, 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. /// Prefetch timeout (ms) for extending fragmented masking classifier window.
#[serde(default = "default_mask_classifier_prefetch_timeout_ms")] #[serde(default = "default_mask_classifier_prefetch_timeout_ms")]
pub mask_classifier_prefetch_timeout_ms: u64, pub mask_classifier_prefetch_timeout_ms: u64,
@@ -1731,6 +1792,8 @@ impl Default for AntiCensorshipConfig {
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(), 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_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_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_classifier_prefetch_timeout_ms: default_mask_classifier_prefetch_timeout_ms(),
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(), mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(), mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
@@ -1817,6 +1880,10 @@ pub enum UpstreamType {
interface: Option<String>, interface: Option<String>,
#[serde(default)] #[serde(default)]
bind_addresses: Option<Vec<String>>, 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 { Socks4 {
address: String, address: String,
@@ -1853,11 +1920,22 @@ pub struct UpstreamConfig {
pub scopes: String, pub scopes: String,
#[serde(skip)] #[serde(skip)]
pub selected_scope: String, 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListenerConfig { pub struct ListenerConfig {
pub ip: IpAddr, 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. /// IP address or hostname to announce in proxy links.
/// Takes precedence over `announce_ip` if both are set. /// Takes precedence over `announce_ip` if both are set.
#[serde(default)] #[serde(default)]
+40 -21
View File
@@ -343,15 +343,28 @@ fn command_exists(binary: &str) -> bool {
}) })
} }
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr>>) { 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)>) {
let mode = cfg.server.conntrack_control.mode; let mode = cfg.server.conntrack_control.mode;
let mut v4_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new(); let mut v4_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
let mut v6_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new(); let mut v6_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
match mode { match mode {
ConntrackMode::Tracked => {} ConntrackMode::Tracked => {}
ConntrackMode::Notrack => { ConntrackMode::Notrack => {
if cfg.server.listeners.is_empty() { if cfg.server.listeners.is_empty() {
let port = cfg.server.port;
if let Some(ipv4) = cfg if let Some(ipv4) = cfg
.server .server
.listen_addr_ipv4 .listen_addr_ipv4
@@ -359,9 +372,9 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr
.and_then(|s| s.parse::<IpAddr>().ok()) .and_then(|s| s.parse::<IpAddr>().ok())
{ {
if ipv4.is_unspecified() { if ipv4.is_unspecified() {
v4_targets.insert(None); v4_targets.insert((None, port));
} else { } else {
v4_targets.insert(Some(ipv4)); v4_targets.insert((Some(ipv4), port));
} }
} }
if let Some(ipv6) = cfg if let Some(ipv6) = cfg
@@ -371,33 +384,39 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr
.and_then(|s| s.parse::<IpAddr>().ok()) .and_then(|s| s.parse::<IpAddr>().ok())
{ {
if ipv6.is_unspecified() { if ipv6.is_unspecified() {
v6_targets.insert(None); v6_targets.insert((None, port));
} else { } else {
v6_targets.insert(Some(ipv6)); v6_targets.insert((Some(ipv6), port));
} }
} }
} else { } else {
for listener in &cfg.server.listeners { for listener in &cfg.server.listeners {
let port = listener.port.unwrap_or(cfg.server.port);
if listener.ip.is_ipv4() { if listener.ip.is_ipv4() {
if listener.ip.is_unspecified() { if listener.ip.is_unspecified() {
v4_targets.insert(None); v4_targets.insert((None, port));
} else { } else {
v4_targets.insert(Some(listener.ip)); v4_targets.insert((Some(listener.ip), port));
} }
} else if listener.ip.is_unspecified() { } else if listener.ip.is_unspecified() {
v6_targets.insert(None); v6_targets.insert((None, port));
} else { } else {
v6_targets.insert(Some(listener.ip)); v6_targets.insert((Some(listener.ip), port));
} }
} }
} }
} }
ConntrackMode::Hybrid => { ConntrackMode::Hybrid => {
let ports = listener_port_set(cfg);
for ip in &cfg.server.conntrack_control.hybrid_listener_ips { for ip in &cfg.server.conntrack_control.hybrid_listener_ips {
if ip.is_ipv4() { if ip.is_ipv4() {
v4_targets.insert(Some(*ip)); for port in &ports {
v4_targets.insert((Some(*ip), *port));
}
} else { } else {
v6_targets.insert(Some(*ip)); for port in &ports {
v6_targets.insert((Some(*ip), *port));
}
} }
} }
} }
@@ -422,19 +441,19 @@ async fn apply_nft_rules(cfg: &ProxyConfig) -> Result<(), String> {
let (v4_targets, v6_targets) = notrack_targets(cfg); let (v4_targets, v6_targets) = notrack_targets(cfg);
let mut rules = Vec::new(); let mut rules = Vec::new();
for ip in v4_targets { for (ip, port) in v4_targets {
let rule = if let Some(ip) = ip { let rule = if let Some(ip) = ip {
format!("tcp dport {} ip daddr {} notrack", cfg.server.port, ip) format!("tcp dport {} ip daddr {} notrack", port, ip)
} else { } else {
format!("tcp dport {} notrack", cfg.server.port) format!("tcp dport {} notrack", port)
}; };
rules.push(rule); rules.push(rule);
} }
for ip in v6_targets { for (ip, port) in v6_targets {
let rule = if let Some(ip) = ip { let rule = if let Some(ip) = ip {
format!("tcp dport {} ip6 daddr {} notrack", cfg.server.port, ip) format!("tcp dport {} ip6 daddr {} notrack", port, ip)
} else { } else {
format!("tcp dport {} notrack", cfg.server.port) format!("tcp dport {} notrack", port)
}; };
rules.push(rule); rules.push(rule);
} }
@@ -498,7 +517,7 @@ async fn apply_iptables_rules_for_binary(
let (v4_targets, v6_targets) = notrack_targets(cfg); let (v4_targets, v6_targets) = notrack_targets(cfg);
let selected = if ipv4 { v4_targets } else { v6_targets }; let selected = if ipv4 { v4_targets } else { v6_targets };
for ip in selected { for (ip, port) in selected {
let mut args = vec![ let mut args = vec![
"-t".to_string(), "-t".to_string(),
"raw".to_string(), "raw".to_string(),
@@ -507,7 +526,7 @@ async fn apply_iptables_rules_for_binary(
"-p".to_string(), "-p".to_string(),
"tcp".to_string(), "tcp".to_string(),
"--dport".to_string(), "--dport".to_string(),
cfg.server.port.to_string(), port.to_string(),
]; ];
if let Some(ip) = ip { if let Some(ip) = ip {
args.push("-d".to_string()); args.push("-d".to_string());
+32 -9
View File
@@ -9,7 +9,7 @@ use tokio::net::UnixListener;
use tokio::sync::{Semaphore, watch}; use tokio::sync::{Semaphore, watch};
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use crate::config::ProxyConfig; use crate::config::{ProxyConfig, RstOnCloseMode};
use crate::crypto::SecureRandom; use crate::crypto::SecureRandom;
use crate::ip_tracker::UserIpTracker; use crate::ip_tracker::UserIpTracker;
use crate::proxy::ClientHandler; use crate::proxy::ClientHandler;
@@ -21,6 +21,7 @@ use crate::stats::{ReplayChecker, Stats};
use crate::stream::BufferPool; use crate::stream::BufferPool;
use crate::tls_front::TlsFrontCache; use crate::tls_front::TlsFrontCache;
use crate::transport::middle_proxy::MePool; use crate::transport::middle_proxy::MePool;
use crate::transport::socket::set_linger_zero;
use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes}; use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes};
use super::helpers::{is_expected_handshake_eof, print_proxy_links}; use super::helpers::{is_expected_handshake_eof, print_proxy_links};
@@ -30,6 +31,19 @@ pub(crate) struct BoundListeners {
pub(crate) has_unix_listener: bool, 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)] #[allow(clippy::too_many_arguments)]
pub(crate) async fn bind_listeners( pub(crate) async fn bind_listeners(
config: &Arc<ProxyConfig>, config: &Arc<ProxyConfig>,
@@ -62,7 +76,8 @@ pub(crate) async fn bind_listeners(
let mut listeners = Vec::new(); let mut listeners = Vec::new();
for listener_conf in &config.server.listeners { for listener_conf in &config.server.listeners {
let addr = SocketAddr::new(listener_conf.ip, config.server.port); let listener_port = listener_port_or_legacy(listener_conf, config);
let addr = SocketAddr::new(listener_conf.ip, listener_port);
if addr.is_ipv4() && !decision_ipv4_dc { if addr.is_ipv4() && !decision_ipv4_dc {
warn!(%addr, "Skipping IPv4 listener: IPv4 disabled by [network]"); warn!(%addr, "Skipping IPv4 listener: IPv4 disabled by [network]");
continue; continue;
@@ -105,11 +120,7 @@ pub(crate) async fn bind_listeners(
if config.general.links.public_host.is_none() if config.general.links.public_host.is_none()
&& !config.general.links.show.is_empty() && !config.general.links.show.is_empty()
{ {
let link_port = config let link_port = config.general.links.public_port.unwrap_or(listener_port);
.general
.links
.public_port
.unwrap_or(config.server.port);
print_proxy_links(&public_host, link_port, config); print_proxy_links(&public_host, link_port, config);
} }
@@ -157,7 +168,7 @@ pub(crate) async fn bind_listeners(
.general .general
.links .links
.public_port .public_port
.unwrap_or(config.server.port), .unwrap_or(default_link_port(config)),
) )
} else { } else {
let ip = detected_ip_v4.or(detected_ip_v6).map(|ip| ip.to_string()); let ip = detected_ip_v4.or(detected_ip_v6).map(|ip| ip.to_string());
@@ -172,7 +183,7 @@ pub(crate) async fn bind_listeners(
.general .general
.links .links
.public_port .public_port
.unwrap_or(config.server.port), .unwrap_or(default_link_port(config)),
) )
}; };
@@ -380,6 +391,15 @@ pub(crate) fn spawn_tcp_accept_loops(
loop { loop {
match listener.accept().await { match listener.accept().await {
Ok((stream, peer_addr)) => { 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() { if !*admission_rx_tcp.borrow() {
debug!(peer = %peer_addr, "Admission gate closed, dropping connection"); debug!(peer = %peer_addr, "Admission gate closed, dropping connection");
drop(stream); drop(stream);
@@ -454,6 +474,9 @@ pub(crate) fn spawn_tcp_accept_loops(
shared, shared,
proxy_protocol_enabled, proxy_protocol_enabled,
real_peer_report_for_handler, real_peer_report_for_handler,
#[cfg(unix)]
raw_fd,
rst_mode,
) )
.run() .run()
.await .await
+42 -27
View File
@@ -81,23 +81,11 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
} }
} }
#[cfg(unix)] // Shared maestro startup and main loop. `drop_after_bind` runs on Unix after listeners are bound
async fn run_inner( // (for privilege drop); it is a no-op on other platforms.
daemon_opts: DaemonOptions, async fn run_telemt_core(
drop_after_bind: impl FnOnce(),
) -> std::result::Result<(), Box<dyn std::error::Error>> { ) -> 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 = Instant::now();
let process_started_at_epoch_secs = SystemTime::now() let process_started_at_epoch_secs = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@@ -761,17 +749,8 @@ async fn run_inner(
std::process::exit(1); std::process::exit(1);
} }
// Drop privileges after binding sockets (which may require root for port < 1024) // On Unix, caller supplies privilege drop after bind (may require root for port < 1024).
if daemon_opts.user.is_some() || daemon_opts.group.is_some() { drop_after_bind();
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( runtime_tasks::apply_runtime_log_filter(
has_rust_log, has_rust_log,
@@ -819,3 +798,39 @@ async fn run_inner(
Ok(()) 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
}
+1
View File
@@ -97,6 +97,7 @@ pub async fn run_probe(
let UpstreamType::Direct { let UpstreamType::Direct {
interface, interface,
bind_addresses, bind_addresses,
..
} = &upstream.upstream_type } = &upstream.upstream_type
else { else {
continue; continue;
+28 -1
View File
@@ -804,6 +804,9 @@ pub struct RunningClientHandler {
beobachten: Arc<BeobachtenStore>, beobachten: Arc<BeobachtenStore>,
shared: Arc<ProxySharedState>, shared: Arc<ProxySharedState>,
proxy_protocol_enabled: bool, proxy_protocol_enabled: bool,
#[cfg(unix)]
raw_fd: std::os::unix::io::RawFd,
rst_on_close: crate::config::RstOnCloseMode,
} }
impl ClientHandler { impl ClientHandler {
@@ -825,6 +828,11 @@ impl ClientHandler {
proxy_protocol_enabled: bool, proxy_protocol_enabled: bool,
real_peer_report: Arc<std::sync::Mutex<Option<SocketAddr>>>, real_peer_report: Arc<std::sync::Mutex<Option<SocketAddr>>>,
) -> RunningClientHandler { ) -> RunningClientHandler {
#[cfg(unix)]
let raw_fd = {
use std::os::unix::io::AsRawFd;
stream.as_raw_fd()
};
Self::new_with_shared( Self::new_with_shared(
stream, stream,
peer, peer,
@@ -842,6 +850,9 @@ impl ClientHandler {
ProxySharedState::new(), ProxySharedState::new(),
proxy_protocol_enabled, proxy_protocol_enabled,
real_peer_report, real_peer_report,
#[cfg(unix)]
raw_fd,
crate::config::RstOnCloseMode::Off,
) )
} }
@@ -863,6 +874,8 @@ impl ClientHandler {
shared: Arc<ProxySharedState>, shared: Arc<ProxySharedState>,
proxy_protocol_enabled: bool, proxy_protocol_enabled: bool,
real_peer_report: Arc<std::sync::Mutex<Option<SocketAddr>>>, 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 { ) -> RunningClientHandler {
let normalized_peer = normalize_ip(peer); let normalized_peer = normalize_ip(peer);
RunningClientHandler { RunningClientHandler {
@@ -883,6 +896,9 @@ impl ClientHandler {
beobachten, beobachten,
shared, shared,
proxy_protocol_enabled, proxy_protocol_enabled,
#[cfg(unix)]
raw_fd,
rst_on_close,
} }
} }
} }
@@ -901,6 +917,10 @@ impl RunningClientHandler {
debug!(peer = %peer, error = %e, "Failed to configure client socket"); 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? { let outcome = match self.do_handshake().await? {
Some(outcome) => outcome, Some(outcome) => outcome,
None => return Ok(()), None => return Ok(()),
@@ -908,7 +928,14 @@ impl RunningClientHandler {
// Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts) // Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts)
match outcome { match outcome {
HandshakeOutcome::NeedsRelay(fut) | HandshakeOutcome::NeedsMasking(fut) => fut.await, 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,
} }
} }
+63 -23
View File
@@ -28,14 +28,10 @@ use tracing::debug;
const MASK_TIMEOUT: Duration = Duration::from_secs(5); const MASK_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(test)] #[cfg(test)]
const MASK_TIMEOUT: Duration = Duration::from_millis(50); const MASK_TIMEOUT: Duration = Duration::from_millis(50);
/// Maximum duration for the entire masking relay. /// Maximum duration for the entire masking relay under test (replaced by config at runtime).
/// Limits resource consumption from slow-loris attacks and port scanners.
#[cfg(not(test))]
const MASK_RELAY_TIMEOUT: Duration = Duration::from_secs(60);
#[cfg(test)] #[cfg(test)]
const MASK_RELAY_TIMEOUT: Duration = Duration::from_millis(200); const MASK_RELAY_TIMEOUT: Duration = Duration::from_millis(200);
#[cfg(not(test))] /// Per-read idle timeout for masking relay and drain paths under test (replaced by config at runtime).
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
#[cfg(test)] #[cfg(test)]
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100); const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
const MASK_BUFFER_SIZE: usize = 8192; const MASK_BUFFER_SIZE: usize = 8192;
@@ -55,6 +51,7 @@ async fn copy_with_idle_timeout<R, W>(
writer: &mut W, writer: &mut W,
byte_cap: usize, byte_cap: usize,
shutdown_on_eof: bool, shutdown_on_eof: bool,
idle_timeout: Duration,
) -> CopyOutcome ) -> CopyOutcome
where where
R: AsyncRead + Unpin, R: AsyncRead + Unpin,
@@ -78,7 +75,7 @@ where
} }
let read_len = remaining_budget.min(MASK_BUFFER_SIZE); 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 read_res = timeout(idle_timeout, reader.read(&mut buf[..read_len])).await;
let n = match read_res { let n = match read_res {
Ok(Ok(n)) => n, Ok(Ok(n)) => n,
Ok(Err(_)) | Err(_) => break, Ok(Err(_)) | Err(_) => break,
@@ -86,13 +83,13 @@ where
if n == 0 { if n == 0 {
ended_by_eof = true; ended_by_eof = true;
if shutdown_on_eof { if shutdown_on_eof {
let _ = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.shutdown()).await; let _ = timeout(idle_timeout, writer.shutdown()).await;
} }
break; break;
} }
total = total.saturating_add(n); total = total.saturating_add(n);
let write_res = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.write_all(&buf[..n])).await; let write_res = timeout(idle_timeout, writer.write_all(&buf[..n])).await;
match write_res { match write_res {
Ok(Ok(())) => {} Ok(Ok(())) => {}
Ok(Err(_)) | Err(_) => break, Ok(Err(_)) | Err(_) => break,
@@ -230,13 +227,20 @@ where
} }
} }
async fn consume_client_data_with_timeout_and_cap<R>(reader: R, byte_cap: usize) async fn consume_client_data_with_timeout_and_cap<R>(
where reader: R,
byte_cap: usize,
relay_timeout: Duration,
idle_timeout: Duration,
) where
R: AsyncRead + Unpin, R: AsyncRead + Unpin,
{ {
if timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, byte_cap)) if timeout(
.await relay_timeout,
.is_err() consume_client_data(reader, byte_cap, idle_timeout),
)
.await
.is_err()
{ {
debug!("Timed out while consuming client data on masking fallback path"); debug!("Timed out while consuming client data on masking fallback path");
} }
@@ -639,10 +643,18 @@ pub async fn handle_bad_client<R, W>(
beobachten.record(client_type, peer.ip(), ttl); 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 { if !config.censorship.mask {
// Masking disabled, just consume data // Masking disabled, just consume data
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes) consume_client_data_with_timeout_and_cap(
.await; reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
return; return;
} }
@@ -674,7 +686,7 @@ pub async fn handle_bad_client<R, W>(
return; return;
} }
if timeout( if timeout(
MASK_RELAY_TIMEOUT, relay_timeout,
relay_to_mask( relay_to_mask(
reader, reader,
writer, writer,
@@ -688,6 +700,7 @@ pub async fn handle_bad_client<R, W>(
config.censorship.mask_shape_above_cap_blur_max_bytes, config.censorship.mask_shape_above_cap_blur_max_bytes,
config.censorship.mask_shape_hardening_aggressive_mode, config.censorship.mask_shape_hardening_aggressive_mode,
config.censorship.mask_relay_max_bytes, config.censorship.mask_relay_max_bytes,
idle_timeout,
), ),
) )
.await .await
@@ -703,6 +716,8 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap( consume_client_data_with_timeout_and_cap(
reader, reader,
config.censorship.mask_relay_max_bytes, config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
) )
.await; .await;
wait_mask_outcome_budget(outcome_started, config).await; wait_mask_outcome_budget(outcome_started, config).await;
@@ -712,6 +727,8 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap( consume_client_data_with_timeout_and_cap(
reader, reader,
config.censorship.mask_relay_max_bytes, config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
) )
.await; .await;
wait_mask_outcome_budget(outcome_started, config).await; wait_mask_outcome_budget(outcome_started, config).await;
@@ -742,8 +759,13 @@ pub async fn handle_bad_client<R, W>(
local = %local_addr, local = %local_addr,
"Mask target resolves to local listener; refusing self-referential masking fallback" "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) consume_client_data_with_timeout_and_cap(
.await; reader,
config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
)
.await;
wait_mask_outcome_budget(outcome_started, config).await; wait_mask_outcome_budget(outcome_started, config).await;
return; return;
} }
@@ -777,7 +799,7 @@ pub async fn handle_bad_client<R, W>(
return; return;
} }
if timeout( if timeout(
MASK_RELAY_TIMEOUT, relay_timeout,
relay_to_mask( relay_to_mask(
reader, reader,
writer, writer,
@@ -791,6 +813,7 @@ pub async fn handle_bad_client<R, W>(
config.censorship.mask_shape_above_cap_blur_max_bytes, config.censorship.mask_shape_above_cap_blur_max_bytes,
config.censorship.mask_shape_hardening_aggressive_mode, config.censorship.mask_shape_hardening_aggressive_mode,
config.censorship.mask_relay_max_bytes, config.censorship.mask_relay_max_bytes,
idle_timeout,
), ),
) )
.await .await
@@ -806,6 +829,8 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap( consume_client_data_with_timeout_and_cap(
reader, reader,
config.censorship.mask_relay_max_bytes, config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
) )
.await; .await;
wait_mask_outcome_budget(outcome_started, config).await; wait_mask_outcome_budget(outcome_started, config).await;
@@ -815,6 +840,8 @@ pub async fn handle_bad_client<R, W>(
consume_client_data_with_timeout_and_cap( consume_client_data_with_timeout_and_cap(
reader, reader,
config.censorship.mask_relay_max_bytes, config.censorship.mask_relay_max_bytes,
relay_timeout,
idle_timeout,
) )
.await; .await;
wait_mask_outcome_budget(outcome_started, config).await; wait_mask_outcome_budget(outcome_started, config).await;
@@ -836,6 +863,7 @@ async fn relay_to_mask<R, W, MR, MW>(
shape_above_cap_blur_max_bytes: usize, shape_above_cap_blur_max_bytes: usize,
shape_hardening_aggressive_mode: bool, shape_hardening_aggressive_mode: bool,
mask_relay_max_bytes: usize, mask_relay_max_bytes: usize,
idle_timeout: Duration,
) where ) where
R: AsyncRead + Unpin + Send + 'static, R: AsyncRead + Unpin + Send + 'static,
W: AsyncWrite + Unpin + Send + 'static, W: AsyncWrite + Unpin + Send + 'static,
@@ -857,11 +885,19 @@ async fn relay_to_mask<R, W, MR, MW>(
&mut mask_write, &mut mask_write,
mask_relay_max_bytes, mask_relay_max_bytes,
!shape_hardening_enabled, !shape_hardening_enabled,
idle_timeout,
) )
.await .await
}, },
async { async {
copy_with_idle_timeout(&mut mask_read, &mut writer, mask_relay_max_bytes, true).await copy_with_idle_timeout(
&mut mask_read,
&mut writer,
mask_relay_max_bytes,
true,
idle_timeout,
)
.await
} }
); );
@@ -889,7 +925,11 @@ async fn relay_to_mask<R, W, MR, MW>(
} }
/// Just consume all data from client without responding. /// Just consume all data from client without responding.
async fn consume_client_data<R: AsyncRead + Unpin>(mut reader: R, byte_cap: usize) { async fn consume_client_data<R: AsyncRead + Unpin>(
mut reader: R,
byte_cap: usize,
idle_timeout: Duration,
) {
if byte_cap == 0 { if byte_cap == 0 {
return; return;
} }
@@ -905,7 +945,7 @@ async fn consume_client_data<R: AsyncRead + Unpin>(mut reader: R, byte_cap: usiz
} }
let read_len = remaining_budget.min(MASK_BUFFER_SIZE); let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
let n = match timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf[..read_len])).await { let n = match timeout(idle_timeout, reader.read(&mut buf[..read_len])).await {
Ok(Ok(n)) => n, Ok(Ok(n)) => n,
Ok(Err(_)) | Err(_) => break, Ok(Err(_)) | Err(_) => break,
}; };
@@ -31,11 +31,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -27,11 +27,14 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -25,11 +25,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -38,11 +38,14 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -16,11 +16,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -39,11 +39,14 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -229,11 +232,14 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -470,11 +476,14 @@ async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sen
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -544,11 +553,14 @@ async fn capture_forwarded_probe_len(tls_len: u16, body_sent: usize) -> usize {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -13,11 +13,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -25,11 +25,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
+81
View File
@@ -332,11 +332,14 @@ async fn relay_task_abort_releases_user_gate_and_ip_reservation() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -446,11 +449,14 @@ async fn relay_cutover_releases_user_gate_and_ip_reservation() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -570,11 +576,14 @@ async fn integration_route_cutover_and_quota_overlap_fails_closed_and_releases_s
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -740,11 +749,14 @@ async fn proxy_protocol_header_is_rejected_when_trust_list_is_empty() {
upstream_type: crate::config::UpstreamType::Direct { upstream_type: crate::config::UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -817,11 +829,14 @@ async fn proxy_protocol_header_from_untrusted_peer_range_is_rejected_under_load(
upstream_type: crate::config::UpstreamType::Direct { upstream_type: crate::config::UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -977,11 +992,14 @@ async fn short_tls_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1065,11 +1083,14 @@ async fn tls12_record_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1151,11 +1172,14 @@ async fn handle_client_stream_increments_connects_all_exactly_once() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1244,11 +1268,14 @@ async fn running_client_handler_increments_connects_all_exactly_once() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1334,11 +1361,14 @@ async fn idle_pooled_connection_closes_cleanly_in_generic_stream_path() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1405,11 +1435,14 @@ async fn idle_pooled_connection_closes_cleanly_in_client_handler_path() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1491,11 +1524,14 @@ async fn partial_tls_header_stall_triggers_handshake_timeout() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1816,11 +1852,14 @@ async fn valid_tls_path_does_not_fall_back_to_mask_backend() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1925,11 +1964,14 @@ async fn valid_tls_with_invalid_mtproto_falls_back_to_mask_backend() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2032,11 +2074,14 @@ async fn client_handler_tls_bad_mtproto_is_forwarded_to_mask_backend() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2154,11 +2199,14 @@ async fn alpn_mismatch_tls_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2247,11 +2295,14 @@ async fn invalid_hmac_tls_probe_is_masked_through_client_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2346,11 +2397,14 @@ async fn burst_invalid_tls_probes_are_masked_verbatim() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -3251,11 +3305,14 @@ async fn relay_connect_error_releases_user_and_ip_before_return() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -3812,11 +3869,14 @@ async fn untrusted_proxy_header_source_is_rejected() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -3882,11 +3942,14 @@ async fn empty_proxy_trusted_cidrs_rejects_proxy_header_by_default() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -3979,11 +4042,14 @@ async fn oversized_tls_record_is_masked_in_generic_stream_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4082,11 +4148,14 @@ async fn oversized_tls_record_is_masked_in_client_handler_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4199,11 +4268,14 @@ async fn tls_record_len_min_minus_1_is_rejected_in_generic_stream_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4302,11 +4374,14 @@ async fn tls_record_len_min_minus_1_is_rejected_in_client_handler_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4408,11 +4483,14 @@ async fn tls_record_len_16384_is_accepted_in_generic_stream_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4509,11 +4587,14 @@ async fn tls_record_len_16384_is_accepted_in_client_handler_pipeline() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -24,11 +24,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -26,11 +26,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -27,11 +27,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -41,11 +41,14 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1293,11 +1293,14 @@ async fn direct_relay_abort_midflight_releases_route_gauge() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1400,11 +1403,14 @@ async fn direct_relay_cutover_midflight_releases_route_gauge() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1522,11 +1528,14 @@ async fn direct_relay_cutover_storm_multi_session_keeps_generic_errors_and_relea
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1758,8 +1767,11 @@ async fn negative_direct_relay_dc_connection_refused_fails_fast() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
100, 100,
@@ -1849,8 +1861,11 @@ async fn adversarial_direct_relay_cutover_integrity() {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
100, 100,
@@ -47,7 +47,7 @@ async fn consume_client_data_stops_after_byte_cap_without_eof() {
}; };
let cap = 10_000usize; let cap = 10_000usize;
consume_client_data(reader, cap).await; consume_client_data(reader, cap, MASK_RELAY_IDLE_TIMEOUT).await;
let total = produced.load(Ordering::Relaxed); let total = produced.load(Ordering::Relaxed);
assert!( assert!(
@@ -31,7 +31,7 @@ async fn stalling_client_terminates_at_idle_not_relay_timeout() {
let result = tokio::time::timeout( let result = tokio::time::timeout(
MASK_RELAY_TIMEOUT, MASK_RELAY_TIMEOUT,
consume_client_data(reader, MASK_BUFFER_SIZE * 4), consume_client_data(reader, MASK_BUFFER_SIZE * 4, MASK_RELAY_IDLE_TIMEOUT),
) )
.await; .await;
@@ -57,9 +57,12 @@ async fn fast_reader_drains_to_eof() {
let data = vec![0xAAu8; 32 * 1024]; let data = vec![0xAAu8; 32 * 1024];
let reader = std::io::Cursor::new(data); let reader = std::io::Cursor::new(data);
tokio::time::timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, usize::MAX)) tokio::time::timeout(
.await MASK_RELAY_TIMEOUT,
.expect("consume_client_data did not complete for fast EOF reader"); consume_client_data(reader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
)
.await
.expect("consume_client_data did not complete for fast EOF reader");
} }
#[tokio::test] #[tokio::test]
@@ -81,7 +84,7 @@ async fn io_error_terminates_cleanly() {
tokio::time::timeout( tokio::time::timeout(
MASK_RELAY_TIMEOUT, MASK_RELAY_TIMEOUT,
consume_client_data(ErrReader, usize::MAX), consume_client_data(ErrReader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
) )
.await .await
.expect("consume_client_data did not return on I/O error"); .expect("consume_client_data did not return on I/O error");
@@ -34,7 +34,11 @@ async fn consume_stall_stress_finishes_within_idle_budget() {
set.spawn(async { set.spawn(async {
tokio::time::timeout( tokio::time::timeout(
MASK_RELAY_TIMEOUT, MASK_RELAY_TIMEOUT,
consume_client_data(OneByteThenStall { sent: false }, usize::MAX), consume_client_data(
OneByteThenStall { sent: false },
usize::MAX,
MASK_RELAY_IDLE_TIMEOUT,
),
) )
.await .await
.expect("consume_client_data exceeded relay timeout under stall load"); .expect("consume_client_data exceeded relay timeout under stall load");
@@ -56,7 +60,7 @@ async fn consume_stall_stress_finishes_within_idle_budget() {
#[tokio::test] #[tokio::test]
async fn consume_zero_cap_returns_immediately() { async fn consume_zero_cap_returns_immediately() {
let started = Instant::now(); let started = Instant::now();
consume_client_data(tokio::io::empty(), 0).await; consume_client_data(tokio::io::empty(), 0, MASK_RELAY_IDLE_TIMEOUT).await;
assert!( assert!(
started.elapsed() < MASK_RELAY_IDLE_TIMEOUT, started.elapsed() < MASK_RELAY_IDLE_TIMEOUT,
"zero byte cap must return immediately" "zero byte cap must return immediately"
@@ -127,7 +127,14 @@ 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 reader = FinitePatternReader::new(PROD_CAP_BYTES + (256 * 1024), 4096, read_calls);
let mut writer = CountingWriter::default(); let mut writer = CountingWriter::default();
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await; let outcome = copy_with_idle_timeout(
&mut reader,
&mut writer,
PROD_CAP_BYTES,
true,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
assert_eq!( assert_eq!(
outcome.total, PROD_CAP_BYTES, outcome.total, PROD_CAP_BYTES,
@@ -145,7 +152,13 @@ async fn negative_consume_with_zero_cap_performs_no_reads() {
let read_calls = Arc::new(AtomicUsize::new(0)); let read_calls = Arc::new(AtomicUsize::new(0));
let reader = FinitePatternReader::new(1024, 64, Arc::clone(&read_calls)); let reader = FinitePatternReader::new(1024, 64, Arc::clone(&read_calls));
consume_client_data_with_timeout_and_cap(reader, 0).await; consume_client_data_with_timeout_and_cap(
reader,
0,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
assert_eq!( assert_eq!(
read_calls.load(Ordering::Relaxed), read_calls.load(Ordering::Relaxed),
@@ -161,7 +174,14 @@ async fn edge_copy_below_cap_reports_eof_without_overread() {
let mut reader = FinitePatternReader::new(payload, 3072, read_calls); let mut reader = FinitePatternReader::new(payload, 3072, read_calls);
let mut writer = CountingWriter::default(); let mut writer = CountingWriter::default();
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await; let outcome = copy_with_idle_timeout(
&mut reader,
&mut writer,
PROD_CAP_BYTES,
true,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
assert_eq!(outcome.total, payload); assert_eq!(outcome.total, payload);
assert_eq!(writer.written, payload); assert_eq!(writer.written, payload);
@@ -175,7 +195,13 @@ async fn edge_copy_below_cap_reports_eof_without_overread() {
async fn adversarial_blackhat_never_ready_reader_is_bounded_by_timeout_guards() { async fn adversarial_blackhat_never_ready_reader_is_bounded_by_timeout_guards() {
let started = Instant::now(); let started = Instant::now();
consume_client_data_with_timeout_and_cap(NeverReadyReader, PROD_CAP_BYTES).await; consume_client_data_with_timeout_and_cap(
NeverReadyReader,
PROD_CAP_BYTES,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
assert!( assert!(
started.elapsed() < Duration::from_millis(350), started.elapsed() < Duration::from_millis(350),
@@ -190,7 +216,12 @@ async fn integration_consume_path_honors_production_cap_for_large_payload() {
let bounded = timeout( let bounded = timeout(
Duration::from_millis(350), Duration::from_millis(350),
consume_client_data_with_timeout_and_cap(reader, PROD_CAP_BYTES), consume_client_data_with_timeout_and_cap(
reader,
PROD_CAP_BYTES,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
),
) )
.await; .await;
@@ -206,7 +237,13 @@ async fn adversarial_consume_path_never_reads_beyond_declared_byte_cap() {
let total_read = Arc::new(AtomicUsize::new(0)); let total_read = Arc::new(AtomicUsize::new(0));
let reader = BudgetProbeReader::new(256 * 1024, Arc::clone(&total_read)); let reader = BudgetProbeReader::new(256 * 1024, Arc::clone(&total_read));
consume_client_data_with_timeout_and_cap(reader, byte_cap).await; consume_client_data_with_timeout_and_cap(
reader,
byte_cap,
MASK_RELAY_TIMEOUT,
MASK_RELAY_IDLE_TIMEOUT,
)
.await;
assert!( assert!(
total_read.load(Ordering::Relaxed) <= byte_cap, total_read.load(Ordering::Relaxed) <= byte_cap,
@@ -231,7 +268,9 @@ async fn light_fuzz_cap_and_payload_matrix_preserves_min_budget_invariant() {
let mut reader = FinitePatternReader::new(payload, chunk, read_calls); let mut reader = FinitePatternReader::new(payload, chunk, read_calls);
let mut writer = CountingWriter::default(); let mut writer = CountingWriter::default();
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, cap, true).await; let outcome =
copy_with_idle_timeout(&mut reader, &mut writer, cap, true, MASK_RELAY_IDLE_TIMEOUT)
.await;
let expected = payload.min(cap); let expected = payload.min(cap);
assert_eq!( assert_eq!(
@@ -261,7 +300,14 @@ async fn stress_parallel_copy_tasks_with_production_cap_complete_without_leaks()
read_calls, read_calls,
); );
let mut writer = CountingWriter::default(); let mut writer = CountingWriter::default();
copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await copy_with_idle_timeout(
&mut reader,
&mut writer,
PROD_CAP_BYTES,
true,
MASK_RELAY_IDLE_TIMEOUT,
)
.await
})); }));
} }
@@ -26,6 +26,7 @@ async fn relay_to_mask_enforces_masking_session_byte_cap() {
0, 0,
false, false,
32 * 1024, 32 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
) )
.await; .await;
}); });
@@ -81,6 +82,7 @@ async fn relay_to_mask_propagates_client_half_close_without_waiting_for_other_di
0, 0,
false, false,
32 * 1024, 32 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
) )
.await; .await;
}); });
@@ -1377,6 +1377,7 @@ async fn relay_to_mask_keeps_backend_to_client_flow_when_client_to_backend_stall
0, 0,
false, false,
5 * 1024 * 1024, 5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
) )
.await; .await;
}); });
@@ -1508,6 +1509,7 @@ async fn relay_to_mask_timeout_cancels_and_drops_all_io_endpoints() {
0, 0,
false, false,
5 * 1024 * 1024, 5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
), ),
) )
.await; .await;
@@ -228,6 +228,7 @@ async fn relay_path_idle_timeout_eviction_remains_effective() {
0, 0,
false, false,
5 * 1024 * 1024, 5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
) )
.await; .await;
@@ -44,6 +44,7 @@ async fn run_relay_case(
above_cap_blur_max_bytes, above_cap_blur_max_bytes,
false, false,
5 * 1024 * 1024, 5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
) )
.await; .await;
}); });
@@ -89,6 +89,7 @@ async fn relay_to_mask_applies_cap_clamped_padding_for_non_power_of_two_cap() {
0, 0,
false, false,
5 * 1024 * 1024, 5 * 1024 * 1024,
MASK_RELAY_IDLE_TIMEOUT,
) )
.await; .await;
}); });
@@ -53,11 +53,14 @@ fn new_client_harness() -> ClientHarness {
upstream_type: UpstreamType::Direct { upstream_type: UpstreamType::Direct {
interface: None, interface: None,
bind_addresses: None, bind_addresses: None,
bindtodevice: None,
}, },
weight: 1, weight: 1,
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
+57 -95
View File
@@ -67,10 +67,8 @@ struct FamilyReconnectOutcome {
key: (i32, IpFamily), key: (i32, IpFamily),
dc: i32, dc: i32,
family: IpFamily, family: IpFamily,
alive: usize,
required: usize, required: usize,
endpoint_count: usize, endpoint_count: usize,
restored: usize,
} }
pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_connections: usize) { pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_connections: usize) {
@@ -82,8 +80,6 @@ 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 single_endpoint_outage: HashSet<(i32, IpFamily)> = HashSet::new();
let mut shadow_rotate_deadline: HashMap<(i32, IpFamily), Instant> = HashMap::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 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 floor_warn_next_allowed: HashMap<(i32, IpFamily), Instant> = HashMap::new();
let mut drain_warn_next_allowed: HashMap<u64, Instant> = HashMap::new(); let mut drain_warn_next_allowed: HashMap<u64, Instant> = HashMap::new();
let mut degraded_interval = true; let mut degraded_interval = true;
@@ -109,8 +105,6 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
&mut single_endpoint_outage, &mut single_endpoint_outage,
&mut shadow_rotate_deadline, &mut shadow_rotate_deadline,
&mut idle_refresh_next_attempt, &mut idle_refresh_next_attempt,
&mut adaptive_idle_since,
&mut adaptive_recover_until,
&mut floor_warn_next_allowed, &mut floor_warn_next_allowed,
) )
.await; .await;
@@ -126,8 +120,6 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
&mut single_endpoint_outage, &mut single_endpoint_outage,
&mut shadow_rotate_deadline, &mut shadow_rotate_deadline,
&mut idle_refresh_next_attempt, &mut idle_refresh_next_attempt,
&mut adaptive_idle_since,
&mut adaptive_recover_until,
&mut floor_warn_next_allowed, &mut floor_warn_next_allowed,
) )
.await; .await;
@@ -360,8 +352,6 @@ async fn check_family(
single_endpoint_outage: &mut HashSet<(i32, IpFamily)>, single_endpoint_outage: &mut HashSet<(i32, IpFamily)>,
shadow_rotate_deadline: &mut HashMap<(i32, IpFamily), Instant>, shadow_rotate_deadline: &mut HashMap<(i32, IpFamily), Instant>,
idle_refresh_next_attempt: &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>, floor_warn_next_allowed: &mut HashMap<(i32, IpFamily), Instant>,
) -> bool { ) -> bool {
let enabled = match family { let enabled = match family {
@@ -393,10 +383,7 @@ async fn check_family(
let reconnect_budget = health_reconnect_budget(pool, dc_endpoints.len()); let reconnect_budget = health_reconnect_budget(pool, dc_endpoints.len());
let reconnect_sem = Arc::new(Semaphore::new(reconnect_budget)); 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_addr_counts = HashMap::<(i32, SocketAddr), usize>::new();
let mut live_writer_ids_by_addr = HashMap::<(i32, SocketAddr), Vec<u64>>::new(); let mut live_writer_ids_by_addr = HashMap::<(i32, SocketAddr), Vec<u64>>::new();
@@ -435,8 +422,6 @@ async fn check_family(
&live_addr_counts, &live_addr_counts,
&live_writer_ids_by_addr, &live_writer_ids_by_addr,
&bound_clients_by_writer, &bound_clients_by_writer,
adaptive_idle_since,
adaptive_recover_until,
) )
.await; .await;
pool.set_adaptive_floor_runtime_caps( pool.set_adaptive_floor_runtime_caps(
@@ -503,8 +488,6 @@ async fn check_family(
outage_next_attempt.remove(&key); outage_next_attempt.remove(&key);
shadow_rotate_deadline.remove(&key); shadow_rotate_deadline.remove(&key);
idle_refresh_next_attempt.remove(&key); idle_refresh_next_attempt.remove(&key);
adaptive_idle_since.remove(&key);
adaptive_recover_until.remove(&key);
info!( info!(
dc = %dc, dc = %dc,
?family, ?family,
@@ -632,22 +615,28 @@ async fn check_family(
restored += 1; restored += 1;
continue; continue;
} }
pool_for_reconnect
.stats let base_req = pool_for_reconnect
.increment_me_floor_cap_block_total(); .required_writers_for_dc_with_floor_mode(endpoints_for_dc.len(), false);
pool_for_reconnect if alive + restored >= base_req {
.stats pool_for_reconnect
.increment_me_floor_swap_idle_failed_total(); .stats
debug!( .increment_me_floor_cap_block_total();
dc = %dc, pool_for_reconnect
?family, .stats
alive, .increment_me_floor_swap_idle_failed_total();
required, debug!(
active_cap_effective_total, dc = %dc,
"Adaptive floor cap reached, reconnect attempt blocked" ?family,
); alive,
break; 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( let res = tokio::time::timeout(
pool_for_reconnect.reconnect_runtime.me_one_timeout, pool_for_reconnect.reconnect_runtime.me_one_timeout,
pool_for_reconnect.connect_endpoints_round_robin( pool_for_reconnect.connect_endpoints_round_robin(
@@ -663,11 +652,9 @@ async fn check_family(
pool_for_reconnect.stats.increment_me_reconnect_success(); pool_for_reconnect.stats.increment_me_reconnect_success();
} }
Ok(false) => { Ok(false) => {
pool_for_reconnect.stats.increment_me_reconnect_attempt();
debug!(dc = %dc, ?family, "ME round-robin reconnect failed") debug!(dc = %dc, ?family, "ME round-robin reconnect failed")
} }
Err(_) => { Err(_) => {
pool_for_reconnect.stats.increment_me_reconnect_attempt();
debug!(dc = %dc, ?family, "ME reconnect timed out"); debug!(dc = %dc, ?family, "ME reconnect timed out");
} }
} }
@@ -678,10 +665,8 @@ async fn check_family(
key, key,
dc, dc,
family, family,
alive,
required, required,
endpoint_count: endpoints_for_dc.len(), endpoint_count: endpoints_for_dc.len(),
restored,
} }
}); });
} }
@@ -695,7 +680,7 @@ async fn check_family(
} }
}; };
let now = Instant::now(); let now = Instant::now();
let now_alive = outcome.alive + outcome.restored; let now_alive = live_active_writers_for_dc_family(pool, outcome.dc, outcome.family).await;
if now_alive >= outcome.required { if now_alive >= outcome.required {
info!( info!(
dc = %outcome.dc, dc = %outcome.dc,
@@ -851,6 +836,33 @@ fn should_emit_rate_limited_warn(
false 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( fn adaptive_floor_class_min(
pool: &Arc<MePool>, pool: &Arc<MePool>,
endpoint_count: usize, endpoint_count: usize,
@@ -904,8 +916,6 @@ async fn build_family_floor_plan(
live_addr_counts: &HashMap<(i32, SocketAddr), usize>, live_addr_counts: &HashMap<(i32, SocketAddr), usize>,
live_writer_ids_by_addr: &HashMap<(i32, SocketAddr), Vec<u64>>, live_writer_ids_by_addr: &HashMap<(i32, SocketAddr), Vec<u64>>,
bound_clients_by_writer: &HashMap<u64, usize>, bound_clients_by_writer: &HashMap<u64, usize>,
adaptive_idle_since: &mut HashMap<(i32, IpFamily), Instant>,
adaptive_recover_until: &mut HashMap<(i32, IpFamily), Instant>,
) -> FamilyFloorPlan { ) -> FamilyFloorPlan {
let mut entries = Vec::<DcFloorPlanEntry>::new(); let mut entries = Vec::<DcFloorPlanEntry>::new();
let mut by_dc = HashMap::<i32, DcFloorPlanEntry>::new(); let mut by_dc = HashMap::<i32, DcFloorPlanEntry>::new();
@@ -921,18 +931,7 @@ async fn build_family_floor_plan(
if endpoints.is_empty() { if endpoints.is_empty() {
continue; 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 base_required = pool.required_writers_for_dc(endpoints.len()).max(1);
let min_required = if is_adaptive { let min_required = if is_adaptive {
adaptive_floor_class_min(pool, endpoints.len(), base_required) adaptive_floor_class_min(pool, endpoints.len(), base_required)
@@ -947,11 +946,11 @@ async fn build_family_floor_plan(
if max_required < min_required { if max_required < min_required {
max_required = min_required; max_required = min_required;
} }
let desired_raw = if is_adaptive && reduce_for_idle { // We initialize target_required at base_required to prevent 0-writer blackouts
min_required // caused by proactively dropping an idle DC to a single fragile connection.
} else { // The Adaptive Floor constraint loop below will gracefully compress idle DCs
base_required // (prioritized via has_bound_clients = false) to min_required only when global capacity is reached.
}; let desired_raw = base_required;
let target_required = desired_raw.clamp(min_required, max_required); let target_required = desired_raw.clamp(min_required, max_required);
let alive = endpoints let alive = endpoints
.iter() .iter()
@@ -1278,43 +1277,6 @@ 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( fn has_bound_clients_on_endpoint(
writer_ids: &[u64], writer_ids: &[u64],
bound_clients_by_writer: &HashMap<u64, usize>, bound_clients_by_writer: &HashMap<u64, usize>,
@@ -1364,6 +1326,7 @@ async fn recover_single_endpoint_outage(
); );
return; return;
}; };
pool.stats.increment_me_reconnect_attempt();
pool.stats pool.stats
.increment_me_single_endpoint_outage_reconnect_attempt_total(); .increment_me_single_endpoint_outage_reconnect_attempt_total();
@@ -1439,7 +1402,6 @@ async fn recover_single_endpoint_outage(
return; return;
} }
pool.stats.increment_me_reconnect_attempt();
let current_ms = *outage_backoff.get(&key).unwrap_or(&min_backoff_ms); let current_ms = *outage_backoff.get(&key).unwrap_or(&min_backoff_ms);
let next_ms = current_ms.saturating_mul(2).min(max_backoff_ms); let next_ms = current_ms.saturating_mul(2).min(max_backoff_ms);
outage_backoff.insert(key, next_ms); outage_backoff.insert(key, next_ms);
+8 -1
View File
@@ -67,6 +67,7 @@ pub fn format_sample_line(sample: &MePingSample) -> String {
fn format_direct_with_config( fn format_direct_with_config(
interface: &Option<String>, interface: &Option<String>,
bind_addresses: &Option<Vec<String>>, bind_addresses: &Option<Vec<String>>,
bindtodevice: &Option<String>,
) -> Option<String> { ) -> Option<String> {
let mut direct_parts: Vec<String> = Vec::new(); let mut direct_parts: Vec<String> = Vec::new();
if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) { if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) {
@@ -75,6 +76,9 @@ fn format_direct_with_config(
if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) { if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
direct_parts.push(format!("src={}", src.join(","))); 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() { if direct_parts.is_empty() {
None None
} else { } else {
@@ -231,8 +235,11 @@ pub async fn format_me_route(
UpstreamType::Direct { UpstreamType::Direct {
interface, interface,
bind_addresses, bind_addresses,
bindtodevice,
} => { } => {
if let Some(route) = format_direct_with_config(interface, bind_addresses) { if let Some(route) =
format_direct_with_config(interface, bind_addresses, bindtodevice)
{
route route
} else { } else {
detect_direct_route_details(reports, prefer_ipv6, v4_ok, v6_ok) detect_direct_route_details(reports, prefer_ipv6, v4_ok, v6_ok)
+38 -16
View File
@@ -1422,22 +1422,6 @@ impl MePool {
MeFloorMode::from_u8(self.floor_runtime.me_floor_mode.load(Ordering::Relaxed)) 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 { pub(super) fn adaptive_floor_min_writers_multi_endpoint(&self) -> usize {
(self (self
.floor_runtime .floor_runtime
@@ -1659,6 +1643,7 @@ impl MePool {
&self, &self,
contour: WriterContour, contour: WriterContour,
allow_coverage_override: bool, allow_coverage_override: bool,
writer_dc: i32,
) -> bool { ) -> bool {
let (active_writers, warm_writers, _) = self.non_draining_writer_counts_by_contour().await; let (active_writers, warm_writers, _) = self.non_draining_writer_counts_by_contour().await;
match contour { match contour {
@@ -1670,6 +1655,43 @@ impl MePool {
if !allow_coverage_override { if !allow_coverage_override {
return false; 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; let coverage_required = self.active_coverage_required_total().await;
active_writers < coverage_required active_writers < coverage_required
} }
+17 -2
View File
@@ -77,6 +77,12 @@ impl MePool {
return Vec::new(); return Vec::new();
} }
if endpoints.len() == 1 && self.single_endpoint_outage_disable_quarantine() {
let mut guard = self.endpoint_quarantine.lock().await;
guard.retain(|_, expiry| *expiry > Instant::now());
return endpoints.to_vec();
}
let mut guard = self.endpoint_quarantine.lock().await; let mut guard = self.endpoint_quarantine.lock().await;
let now = Instant::now(); let now = Instant::now();
guard.retain(|_, expiry| *expiry > now); guard.retain(|_, expiry| *expiry > now);
@@ -236,8 +242,18 @@ impl MePool {
let fast_retries = self.reconnect_runtime.me_reconnect_fast_retry_count.max(1); let fast_retries = self.reconnect_runtime.me_reconnect_fast_retry_count.max(1);
let mut total_attempts = 0u32; let mut total_attempts = 0u32;
let same_endpoint_quarantined = self.is_endpoint_quarantined(addr).await; let same_endpoint_quarantined = self.is_endpoint_quarantined(addr).await;
let dc_endpoints = self.endpoints_for_dc(writer_dc).await;
let single_endpoint_dc = dc_endpoints.len() == 1 && dc_endpoints[0] == addr;
let bypass_quarantine_for_single_endpoint =
single_endpoint_dc && self.single_endpoint_outage_disable_quarantine();
if !same_endpoint_quarantined { if !same_endpoint_quarantined || bypass_quarantine_for_single_endpoint {
if same_endpoint_quarantined && bypass_quarantine_for_single_endpoint {
debug!(
%addr,
"Bypassing quarantine for immediate reconnect on single-endpoint DC"
);
}
for attempt in 0..fast_retries { for attempt in 0..fast_retries {
if total_attempts >= ME_REFILL_TOTAL_ATTEMPT_CAP { if total_attempts >= ME_REFILL_TOTAL_ATTEMPT_CAP {
break; break;
@@ -276,7 +292,6 @@ impl MePool {
); );
} }
let dc_endpoints = self.endpoints_for_dc(writer_dc).await;
if dc_endpoints.is_empty() { if dc_endpoints.is_empty() {
self.stats.increment_me_refill_failed_total(); self.stats.increment_me_refill_failed_total();
return false; return false;
+1 -1
View File
@@ -342,7 +342,7 @@ impl MePool {
allow_coverage_override: bool, allow_coverage_override: bool,
) -> Result<()> { ) -> Result<()> {
if !self if !self
.can_open_writer_for_contour(contour, allow_coverage_override) .can_open_writer_for_contour(contour, allow_coverage_override, writer_dc)
.await .await
{ {
return Err(ProxyError::Proxy(format!( return Err(ProxyError::Proxy(format!(
@@ -109,18 +109,16 @@ async fn connectable_endpoints_waits_until_quarantine_expires() {
{ {
let mut guard = pool.endpoint_quarantine.lock().await; let mut guard = pool.endpoint_quarantine.lock().await;
guard.insert(addr, Instant::now() + Duration::from_millis(80)); guard.insert(addr, Instant::now() + Duration::from_millis(500));
} }
let started = Instant::now(); let endpoints = tokio::time::timeout(
let endpoints = pool.connectable_endpoints_for_test(&[addr]).await; Duration::from_millis(120),
let elapsed = started.elapsed(); pool.connectable_endpoints_for_test(&[addr]),
)
.await
.expect("single-endpoint outage mode should bypass quarantine delay");
assert_eq!(endpoints, vec![addr]); assert_eq!(endpoints, vec![addr]);
assert!(
elapsed >= Duration::from_millis(50),
"single-endpoint DC should honor quarantine before retry"
);
} }
#[tokio::test] #[tokio::test]
+67 -2
View File
@@ -102,14 +102,29 @@ pub fn configure_client_socket(
Ok(()) Ok(())
} }
/// Set socket to send RST on close (for masking) /// Set socket to send RST on close instead of FIN, eliminating
#[allow(dead_code)] /// FIN-WAIT-1 and orphan socket accumulation on high-churn workloads.
pub fn set_linger_zero(stream: &TcpStream) -> Result<()> { pub fn set_linger_zero(stream: &TcpStream) -> Result<()> {
let socket = socket2::SockRef::from(stream); let socket = socket2::SockRef::from(stream);
socket.set_linger(Some(Duration::ZERO))?; socket.set_linger(Some(Duration::ZERO))?;
Ok(()) Ok(())
} }
/// Restore default linger behaviour (graceful FIN) on a socket
/// identified by its raw file descriptor. Safe to call after
/// `TcpStream::into_split()` because the fd remains valid until
/// both halves are dropped.
#[cfg(unix)]
pub fn clear_linger_fd(fd: std::os::unix::io::RawFd) -> Result<()> {
use std::os::unix::io::BorrowedFd;
// SAFETY: the fd is still open — the caller guarantees the
// TcpStream (or its split halves) is alive.
let borrowed = unsafe { BorrowedFd::borrow_raw(fd) };
let socket = socket2::SockRef::from(&borrowed);
socket.set_linger(None)?;
Ok(())
}
/// Create a new TCP socket for outgoing connections /// Create a new TCP socket for outgoing connections
#[allow(dead_code)] #[allow(dead_code)]
pub fn create_outgoing_socket(addr: SocketAddr) -> Result<Socket> { pub fn create_outgoing_socket(addr: SocketAddr) -> Result<Socket> {
@@ -143,6 +158,56 @@ pub fn create_outgoing_socket_bound(addr: SocketAddr, bind_addr: Option<IpAddr>)
Ok(socket) Ok(socket)
} }
/// Pin an outgoing socket to a specific Linux network interface via SO_BINDTODEVICE.
#[cfg(target_os = "linux")]
pub fn bind_outgoing_socket_to_device(socket: &Socket, device: &str) -> Result<()> {
use std::io::{Error, ErrorKind};
use std::os::fd::AsRawFd;
let name = device.trim();
if name.is_empty() {
return Err(Error::new(
ErrorKind::InvalidInput,
"bindtodevice must not be empty",
));
}
// The kernel expects an interface name buffer with a trailing NUL.
if name.len() >= libc::IFNAMSIZ {
return Err(Error::new(
ErrorKind::InvalidInput,
"bindtodevice exceeds IFNAMSIZ",
));
}
let mut ifname = [0u8; libc::IFNAMSIZ];
ifname[..name.len()].copy_from_slice(name.as_bytes());
let rc = unsafe {
libc::setsockopt(
socket.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_BINDTODEVICE,
ifname.as_ptr().cast::<libc::c_void>(),
(name.len() + 1) as libc::socklen_t,
)
};
if rc != 0 {
return Err(Error::last_os_error());
}
debug!("Pinned outgoing socket to interface {}", name);
Ok(())
}
/// Stub for non-Linux targets where SO_BINDTODEVICE is unavailable.
#[cfg(not(target_os = "linux"))]
pub fn bind_outgoing_socket_to_device(_socket: &Socket, _device: &str) -> Result<()> {
use std::io::{Error, ErrorKind};
Err(Error::new(
ErrorKind::Unsupported,
"bindtodevice is supported only on Linux",
))
}
/// Get local address of a socket /// Get local address of a socket
#[allow(dead_code)] #[allow(dead_code)]
pub fn get_local_addr(stream: &TcpStream) -> Option<SocketAddr> { pub fn get_local_addr(stream: &TcpStream) -> Option<SocketAddr> {
+234 -26
View File
@@ -26,7 +26,9 @@ use crate::stats::Stats;
use crate::transport::shadowsocks::{ use crate::transport::shadowsocks::{
ShadowsocksStream, connect_shadowsocks, sanitize_shadowsocks_url, ShadowsocksStream, connect_shadowsocks, sanitize_shadowsocks_url,
}; };
use crate::transport::socket::{create_outgoing_socket_bound, resolve_interface_ip}; use crate::transport::socket::{
bind_outgoing_socket_to_device, create_outgoing_socket_bound, resolve_interface_ip,
};
use crate::transport::socks::{connect_socks4, connect_socks5}; use crate::transport::socks::{connect_socks4, connect_socks5};
/// Number of Telegram datacenters /// Number of Telegram datacenters
@@ -327,6 +329,17 @@ pub struct UpstreamManager {
} }
impl UpstreamManager { impl UpstreamManager {
fn is_unscoped_upstream(upstream: &UpstreamConfig) -> bool {
upstream.scopes.is_empty()
}
fn should_check_in_default_dc_connectivity(
has_unscoped: bool,
upstream: &UpstreamConfig,
) -> bool {
!has_unscoped || Self::is_unscoped_upstream(upstream)
}
pub fn new( pub fn new(
configs: Vec<UpstreamConfig>, configs: Vec<UpstreamConfig>,
connect_retry_attempts: u32, connect_retry_attempts: u32,
@@ -453,6 +466,87 @@ impl UpstreamManager {
} }
} }
fn resolve_probe_dc_families(
upstream: &UpstreamConfig,
ipv4_available: bool,
ipv6_available: bool,
) -> (bool, bool) {
(
upstream.ipv4.unwrap_or(ipv4_available),
upstream.ipv6.unwrap_or(ipv6_available),
)
}
fn resolve_runtime_dc_families(
upstream: &UpstreamConfig,
dc_preference: IpPreference,
) -> (bool, bool) {
let (auto_ipv4, auto_ipv6) = match dc_preference {
IpPreference::PreferV4 => (true, false),
IpPreference::PreferV6 => (false, true),
IpPreference::BothWork | IpPreference::Unknown | IpPreference::Unavailable => {
(true, true)
}
};
(
upstream.ipv4.unwrap_or(auto_ipv4),
upstream.ipv6.unwrap_or(auto_ipv6),
)
}
fn dc_table_addr(dc_idx: i16, ipv6: bool, port: u16) -> Option<SocketAddr> {
let arr_idx = UpstreamState::dc_array_idx(dc_idx)?;
let ip = if ipv6 {
TG_DATACENTERS_V6[arr_idx]
} else {
TG_DATACENTERS_V4[arr_idx]
};
Some(SocketAddr::new(ip, port))
}
fn resolve_runtime_dc_target(
target: SocketAddr,
dc_idx: Option<i16>,
upstream: &UpstreamConfig,
dc_preference: IpPreference,
) -> Result<SocketAddr> {
let (allow_ipv4, allow_ipv6) = Self::resolve_runtime_dc_families(upstream, dc_preference);
if (target.is_ipv4() && allow_ipv4) || (target.is_ipv6() && allow_ipv6) {
return Ok(target);
}
if !allow_ipv4 && !allow_ipv6 {
return Err(ProxyError::Config(format!(
"Upstream DC family policy blocks all families for target {target}"
)));
}
let Some(dc_idx) = dc_idx else {
return Err(ProxyError::Config(format!(
"Upstream DC family policy cannot remap target {target} without dc_idx"
)));
};
let remapped = if target.is_ipv4() {
if allow_ipv6 {
Self::dc_table_addr(dc_idx, true, target.port())
} else {
None
}
} else if allow_ipv4 {
Self::dc_table_addr(dc_idx, false, target.port())
} else {
None
};
remapped.ok_or_else(|| {
ProxyError::Config(format!(
"Upstream DC family policy rejected target {target} (dc_idx={dc_idx})"
))
})
}
#[cfg(unix)] #[cfg(unix)]
fn resolve_interface_addrs(name: &str, want_ipv6: bool) -> Vec<IpAddr> { fn resolve_interface_addrs(name: &str, want_ipv6: bool) -> Vec<IpAddr> {
use nix::ifaddrs::getifaddrs; use nix::ifaddrs::getifaddrs;
@@ -726,18 +820,28 @@ impl UpstreamManager {
.await .await
.ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?; .ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?;
let mut upstream = { let (mut upstream, bind_rr, dc_preference) = {
let guard = self.upstreams.read().await; let guard = self.upstreams.read().await;
guard[idx].config.clone() let state = &guard[idx];
let dc_preference = dc_idx
.and_then(UpstreamState::dc_array_idx)
.map(|dc_array_idx| state.dc_ip_pref[dc_array_idx])
.unwrap_or(IpPreference::Unknown);
(
state.config.clone(),
Some(state.bind_rr.clone()),
dc_preference,
)
}; };
if let Some(s) = scope { if let Some(s) = scope {
upstream.selected_scope = s.to_string(); upstream.selected_scope = s.to_string();
} }
let bind_rr = { let target = if dc_idx.is_some() {
let guard = self.upstreams.read().await; Self::resolve_runtime_dc_target(target, dc_idx, &upstream, dc_preference)?
guard.get(idx).map(|u| u.bind_rr.clone()) } else {
target
}; };
let (stream, _) = self let (stream, _) = self
@@ -758,9 +862,18 @@ impl UpstreamManager {
.await .await
.ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?; .ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?;
let mut upstream = { let (mut upstream, bind_rr, dc_preference) = {
let guard = self.upstreams.read().await; let guard = self.upstreams.read().await;
guard[idx].config.clone() let state = &guard[idx];
let dc_preference = dc_idx
.and_then(UpstreamState::dc_array_idx)
.map(|dc_array_idx| state.dc_ip_pref[dc_array_idx])
.unwrap_or(IpPreference::Unknown);
(
state.config.clone(),
Some(state.bind_rr.clone()),
dc_preference,
)
}; };
// Set scope for configuration copy // Set scope for configuration copy
@@ -768,9 +881,10 @@ impl UpstreamManager {
upstream.selected_scope = s.to_string(); upstream.selected_scope = s.to_string();
} }
let bind_rr = { let target = if dc_idx.is_some() {
let guard = self.upstreams.read().await; Self::resolve_runtime_dc_target(target, dc_idx, &upstream, dc_preference)?
guard.get(idx).map(|u| u.bind_rr.clone()) } else {
target
}; };
let (stream, egress) = self let (stream, egress) = self
@@ -928,6 +1042,7 @@ impl UpstreamManager {
UpstreamType::Direct { UpstreamType::Direct {
interface, interface,
bind_addresses, bind_addresses,
bindtodevice,
} => { } => {
let bind_ip = Self::resolve_bind_address( let bind_ip = Self::resolve_bind_address(
interface, interface,
@@ -943,6 +1058,10 @@ impl UpstreamManager {
} }
let socket = create_outgoing_socket_bound(target, bind_ip)?; let socket = create_outgoing_socket_bound(target, bind_ip)?;
if let Some(device) = bindtodevice.as_deref().filter(|value| !value.is_empty()) {
bind_outgoing_socket_to_device(&socket, device).map_err(ProxyError::Io)?;
debug!(bindtodevice = %device, target = %target, "Pinned socket to interface");
}
if let Some(ip) = bind_ip { if let Some(ip) = bind_ip {
debug!(bind = %ip, target = %target, "Bound outgoing socket"); debug!(bind = %ip, target = %target, "Bound outgoing socket");
} else if interface.is_some() || bind_addresses.is_some() { } else if interface.is_some() || bind_addresses.is_some() {
@@ -1201,14 +1320,26 @@ impl UpstreamManager {
.map(|(i, u)| (i, u.config.clone(), u.bind_rr.clone())) .map(|(i, u)| (i, u.config.clone(), u.bind_rr.clone()))
.collect() .collect()
}; };
let has_unscoped = upstreams
.iter()
.any(|(_, cfg, _)| Self::is_unscoped_upstream(cfg));
let mut all_results = Vec::new(); let mut all_results = Vec::new();
for (upstream_idx, upstream_config, bind_rr) in &upstreams { for (upstream_idx, upstream_config, bind_rr) in &upstreams {
// DC connectivity checks should follow the default routing path.
// Scoped upstreams are included only when no unscoped upstream exists.
if !Self::should_check_in_default_dc_connectivity(has_unscoped, upstream_config) {
continue;
}
let (upstream_ipv4_enabled, upstream_ipv6_enabled) =
Self::resolve_probe_dc_families(upstream_config, ipv4_enabled, ipv6_enabled);
let upstream_name = match &upstream_config.upstream_type { let upstream_name = match &upstream_config.upstream_type {
UpstreamType::Direct { UpstreamType::Direct {
interface, interface,
bind_addresses, bind_addresses,
bindtodevice,
} => { } => {
let mut direct_parts = Vec::new(); let mut direct_parts = Vec::new();
if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) { if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) {
@@ -1217,6 +1348,9 @@ impl UpstreamManager {
if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) { if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
direct_parts.push(format!("src={}", src.join(","))); 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() { if direct_parts.is_empty() {
"direct".to_string() "direct".to_string()
} else { } else {
@@ -1233,7 +1367,7 @@ impl UpstreamManager {
}; };
let mut v6_results = Vec::with_capacity(NUM_DCS); let mut v6_results = Vec::with_capacity(NUM_DCS);
if ipv6_enabled { if upstream_ipv6_enabled {
for dc_zero_idx in 0..NUM_DCS { for dc_zero_idx in 0..NUM_DCS {
let dc_v6 = TG_DATACENTERS_V6[dc_zero_idx]; let dc_v6 = TG_DATACENTERS_V6[dc_zero_idx];
let addr_v6 = SocketAddr::new(dc_v6, TG_DATACENTER_PORT); let addr_v6 = SocketAddr::new(dc_v6, TG_DATACENTER_PORT);
@@ -1284,13 +1418,17 @@ impl UpstreamManager {
dc_idx: dc_zero_idx + 1, dc_idx: dc_zero_idx + 1,
dc_addr: SocketAddr::new(dc_v6, TG_DATACENTER_PORT), dc_addr: SocketAddr::new(dc_v6, TG_DATACENTER_PORT),
rtt_ms: None, rtt_ms: None,
error: Some("ipv6 disabled".to_string()), error: Some(if ipv6_enabled {
"ipv6 disabled by upstream policy".to_string()
} else {
"ipv6 disabled".to_string()
}),
}); });
} }
} }
let mut v4_results = Vec::with_capacity(NUM_DCS); let mut v4_results = Vec::with_capacity(NUM_DCS);
if ipv4_enabled { if upstream_ipv4_enabled {
for dc_zero_idx in 0..NUM_DCS { for dc_zero_idx in 0..NUM_DCS {
let dc_v4 = TG_DATACENTERS_V4[dc_zero_idx]; let dc_v4 = TG_DATACENTERS_V4[dc_zero_idx];
let addr_v4 = SocketAddr::new(dc_v4, TG_DATACENTER_PORT); let addr_v4 = SocketAddr::new(dc_v4, TG_DATACENTER_PORT);
@@ -1341,7 +1479,11 @@ impl UpstreamManager {
dc_idx: dc_zero_idx + 1, dc_idx: dc_zero_idx + 1,
dc_addr: SocketAddr::new(dc_v4, TG_DATACENTER_PORT), dc_addr: SocketAddr::new(dc_v4, TG_DATACENTER_PORT),
rtt_ms: None, rtt_ms: None,
error: Some("ipv4 disabled".to_string()), error: Some(if ipv4_enabled {
"ipv4 disabled by upstream policy".to_string()
} else {
"ipv4 disabled".to_string()
}),
}); });
} }
} }
@@ -1361,7 +1503,9 @@ impl UpstreamManager {
match addr_str.parse::<SocketAddr>() { match addr_str.parse::<SocketAddr>() {
Ok(addr) => { Ok(addr) => {
let is_v6 = addr.is_ipv6(); let is_v6 = addr.is_ipv6();
if (is_v6 && !ipv6_enabled) || (!is_v6 && !ipv4_enabled) { if (is_v6 && !upstream_ipv6_enabled)
|| (!is_v6 && !upstream_ipv4_enabled)
{
continue; continue;
} }
let result = tokio::time::timeout( let result = tokio::time::timeout(
@@ -1596,13 +1740,32 @@ impl UpstreamManager {
continue; continue;
} }
let count = self.upstreams.read().await.len(); let target_upstreams: Vec<usize> = {
for i in 0..count { let guard = self.upstreams.read().await;
let has_unscoped = guard
.iter()
.any(|upstream| Self::is_unscoped_upstream(&upstream.config));
guard
.iter()
.enumerate()
.filter(|(_, upstream)| {
Self::should_check_in_default_dc_connectivity(
has_unscoped,
&upstream.config,
)
})
.map(|(idx, _)| idx)
.collect()
};
for i in target_upstreams {
let (config, bind_rr) = { let (config, bind_rr) = {
let guard = self.upstreams.read().await; let guard = self.upstreams.read().await;
let u = &guard[i]; let u = &guard[i];
(u.config.clone(), u.bind_rr.clone()) (u.config.clone(), u.bind_rr.clone())
}; };
let (upstream_ipv4_enabled, upstream_ipv6_enabled) =
Self::resolve_probe_dc_families(&config, ipv4_enabled, ipv6_enabled);
let mut healthy_groups = 0usize; let mut healthy_groups = 0usize;
let mut latency_updates: Vec<(usize, f64)> = Vec::new(); let mut latency_updates: Vec<(usize, f64)> = Vec::new();
@@ -1618,14 +1781,30 @@ impl UpstreamManager {
continue; continue;
} }
let rotation_key = (i, group.dc_idx, is_primary); let filtered_endpoints: Vec<SocketAddr> = endpoints
let start_idx = .iter()
*endpoint_rotation.entry(rotation_key).or_insert(0) % endpoints.len(); .copied()
let mut next_idx = (start_idx + 1) % endpoints.len(); .filter(|endpoint| {
if endpoint.is_ipv4() {
upstream_ipv4_enabled
} else {
upstream_ipv6_enabled
}
})
.collect();
for step in 0..endpoints.len() { if filtered_endpoints.is_empty() {
let endpoint_idx = (start_idx + step) % endpoints.len(); continue;
let endpoint = endpoints[endpoint_idx]; }
let rotation_key = (i, group.dc_idx, is_primary);
let start_idx = *endpoint_rotation.entry(rotation_key).or_insert(0)
% filtered_endpoints.len();
let mut next_idx = (start_idx + 1) % filtered_endpoints.len();
for step in 0..filtered_endpoints.len() {
let endpoint_idx = (start_idx + step) % filtered_endpoints.len();
let endpoint = filtered_endpoints[endpoint_idx];
let start = Instant::now(); let start = Instant::now();
let result = tokio::time::timeout( let result = tokio::time::timeout(
@@ -1644,7 +1823,7 @@ impl UpstreamManager {
Ok(Ok(_stream)) => { Ok(Ok(_stream)) => {
group_ok = true; group_ok = true;
group_rtt_ms = Some(start.elapsed().as_secs_f64() * 1000.0); group_rtt_ms = Some(start.elapsed().as_secs_f64() * 1000.0);
next_idx = (endpoint_idx + 1) % endpoints.len(); next_idx = (endpoint_idx + 1) % filtered_endpoints.len();
break; break;
} }
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -1859,6 +2038,33 @@ mod tests {
assert!(!UpstreamManager::is_hard_connect_error(&error)); assert!(!UpstreamManager::is_hard_connect_error(&error));
} }
#[test]
fn unscoped_selection_detects_default_route_upstream() {
let mut upstream = UpstreamConfig {
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,
};
assert!(UpstreamManager::is_unscoped_upstream(&upstream));
upstream.scopes = "local".to_string();
assert!(!UpstreamManager::is_unscoped_upstream(&upstream));
assert!(!UpstreamManager::should_check_in_default_dc_connectivity(
true, &upstream
));
assert!(UpstreamManager::should_check_in_default_dc_connectivity(
false, &upstream
));
}
#[test] #[test]
fn resolve_bind_address_prefers_explicit_bind_ip() { fn resolve_bind_address_prefers_explicit_bind_ip() {
let target = "203.0.113.10:443".parse::<SocketAddr>().unwrap(); let target = "203.0.113.10:443".parse::<SocketAddr>().unwrap();
@@ -1899,6 +2105,8 @@ mod tests {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
100, 100,
File diff suppressed because it is too large Load Diff
+4401 -241
View File
File diff suppressed because it is too large Load Diff
+56 -55
View File
@@ -24,7 +24,7 @@ from urllib.request import Request, urlopen
# Exceptions # Exceptions
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TememtAPIError(Exception): class TelemtAPIError(Exception):
"""Raised when the API returns an error envelope or a transport error.""" """Raised when the API returns an error envelope or a transport error."""
def __init__(self, message: str, code: str | None = None, def __init__(self, message: str, code: str | None = None,
@@ -35,7 +35,7 @@ class TememtAPIError(Exception):
self.request_id = request_id self.request_id = request_id
def __repr__(self) -> str: def __repr__(self) -> str:
return (f"TememtAPIError(message={str(self)!r}, code={self.code!r}, " return (f"TelemtAPIError(message={str(self)!r}, code={self.code!r}, "
f"http_status={self.http_status}, request_id={self.request_id})") f"http_status={self.http_status}, request_id={self.request_id})")
@@ -58,7 +58,7 @@ class APIResponse:
# Main client # Main client
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TememtAPI: class TelemtAPI:
""" """
HTTP client for the Telemt Control API. HTTP client for the Telemt Control API.
@@ -75,10 +75,10 @@ class TememtAPI:
""" """
def __init__( def __init__(
self, self,
base_url: str = "http://127.0.0.1:9091", base_url: str = "http://127.0.0.1:9091",
auth_header: str | None = None, auth_header: str | None = None,
timeout: int = 10, timeout: int = 10,
) -> None: ) -> None:
self.base_url = base_url.rstrip("/") self.base_url = base_url.rstrip("/")
self.auth_header = auth_header self.auth_header = auth_header
@@ -98,12 +98,12 @@ class TememtAPI:
return h return h
def _request( def _request(
self, self,
method: str, method: str,
path: str, path: str,
body: dict | None = None, body: dict | None = None,
if_match: str | None = None, if_match: str | None = None,
query: dict | None = None, query: dict | None = None,
) -> APIResponse: ) -> APIResponse:
url = self.base_url + path url = self.base_url + path
if query: if query:
@@ -133,22 +133,22 @@ class TememtAPI:
try: try:
payload = json.loads(raw) payload = json.loads(raw)
except Exception: except Exception:
raise TememtAPIError( raise TelemtAPIError(
str(exc), http_status=exc.code str(exc), http_status=exc.code
) from exc ) from exc
err = payload.get("error", {}) err = payload.get("error", {})
raise TememtAPIError( raise TelemtAPIError(
err.get("message", str(exc)), err.get("message", str(exc)),
code=err.get("code"), code=err.get("code"),
http_status=exc.code, http_status=exc.code,
request_id=payload.get("request_id"), request_id=payload.get("request_id"),
) from exc ) from exc
except URLError as exc: except URLError as exc:
raise TememtAPIError(str(exc)) from exc raise TelemtAPIError(str(exc)) from exc
if not payload.get("ok"): if not payload.get("ok"):
err = payload.get("error", {}) err = payload.get("error", {})
raise TememtAPIError( raise TelemtAPIError(
err.get("message", "unknown error"), err.get("message", "unknown error"),
code=err.get("code"), code=err.get("code"),
request_id=payload.get("request_id"), request_id=payload.get("request_id"),
@@ -298,16 +298,16 @@ class TememtAPI:
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def create_user( def create_user(
self, self,
username: str, username: str,
*, *,
secret: str | None = None, secret: str | None = None,
user_ad_tag: str | None = None, user_ad_tag: str | None = None,
max_tcp_conns: int | None = None, max_tcp_conns: int | None = None,
expiration_rfc3339: str | None = None, expiration_rfc3339: str | None = None,
data_quota_bytes: int | None = None, data_quota_bytes: int | None = None,
max_unique_ips: int | None = None, max_unique_ips: int | None = None,
if_match: str | None = None, if_match: str | None = None,
) -> APIResponse: ) -> APIResponse:
"""POST /v1/users — create a new user. """POST /v1/users — create a new user.
@@ -340,16 +340,16 @@ class TememtAPI:
return self._post("/v1/users", body=body, if_match=if_match) return self._post("/v1/users", body=body, if_match=if_match)
def patch_user( def patch_user(
self, self,
username: str, username: str,
*, *,
secret: str | None = None, secret: str | None = None,
user_ad_tag: str | None = None, user_ad_tag: str | None = None,
max_tcp_conns: int | None = None, max_tcp_conns: int | None = None,
expiration_rfc3339: str | None = None, expiration_rfc3339: str | None = None,
data_quota_bytes: int | None = None, data_quota_bytes: int | None = None,
max_unique_ips: int | None = None, max_unique_ips: int | None = None,
if_match: str | None = None, if_match: str | None = None,
) -> APIResponse: ) -> APIResponse:
"""PATCH /v1/users/{username} — partial update; only provided fields change. """PATCH /v1/users/{username} — partial update; only provided fields change.
@@ -385,10 +385,10 @@ class TememtAPI:
if_match=if_match) if_match=if_match)
def delete_user( def delete_user(
self, self,
username: str, username: str,
*, *,
if_match: str | None = None, if_match: str | None = None,
) -> APIResponse: ) -> APIResponse:
"""DELETE /v1/users/{username} — remove user; blocks deletion of last user. """DELETE /v1/users/{username} — remove user; blocks deletion of last user.
@@ -403,11 +403,11 @@ class TememtAPI:
# in the route matcher (documented limitation). The method is provided # in the route matcher (documented limitation). The method is provided
# for completeness and future compatibility. # for completeness and future compatibility.
def rotate_secret( def rotate_secret(
self, self,
username: str, username: str,
*, *,
secret: str | None = None, secret: str | None = None,
if_match: str | None = None, if_match: str | None = None,
) -> APIResponse: ) -> APIResponse:
"""POST /v1/users/{username}/rotate-secret — rotate user secret. """POST /v1/users/{username}/rotate-secret — rotate user secret.
@@ -533,12 +533,12 @@ EXAMPLES
help="Username for user commands") help="Username for user commands")
# user create/patch fields # user create/patch fields
p.add_argument("--secret", default=None) p.add_argument("--secret", default=None)
p.add_argument("--ad-tag", dest="ad_tag", default=None) p.add_argument("--ad-tag", dest="ad_tag", default=None)
p.add_argument("--max-conns", dest="max_conns", type=int, default=None) p.add_argument("--max-conns", dest="max_conns", type=int, default=None)
p.add_argument("--expires", default=None) p.add_argument("--expires", default=None)
p.add_argument("--quota", type=int, default=None) p.add_argument("--quota", type=int, default=None)
p.add_argument("--max-ips", dest="max_ips", type=int, default=None) p.add_argument("--max-ips", dest="max_ips", type=int, default=None)
# events # events
p.add_argument("--limit", type=int, default=None, p.add_argument("--limit", type=int, default=None,
@@ -564,10 +564,10 @@ if __name__ == "__main__":
sys.exit(0) sys.exit(0)
if cmd == "gen-secret": if cmd == "gen-secret":
print(TememtAPI.generate_secret()) print(TelemtAPI.generate_secret())
sys.exit(0) sys.exit(0)
api = TememtAPI(args.url, auth_header=args.auth, timeout=args.timeout) api = TelemtAPI(args.url, auth_header=args.auth, timeout=args.timeout)
try: try:
# -- read endpoints -------------------------------------------------- # -- read endpoints --------------------------------------------------
@@ -690,7 +690,8 @@ if __name__ == "__main__":
parser.error("patch command requires <username>") parser.error("patch command requires <username>")
if not any([args.secret, args.ad_tag, args.max_conns, if not any([args.secret, args.ad_tag, args.max_conns,
args.expires, args.quota, args.max_ips]): args.expires, args.quota, args.max_ips]):
parser.error("patch requires at least one field (--secret, --max-conns, --expires, --quota, --max-ips, --ad-tag)") parser.error(
"patch requires at least one field (--secret, --max-conns, --expires, --quota, --max-ips, --ad-tag)")
_print(api.patch_user( _print(api.patch_user(
args.arg, args.arg,
secret=args.secret, secret=args.secret,
@@ -721,7 +722,7 @@ if __name__ == "__main__":
file=sys.stderr) file=sys.stderr)
sys.exit(1) sys.exit(1)
except TememtAPIError as exc: except TelemtAPIError as exc:
print(f"API error [{exc.http_status}] {exc.code}: {exc}", file=sys.stderr) print(f"API error [{exc.http_status}] {exc.code}: {exc}", file=sys.stderr)
sys.exit(1) sys.exit(1)
except KeyboardInterrupt: except KeyboardInterrupt: