mirror of
https://github.com/telemt/telemt.git
synced 2026-05-22 10:55:42 +03:00
Compare commits
149 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b25bada29 | ||
|
|
bde30eaf05 | ||
|
|
b447f60a72 | ||
|
|
093faed0c2 | ||
|
|
3ca3e8ff0e | ||
|
|
6e3b4a1ce5 | ||
|
|
cd0771eee4 | ||
|
|
a858dd799e | ||
|
|
947ef2beb7 | ||
|
|
376f9b42fb | ||
|
|
191ca35076 | ||
|
|
44485a545e | ||
|
|
17a966b822 | ||
|
|
073eacbb37 | ||
|
|
5c99cd8eb7 | ||
|
|
7494cb3092 | ||
|
|
d100941426 | ||
|
|
d25aa5a1e9 | ||
|
|
f1b7b9aa08 | ||
|
|
3bff4fbfcd | ||
|
|
f5b5ea3bbf | ||
|
|
f36f2eae24 | ||
|
|
497ec6aa84 | ||
|
|
21ca1014ae | ||
|
|
982bfd20b9 | ||
|
|
0bcc3bf935 | ||
|
|
f7913721e2 | ||
|
|
32d5cee01c | ||
|
|
3a17901e83 | ||
|
|
902a4e83cf | ||
|
|
696316f919 | ||
|
|
d7a0319696 | ||
|
|
3fefcdd11f | ||
|
|
57dca639f0 | ||
|
|
13f86062f4 | ||
|
|
9303c7854a | ||
|
|
8267149b53 | ||
|
|
30fab00bfd | ||
|
|
afc07345f5 | ||
|
|
a965b38bd4 | ||
|
|
f0ebbac338 | ||
|
|
286662fc51 | ||
|
|
c5390baaf1 | ||
|
|
1cd1e96079 | ||
|
|
2b995c31b0 | ||
|
|
442320302d | ||
|
|
ac0dde567b | ||
|
|
b2fe9b78d8 | ||
|
|
f039ce1827 | ||
|
|
abff2fd7fe | ||
|
|
0b580eccd3 | ||
|
|
70b63e4e0b | ||
|
|
5f5a3e3fa0 | ||
|
|
3f69b54f5d | ||
|
|
62a90e05a0 | ||
|
|
f9e54ee739 | ||
|
|
1b3d2d8bc5 | ||
|
|
d477d6ee29 | ||
|
|
1383dfcbb1 | ||
|
|
107a7cc758 | ||
|
|
4f3193fdaa | ||
|
|
d6be691c67 | ||
|
|
0b0be07a9c | ||
|
|
26c40092f3 | ||
|
|
192a852034 | ||
|
|
16c7a63fbc | ||
|
|
69a73d5fec | ||
|
|
7b1aa46753 | ||
|
|
a728c727bc | ||
|
|
d23ce4a184 | ||
|
|
e48e1b141d | ||
|
|
82da541f9c | ||
|
|
6d5a1a29df | ||
|
|
026ca5cc1d | ||
|
|
b11dec7f91 | ||
|
|
edd1405562 | ||
|
|
45dd7485a9 | ||
|
|
901cf11c51 | ||
|
|
7acc76b422 | ||
|
|
227a64ef06 | ||
|
|
6748ed920e | ||
|
|
303b273c77 | ||
|
|
3bcc129b8d | ||
|
|
3ffbd294d2 | ||
|
|
ddeda8d914 | ||
|
|
17fd01a2c4 | ||
|
|
8ed43a562c | ||
|
|
fd6243b6cc | ||
|
|
44127c6f96 | ||
|
|
a0c7a9e62c | ||
|
|
d7af1cc206 | ||
|
|
f8e22970c1 | ||
|
|
792f626336 | ||
|
|
c5c98bb7fa | ||
|
|
6102280345 | ||
|
|
177f0f0325 | ||
|
|
abcce12368 | ||
|
|
31cbf31491 | ||
|
|
f479ecd1ad | ||
|
|
5c953eb4ba | ||
|
|
3771eb4ab2 | ||
|
|
b246f0ed99 | ||
|
|
07d19027f6 | ||
|
|
877d16659e | ||
|
|
1265234491 | ||
|
|
07b53785c5 | ||
|
|
1e3522652c | ||
|
|
79f4ff4eec | ||
|
|
e6c64525e3 | ||
|
|
ec231aade6 | ||
|
|
59df74e341 | ||
|
|
21a33e4d2a | ||
|
|
73bf23eb61 | ||
|
|
4a904568da | ||
|
|
a526fee728 | ||
|
|
265478b9ca | ||
|
|
038f688e75 | ||
|
|
fa3a1b4dbc | ||
|
|
e2e8b54f87 | ||
|
|
970313edcb | ||
|
|
45c66bc823 | ||
|
|
5e38a72add | ||
|
|
731619bfaa | ||
|
|
c23cdddbd2 | ||
|
|
7ba02ea3d5 | ||
|
|
1e06c32718 | ||
|
|
38c5f73d6a | ||
|
|
010f176ad4 | ||
|
|
2f616500c9 | ||
|
|
852dc11722 | ||
|
|
cda9600169 | ||
|
|
dc03c73dd6 | ||
|
|
c99f55f216 | ||
|
|
f5786d284b | ||
|
|
0281cad564 | ||
|
|
91d9cb8de0 | ||
|
|
9e74a78209 | ||
|
|
9933cdf245 | ||
|
|
b4a3ad9aad | ||
|
|
23156a840d | ||
|
|
cf9d4b2c61 | ||
|
|
63cfc067f6 | ||
|
|
5863b33b81 | ||
|
|
7ce87749c0 | ||
|
|
185e0081d7 | ||
|
|
b6a30c1b51 | ||
|
|
19f9eb36ac | ||
|
|
2b8159a65e | ||
|
|
86be0d53fe |
16
.github/FUNDING.yml
vendored
Normal file
16
.github/FUNDING.yml
vendored
Normal 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
|
||||
@@ -3,50 +3,39 @@
|
||||
## Purpose
|
||||
|
||||
**Telemt exists to solve technical problems.**
|
||||
- Telemt is open to contributors who want to learn, improve and build meaningful systems together.
|
||||
- It is a place for building, testing, reasoning, documenting, and improving systems.
|
||||
- Discussions that advance this work are in scope, discussions that divert it are not.
|
||||
- Technology has consequences, responsibility is inherent.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
> Design follows intent
|
||||
|
||||
---
|
||||
|
||||
## Principles
|
||||
|
||||
* **Technical over emotional**
|
||||
|
||||
Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
|
||||
- Arguments are grounded in data, logs, reproducible cases, or clear reasoning.
|
||||
|
||||
* **Clarity over noise**
|
||||
|
||||
Communication is structured, concise, and relevant.
|
||||
- Communication is structured, concise, and relevant.
|
||||
|
||||
* **Openness with standards**
|
||||
|
||||
Participation is open. The work remains disciplined.
|
||||
- Participation is open. The work remains disciplined.
|
||||
|
||||
* **Independence of judgment**
|
||||
|
||||
Claims are evaluated on technical merit, not affiliation or posture.
|
||||
- Claims are evaluated on technical merit, not affiliation or posture.
|
||||
|
||||
* **Responsibility over capability**
|
||||
|
||||
Capability does not justify careless use.
|
||||
- Capability does not justify careless use.
|
||||
|
||||
* **Cooperation over friction**
|
||||
|
||||
Progress depends on coordination, mutual support, and honest review.
|
||||
- Progress depends on coordination, mutual support, and honest review.
|
||||
|
||||
* **Good intent, rigorous method**
|
||||
|
||||
Assume good intent, but require rigor.
|
||||
- Assume good intent, but require rigor.
|
||||
|
||||
> **Aussagen gelten nach ihrer Begründung.**
|
||||
|
||||
@@ -68,7 +57,9 @@ Participants are expected to:
|
||||
|
||||
Precision is learned.
|
||||
|
||||
New contributors are welcome. They are expected to grow into these standards. Existing contributors are expected to make that growth possible.
|
||||
- New contributors are welcome
|
||||
- They are expected to grow into these standards
|
||||
- Existing contributors are expected to make that growth possible
|
||||
|
||||
> **Wer behauptet, belegt.**
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
> **Ordnung ist Voraussetzung der Funktion.**
|
||||
|
||||
> Order is the precondition of function.
|
||||
> **Klarheit vor Zustimmung - Bestand vor Beifall**
|
||||
|
||||
> Clarity above approval - substantiality before success
|
||||
---
|
||||
|
||||
## Enforcement
|
||||
@@ -171,42 +161,41 @@ Actions are taken to maintain function, continuity, and signal quality.
|
||||
|
||||
## Final
|
||||
|
||||
Telemt is built on discipline, structure, and shared intent.
|
||||
- Signal over noise.
|
||||
- Facts over opinion.
|
||||
- Systems over rhetoric.
|
||||
**Telemt is built on discipline, structure, and shared intent**
|
||||
- Signal over noise
|
||||
- Facts over opinion
|
||||
- Systems over rhetoric
|
||||
- Work is collective
|
||||
- Outcomes are shared
|
||||
- Responsibility is distributed
|
||||
- Precision is learned
|
||||
- Rigor is expected
|
||||
- Help is part of the work
|
||||
|
||||
- Work is collective.
|
||||
- Outcomes are shared.
|
||||
- Responsibility is distributed.
|
||||
> **Ordnung ist Voraussetzung der Freiheit**
|
||||
|
||||
- Precision is learned.
|
||||
- Rigor is expected.
|
||||
- Help is part of the work.
|
||||
|
||||
> **Ordnung ist Voraussetzung der Freiheit.**
|
||||
|
||||
- If you contribute — contribute with care.
|
||||
- If you speak — speak with substance.
|
||||
- If you engage — engage constructively.
|
||||
- If you contribute — contribute with care
|
||||
- If you speak — speak with substance
|
||||
- If you engage — engage constructively
|
||||
|
||||
---
|
||||
|
||||
## After All
|
||||
|
||||
Systems outlive intentions.
|
||||
- What is built will be used.
|
||||
- What is released will propagate.
|
||||
- What is maintained will define the future state.
|
||||
Systems outlive intentions
|
||||
- What is built will be used
|
||||
- What is released will propagate
|
||||
- What is maintained will define the future state
|
||||
|
||||
There is no neutral infrastructure, only infrastructure shaped well or poorly.
|
||||
There is no neutral infrastructure, only infrastructure shaped well or poorly
|
||||
|
||||
> **Jedes System trägt Verantwortung.**
|
||||
> **Ordnung → Umsetzung → Ergebnis**
|
||||
|
||||
> Every system carries responsibility.
|
||||
> Order → Implementation → Result
|
||||
|
||||
- Stability requires discipline.
|
||||
- Freedom requires structure.
|
||||
- Trust requires honesty.
|
||||
- Stability requires discipline
|
||||
- Freedom requires structure
|
||||
- Trust requires honesty
|
||||
|
||||
In the end: the system reflects its contributors
|
||||
|
||||
In the end: the system reflects its contributors.
|
||||
|
||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -2780,7 +2780,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
||||
|
||||
[[package]]
|
||||
name = "telemt"
|
||||
version = "3.3.39"
|
||||
version = "3.4.3"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"anyhow",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "telemt"
|
||||
version = "3.3.39"
|
||||
version = "3.4.3"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
@@ -98,4 +98,3 @@ harness = false
|
||||
[profile.release]
|
||||
lto = "fat"
|
||||
codegen-units = 1
|
||||
|
||||
|
||||
30
Dockerfile
30
Dockerfile
@@ -77,6 +77,34 @@ COPY config.toml /app/config.toml
|
||||
|
||||
EXPOSE 443 9090 9091
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["/app/telemt", "healthcheck", "/app/config.toml", "--mode", "liveness"]
|
||||
|
||||
ENTRYPOINT ["/app/telemt"]
|
||||
CMD ["config.toml"]
|
||||
|
||||
# ==========================
|
||||
# Production Netfilter Profile
|
||||
# ==========================
|
||||
FROM debian:12-slim AS prod-netfilter
|
||||
|
||||
RUN set -eux; \
|
||||
apt-get update; \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
conntrack \
|
||||
nftables \
|
||||
iptables; \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=minimal /telemt /app/telemt
|
||||
COPY config.toml /app/config.toml
|
||||
|
||||
EXPOSE 443 9090 9091
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["/app/telemt", "healthcheck", "/app/config.toml", "--mode", "liveness"]
|
||||
|
||||
ENTRYPOINT ["/app/telemt"]
|
||||
CMD ["config.toml"]
|
||||
|
||||
@@ -94,5 +122,7 @@ USER nonroot:nonroot
|
||||
|
||||
EXPOSE 443 9090 9091
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD ["/app/telemt", "healthcheck", "/app/config.toml", "--mode", "liveness"]
|
||||
|
||||
ENTRYPOINT ["/app/telemt"]
|
||||
CMD ["config.toml"]
|
||||
|
||||
262
README.md
262
README.md
@@ -1,189 +1,57 @@
|
||||
# Telemt - MTProxy on Rust + Tokio
|
||||
|
||||
[](https://github.com/telemt/telemt/releases/latest) [](https://github.com/telemt/telemt/stargazers) [](https://github.com/telemt/telemt/network/members) [](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***
|
||||
|
||||
### [**Telemt Chat in Telegram**](https://t.me/telemtrs)
|
||||
#### Fixed TLS ClientHello is now available in Telegram Desktop starting from version 6.7.2: to work with EE-MTProxy, please update your client;
|
||||
#### Fixed TLS ClientHello for Telegram Android Client is available in [our chat](https://t.me/telemtrs/30234/36441); official releases for Android and iOS are "work in progress";
|
||||
> [!NOTE]
|
||||
>
|
||||
> Fixed TLS ClientHello is now available in official clients for Desktop / Android / iOS
|
||||
>
|
||||
> To work with EE-MTProxy, please update your client!
|
||||
|
||||
<p align="center">
|
||||
<a href="https://t.me/telemtrs">
|
||||
<img src="/docs/assets/telegram_button.svg" width="150"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
|
||||
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md)
|
||||
- [Full-covered API w/ management](https://github.com/telemt/telemt/blob/main/docs/API.md)
|
||||
- Anti-Replay on Sliding Window
|
||||
- Prometheus-format Metrics
|
||||
- TLS-Fronting and TCP-Splicing for masking from "prying" eyes
|
||||
**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
|
||||
|
||||
⚓ 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](#recognizability-for-dpi-and-crawler)
|
||||
### 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)
|
||||
|
||||
⚓ 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
|
||||
## Features
|
||||
Our implementation of **TLS-fronting** is one of the most deeply debugged, focused, advanced and *almost* **"behaviorally consistent to real"**: we are confident we have it right - [see evidence on our validation and traces](docs/FAQ.en.md#recognizability-for-dpi-and-crawler)
|
||||
|
||||
Our ***Middle-End Pool*** is fastest by design in standard scenarios, compared to other implementations of connecting to the Middle-End Proxy: non dramatically, but usual
|
||||
|
||||
- Full support for all official MTProto proxy modes:
|
||||
- Classic
|
||||
- Secure - with `dd` prefix
|
||||
- Fake TLS - with `ee` prefix + SNI fronting
|
||||
- Replay attack protection
|
||||
- Optional traffic masking: forward unrecognized connections to a real web server, e.g. GitHub 🤪
|
||||
- Configurable keepalives + timeouts + IPv6 and "Fast Mode"
|
||||
- Graceful shutdown on Ctrl+C
|
||||
- Extensive logging via `trace` and `debug` with `RUST_LOG` method
|
||||
|
||||
# GOTO
|
||||
- [Quick Start Guide](#quick-start-guide)
|
||||
- [FAQ](#faq)
|
||||
- [Recognizability for DPI and crawler](#recognizability-for-dpi-and-crawler)
|
||||
- [Client WITH secret-key accesses the MTProxy resource:](#client-with-secret-key-accesses-the-mtproxy-resource)
|
||||
- [Client WITHOUT secret-key gets transparent access to the specified resource:](#client-without-secret-key-gets-transparent-access-to-the-specified-resource)
|
||||
- [Telegram Calls via MTProxy](#telegram-calls-via-mtproxy)
|
||||
- [How does DPI see MTProxy TLS?](#how-does-dpi-see-mtproxy-tls)
|
||||
- [Whitelist on IP](#whitelist-on-ip)
|
||||
- [Too many open files](#too-many-open-files)
|
||||
- [Build](#build)
|
||||
- [Why Rust?](#why-rust)
|
||||
- [Issues](#issues)
|
||||
- [Roadmap](#roadmap)
|
||||
|
||||
|
||||
## Quick Start Guide
|
||||
- [Quick Start Guide RU](docs/QUICK_START_GUIDE.ru.md)
|
||||
- [Quick Start Guide EN](docs/QUICK_START_GUIDE.en.md)
|
||||
- Classic;
|
||||
- Secure - with `dd` prefix;
|
||||
- Fake TLS - with `ee` prefix + SNI fronting;
|
||||
- Replay attack protection;
|
||||
- Optional traffic masking: forward unrecognized connections to a real web server, e.g. GitHub 🤪;
|
||||
- Configurable keepalives + timeouts + IPv6 and "Fast Mode";
|
||||
- Graceful shutdown on Ctrl+C;
|
||||
- Extensive logging via `trace` and `debug` with `RUST_LOG` method.
|
||||
|
||||
## FAQ
|
||||
|
||||
- [FAQ RU](docs/FAQ.ru.md)
|
||||
- [FAQ EN](docs/FAQ.en.md)
|
||||
|
||||
### Recognizability for DPI and crawler
|
||||
|
||||
On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS,
|
||||
based on the ECH extension and the ordering of cipher suites,
|
||||
as well as an overall unique JA3/JA4 fingerprint
|
||||
that does not occur in modern browsers:
|
||||
we have already submitted initial changes to the Telegram Desktop developers and are working on updates for other clients.
|
||||
|
||||
- We consider this a breakthrough aspect, which has no stable analogues today
|
||||
- Based on this: if `telemt` configured correctly, **TLS mode is completely identical to real-life handshake + communication** with a specified host
|
||||
- Here is our evidence:
|
||||
- 212.220.88.77 - "dummy" host, running `telemt`
|
||||
- `petrovich.ru` - `tls` + `masking` host, in HEX: `706574726f766963682e7275`
|
||||
- **No MITM + No Fake Certificates/Crypto** = pure transparent *TCP Splice* to "best" upstream: MTProxy or tls/mask-host:
|
||||
- DPI see legitimate HTTPS to `tls_host`, including *valid chain-of-trust* and entropy
|
||||
- Crawlers completely satisfied receiving responses from `mask_host`
|
||||
#### Client WITH secret-key accesses the MTProxy resource:
|
||||
|
||||
<img width="360" height="439" alt="telemt" src="https://github.com/user-attachments/assets/39352afb-4a11-4ecc-9d91-9e8cfb20607d" />
|
||||
|
||||
#### Client WITHOUT secret-key gets transparent access to the specified resource:
|
||||
- with trusted certificate
|
||||
- with original handshake
|
||||
- with full request-response way
|
||||
- with low-latency overhead
|
||||
```bash
|
||||
root@debian:~/telemt# curl -v -I --resolve petrovich.ru:443:212.220.88.77 https://petrovich.ru/
|
||||
* Added petrovich.ru:443:212.220.88.77 to DNS cache
|
||||
* Hostname petrovich.ru was found in DNS cache
|
||||
* Trying 212.220.88.77:443...
|
||||
* Connected to petrovich.ru (212.220.88.77) port 443 (#0)
|
||||
* ALPN: offers h2,http/1.1
|
||||
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
|
||||
* CAfile: /etc/ssl/certs/ca-certificates.crt
|
||||
* CApath: /etc/ssl/certs
|
||||
* TLSv1.3 (IN), TLS handshake, Server hello (2):
|
||||
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
|
||||
* TLSv1.3 (IN), TLS handshake, Certificate (11):
|
||||
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
|
||||
* TLSv1.3 (IN), TLS handshake, Finished (20):
|
||||
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
|
||||
* TLSv1.3 (OUT), TLS handshake, Finished (20):
|
||||
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
|
||||
* ALPN: server did not agree on a protocol. Uses default.
|
||||
* Server certificate:
|
||||
* subject: C=RU; ST=Saint Petersburg; L=Saint Petersburg; O=STD Petrovich; CN=*.petrovich.ru
|
||||
* start date: Jan 28 11:21:01 2025 GMT
|
||||
* expire date: Mar 1 11:21:00 2026 GMT
|
||||
* subjectAltName: host "petrovich.ru" matched cert's "petrovich.ru"
|
||||
* issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018
|
||||
* SSL certificate verify ok.
|
||||
* using HTTP/1.x
|
||||
> HEAD / HTTP/1.1
|
||||
> Host: petrovich.ru
|
||||
> User-Agent: curl/7.88.1
|
||||
> Accept: */*
|
||||
>
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* old SSL session ID is stale, removing
|
||||
< HTTP/1.1 200 OK
|
||||
HTTP/1.1 200 OK
|
||||
< Server: Variti/0.9.3a
|
||||
Server: Variti/0.9.3a
|
||||
< Date: Thu, 01 Jan 2026 00:0000 GMT
|
||||
Date: Thu, 01 Jan 2026 00:0000 GMT
|
||||
< Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Origin: *
|
||||
< Content-Type: text/html
|
||||
Content-Type: text/html
|
||||
< Cache-Control: no-store
|
||||
Cache-Control: no-store
|
||||
< Expires: Thu, 01 Jan 2026 00:0000 GMT
|
||||
Expires: Thu, 01 Jan 2026 00:0000 GMT
|
||||
< Pragma: no-cache
|
||||
Pragma: no-cache
|
||||
< Set-Cookie: ipp_uid=XXXXX/XXXXX/XXXXX==; Expires=Tue, 31 Dec 2040 23:59:59 GMT; Domain=.petrovich.ru; Path=/
|
||||
Set-Cookie: ipp_uid=XXXXX/XXXXX/XXXXX==; Expires=Tue, 31 Dec 2040 23:59:59 GMT; Domain=.petrovich.ru; Path=/
|
||||
< Content-Type: text/html
|
||||
Content-Type: text/html
|
||||
< Content-Length: 31253
|
||||
Content-Length: 31253
|
||||
< Connection: keep-alive
|
||||
Connection: keep-alive
|
||||
< Keep-Alive: timeout=60
|
||||
Keep-Alive: timeout=60
|
||||
|
||||
<
|
||||
* Connection #0 to host petrovich.ru left intact
|
||||
|
||||
```
|
||||
- We challenged ourselves, we kept trying and we didn't only *beat the air*: now, we have something to show you
|
||||
- Do not just take our word for it? - This is great and we respect that: you can build your own `telemt` or download a build and check it right now
|
||||
### Telegram Calls via MTProxy
|
||||
- Telegram architecture **does NOT allow calls via MTProxy**, but only via SOCKS5, which cannot be obfuscated
|
||||
### How does DPI see MTProxy TLS?
|
||||
- DPI sees MTProxy in Fake TLS (ee) mode as TLS 1.3
|
||||
- the SNI you specify sends both the client and the server;
|
||||
- ALPN is similar to HTTP 1.1/2;
|
||||
- high entropy, which is normal for AES-encrypted traffic;
|
||||
### Whitelist on IP
|
||||
- MTProxy cannot work when there is:
|
||||
- no IP connectivity to the target host: Russian Whitelist on Mobile Networks - "Белый список"
|
||||
- OR all TCP traffic is blocked
|
||||
- OR high entropy/encrypted traffic is blocked: content filters at universities and critical infrastructure
|
||||
- OR all TLS traffic is blocked
|
||||
- OR specified port is blocked: use 443 to make it "like real"
|
||||
- OR provided SNI is blocked: use "officially approved"/innocuous name
|
||||
- like most protocols on the Internet;
|
||||
- these situations are observed:
|
||||
- in China behind the Great Firewall
|
||||
- in Russia on mobile networks, less in wired networks
|
||||
- in Iran during "activity"
|
||||
### Too many open files
|
||||
- On a fresh Linux install the default open file limit is low; under load `telemt` may fail with `Accept error: Too many open files`
|
||||
- **Systemd**: add `LimitNOFILE=65536` to the `[Service]` section (already included in the example above)
|
||||
- **Docker**: add `--ulimit nofile=65536:65536` to your `docker run` command, or in `docker-compose.yml`:
|
||||
```yaml
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 65536
|
||||
hard: 65536
|
||||
```
|
||||
- **System-wide** (optional): add to `/etc/security/limits.conf`:
|
||||
```
|
||||
* soft nofile 1048576
|
||||
* hard nofile 1048576
|
||||
root soft nofile 1048576
|
||||
root hard nofile 1048576
|
||||
```
|
||||
|
||||
# Learn more about Telemt
|
||||
- [Our Architecture](docs/Architecture)
|
||||
- [All Config Options](docs/Config_params)
|
||||
- [How to build your own Telemt?](#build)
|
||||
- [Running on BSD](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md)
|
||||
- [Why Rust?](#why-rust)
|
||||
|
||||
## Build
|
||||
```bash
|
||||
@@ -194,9 +62,8 @@ cd telemt
|
||||
# Starting Release Build
|
||||
cargo build --release
|
||||
|
||||
# Low-RAM devices (1 GB, e.g. NanoPi Neo3 / Raspberry Pi Zero 2):
|
||||
# release profile uses lto = "thin" to reduce peak linker memory.
|
||||
# If your custom toolchain overrides profiles, avoid enabling fat LTO.
|
||||
# Current release profile uses lto = "fat" for maximum optimization (see Cargo.toml).
|
||||
# On low-RAM systems (~1 GB) you can override it to "thin".
|
||||
|
||||
# Move to /bin
|
||||
mv ./target/release/telemt /bin
|
||||
@@ -206,12 +73,6 @@ chmod +x /bin/telemt
|
||||
telemt config.toml
|
||||
```
|
||||
|
||||
### OpenBSD
|
||||
- Build and service setup guide: [OpenBSD Guide (EN)](docs/OPENBSD.en.md)
|
||||
- Example rc.d script: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd)
|
||||
- Status: OpenBSD sandbox hardening with `pledge(2)` and `unveil(2)` is not implemented yet.
|
||||
|
||||
|
||||
## Why Rust?
|
||||
- Long-running reliability and idempotent behavior
|
||||
- Rust's deterministic resource management - RAII
|
||||
@@ -219,23 +80,26 @@ telemt config.toml
|
||||
- Memory safety and reduced attack surface
|
||||
- Tokio's asynchronous architecture
|
||||
|
||||
## Issues
|
||||
- ✅ [SOCKS5 as Upstream](https://github.com/telemt/telemt/issues/1) -> added Upstream Management
|
||||
- ✅ [iOS - Media Upload Hanging-in-Loop](https://github.com/telemt/telemt/issues/2)
|
||||
## Support Telemt
|
||||
|
||||
## Roadmap
|
||||
- Public IP in links
|
||||
- Config Reload-on-fly
|
||||
- Bind to device or IP for outbound/inbound connections
|
||||
- Adtag Support per SNI / Secret
|
||||
- Fail-fast on start + Fail-soft on runtime (only WARN/ERROR)
|
||||
- Zero-copy, minimal allocs on hotpath
|
||||
- DC Healthchecks + global fallback
|
||||
- No global mutable state
|
||||
- Client isolation + Fair Bandwidth
|
||||
- Backpressure-aware IO
|
||||
- "Secret Policy" - SNI / Secret Routing :D
|
||||
- Multi-upstream Balancer and Failover
|
||||
- Strict FSM per handshake
|
||||
- Session-based Antireplay with Sliding window, non-broking reconnects
|
||||
- Web Control: statistic, state of health, latency, client experience...
|
||||
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.
|
||||
|
||||
|
||||

|
||||
|
||||
109
README.ru.md
Normal file
109
README.ru.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Telemt — MTProxy на Rust + Tokio
|
||||
|
||||
[](https://github.com/telemt/telemt/releases/latest) [](https://github.com/telemt/telemt/stargazers) [](https://github.com/telemt/telemt/network/members) [](https://t.me/telemtrs)
|
||||
|
||||
***Решает проблемы раньше, чем другие узнают об их существовании***
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> Исправленный TLS ClientHello доступен в Telegram для настольных ПК, Android и iOS.
|
||||
>
|
||||
> Пожалуйста, обновите клиентское приложение для работы с EE-MTProxy.
|
||||
|
||||
<p align="center">
|
||||
<a href="https://t.me/telemtrs">
|
||||
<img src="/docs/assets/telegram_button.svg" width="150"/>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
**Telemt** — это быстрый, безопасный и функциональный сервер, написанный на Rust. Он полностью реализует официальный алгоритм прокси Telegram и добавляет множество улучшений для продакшена:
|
||||
|
||||
## Установка и обновление одной командой
|
||||
|
||||
```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-и-сканеров)).
|
||||
|
||||
***Middle-End Pool*** оптимизирован для высокой производительности.
|
||||
|
||||
- Поддержка всех режимов MTProto proxy:
|
||||
- Classic;
|
||||
- Secure (префикс `dd`);
|
||||
- Fake TLS (префикс `ee` + SNI fronting);
|
||||
- Защита от replay-атак;
|
||||
- Маскировка трафика (перенаправление неизвестных подключений на реальные сайты);
|
||||
- Настраиваемые keepalive, таймауты, IPv6 и «быстрый режим»;
|
||||
- Корректное завершение работы (Ctrl+C);
|
||||
- Подробное логирование через `trace` и `debug`.
|
||||
|
||||
# Подробнее о Telemt
|
||||
- [FAQ](#faq)
|
||||
- [Архитектура](docs/Architecture)
|
||||
- [Параметры конфигурационного файла](docs/Config_params)
|
||||
- [Сборка](#build)
|
||||
- [Установка на BSD](#%D1%83%D1%81%D1%82%D0%B0%D0%BD%D0%BE%D0%B2%D0%BA%D0%B0-%D0%BD%D0%B0-bsd)
|
||||
- [Почему Rust?](#why-rust)
|
||||
|
||||
## FAQ
|
||||
- [FAQ RU](docs/FAQ.ru.md)
|
||||
- [FAQ EN](docs/FAQ.en.md)
|
||||
|
||||
## Сборка
|
||||
```bash
|
||||
# Клонируйте репозиторий
|
||||
git clone https://github.com/telemt/telemt
|
||||
# Смените каталог на telemt
|
||||
cd telemt
|
||||
# Начните процесс сборки
|
||||
cargo build --release
|
||||
|
||||
# В текущем release-профиле используется lto = "fat" для максимальной оптимизации (см. Cargo.toml).
|
||||
# На системах с малым объёмом RAM (~1 ГБ) можно переопределить это значение на "thin".
|
||||
|
||||
# Перейдите в каталог /bin
|
||||
mv ./target/release/telemt /bin
|
||||
# Сделайте файл исполняемым
|
||||
chmod +x /bin/telemt
|
||||
# Запустите!
|
||||
telemt config.toml
|
||||
```
|
||||
|
||||
## Установка на BSD
|
||||
- Руководство по сборке и настройке на английском языке [OpenBSD Guide (EN)](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md);
|
||||
- Пример rc.d скрипта: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd);
|
||||
- Поддержка sandbox с `pledge(2)` и `unveil(2)` пока не реализована.
|
||||
|
||||
## Почему Rust?
|
||||
- Надёжность для долгоживущих процессов;
|
||||
- Детерминированное управление ресурсами (RAII);
|
||||
- Отсутствие сборщика мусора;
|
||||
- Безопасность памяти;
|
||||
- Асинхронная архитектура Tokio.
|
||||
|
||||
## Поддержать Telemt
|
||||
|
||||
Telemt — это бесплатное программное обеспечение с открытым исходным кодом, разработанное в свободное время.
|
||||
Если оно оказалось вам полезным, вы можете поддержать дальнейшую разработку.
|
||||
|
||||
Принимаемые криптовалюты (BTC, ETH, USDT, 350+ и другие):
|
||||
|
||||
<p align="center">
|
||||
<a href="https://nowpayments.io/donation?api_key=2bf1afd2-abc2-49f9-a012-f1e715b37223" target="_blank" rel="noreferrer noopener">
|
||||
<img src="https://nowpayments.io/images/embeds/donation-button-white.svg" alt="Cryptocurrency & Bitcoin donation button by NOWPayments" height="80">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
Monero (XMR) напрямую:
|
||||
|
||||
```
|
||||
8Bk4tZEYPQWSypeD2hrUXG2rKbAKF16GqEN942ZdAP5cFdSqW6h4DwkP5cJMAdszzuPeHeHZPTyjWWFwzeFdjuci3ktfMoB
|
||||
```
|
||||
|
||||
Все пожертвования пойдут на инфраструктуру, разработку и исследования.
|
||||
|
||||

|
||||
13
config.toml
13
config.toml
@@ -32,13 +32,13 @@ show = "*"
|
||||
port = 443
|
||||
# proxy_protocol = false # Enable if behind HAProxy/nginx with PROXY protocol
|
||||
# metrics_port = 9090
|
||||
# metrics_listen = "0.0.0.0:9090" # Listen address for metrics (overrides metrics_port)
|
||||
# metrics_whitelist = ["127.0.0.1", "::1", "0.0.0.0/0"]
|
||||
# metrics_listen = "127.0.0.1:9090" # Listen address for metrics (overrides metrics_port)
|
||||
# metrics_whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
|
||||
[server.api]
|
||||
enabled = true
|
||||
listen = "0.0.0.0:9091"
|
||||
whitelist = ["127.0.0.0/8"]
|
||||
listen = "127.0.0.1:9091"
|
||||
whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
minimal_runtime_enabled = false
|
||||
minimal_runtime_cache_ttl_ms = 1000
|
||||
|
||||
@@ -48,9 +48,12 @@ ip = "0.0.0.0"
|
||||
|
||||
# === Anti-Censorship & Masking ===
|
||||
[censorship]
|
||||
# Fake-TLS / SNI masking domain used in generated ee-links.
|
||||
# Changing tls_domain invalidates previously generated TLS links.
|
||||
tls_domain = "petrovich.ru"
|
||||
|
||||
mask = true
|
||||
tls_emulation = true # Fetch real cert lengths and emulate TLS records
|
||||
tls_emulation = true # Fetch real cert lengths and emulate TLS records
|
||||
tls_front_dir = "tlsfront" # Cache directory for TLS emulation
|
||||
|
||||
[access.users]
|
||||
|
||||
10
docker-compose.host-netfilter.yml
Normal file
10
docker-compose.host-netfilter.yml
Normal file
@@ -0,0 +1,10 @@
|
||||
services:
|
||||
telemt:
|
||||
build:
|
||||
context: .
|
||||
target: prod-netfilter
|
||||
network_mode: host
|
||||
ports: []
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
- NET_ADMIN
|
||||
8
docker-compose.netfilter.yml
Normal file
8
docker-compose.netfilter.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
telemt:
|
||||
build:
|
||||
context: .
|
||||
target: prod-netfilter
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE
|
||||
- NET_ADMIN
|
||||
@@ -1,7 +1,9 @@
|
||||
services:
|
||||
telemt:
|
||||
image: ghcr.io/telemt/telemt:latest
|
||||
build: .
|
||||
build:
|
||||
context: .
|
||||
target: prod
|
||||
container_name: telemt
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -9,23 +11,29 @@ services:
|
||||
- "127.0.0.1:9090:9090"
|
||||
- "127.0.0.1:9091:9091"
|
||||
# Allow caching 'proxy-secret' in read-only container
|
||||
working_dir: /run/telemt
|
||||
working_dir: /etc/telemt
|
||||
volumes:
|
||||
- ./config.toml:/run/telemt/config.toml:ro
|
||||
- ./config.toml:/etc/telemt/config.toml:ro
|
||||
tmpfs:
|
||||
- /run/telemt:rw,mode=1777,size=1m
|
||||
- /etc/telemt:rw,mode=1777,size=4m
|
||||
environment:
|
||||
- RUST_LOG=info
|
||||
healthcheck:
|
||||
test: [ "CMD", "/app/telemt", "healthcheck", "/etc/telemt/config.toml", "--mode", "liveness" ]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 20s
|
||||
# Uncomment this line if you want to use host network for IPv6, but bridge is default and usually better
|
||||
# network_mode: host
|
||||
cap_drop:
|
||||
- ALL
|
||||
cap_add:
|
||||
- NET_BIND_SERVICE # allow binding to port 443
|
||||
- NET_BIND_SERVICE
|
||||
read_only: true
|
||||
security_opt:
|
||||
- no-new-privileges:true
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 65536
|
||||
hard: 65536
|
||||
hard: 262144
|
||||
|
||||
141
docs/Advanced_settings/HIGH_LOAD.en.md
Normal file
141
docs/Advanced_settings/HIGH_LOAD.en.md
Normal 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
docs/Advanced_settings/HIGH_LOAD.ru.md
Normal file
139
docs/Advanced_settings/HIGH_LOAD.ru.md
Normal 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` под высокой нагрузкой).
|
||||
@@ -9,12 +9,12 @@ API runtime is configured in `[server.api]`.
|
||||
|
||||
| Field | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `enabled` | `bool` | `false` | Enables REST API listener. |
|
||||
| `listen` | `string` (`IP:PORT`) | `127.0.0.1:9091` | API bind address. |
|
||||
| `whitelist` | `CIDR[]` | `127.0.0.1/32, ::1/128` | Source IP allowlist. Empty list means allow all. |
|
||||
| `enabled` | `bool` | `true` | Enables REST API listener. |
|
||||
| `listen` | `string` (`IP:PORT`) | `0.0.0.0:9091` | API bind address. |
|
||||
| `whitelist` | `CIDR[]` | `127.0.0.0/8` | Source IP allowlist. Empty list means allow all. |
|
||||
| `auth_header` | `string` | `""` | Exact value for `Authorization` header. Empty disables header auth. |
|
||||
| `request_body_limit_bytes` | `usize` | `65536` | Maximum request body size. Must be `> 0`. |
|
||||
| `minimal_runtime_enabled` | `bool` | `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]`. |
|
||||
| `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. |
|
||||
@@ -0,0 +1,225 @@
|
||||
# TLS Front Profile Fidelity
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes how Telemt reuses captured TLS behavior in the FakeTLS server flight and how to validate the result on a real deployment.
|
||||
|
||||
When TLS front emulation is enabled, Telemt can capture useful server-side TLS behavior from the selected origin and reuse that behavior in the emulated success path. The goal is not to reproduce the origin byte-for-byte, but to reduce stable synthetic traits and make the emitted server flight structurally closer to the captured profile.
|
||||
|
||||
## Why this change exists
|
||||
|
||||
The project already captures useful server-side TLS behavior in the TLS front fetch path:
|
||||
|
||||
- `change_cipher_spec_count`
|
||||
- `app_data_record_sizes`
|
||||
- `ticket_record_sizes`
|
||||
|
||||
Before this change, the emulator used only part of that information. This left a gap between captured origin behavior and emitted FakeTLS server flight.
|
||||
|
||||
## What is implemented
|
||||
|
||||
- The emulator now replays the observed `ChangeCipherSpec` count from the fetched behavior profile.
|
||||
- The emulator now replays observed ticket-like tail ApplicationData record sizes when raw or merged TLS profile data is available.
|
||||
- The emulator now preserves more of the profiled encrypted-flight structure instead of collapsing it into a smaller synthetic shape.
|
||||
- The emulator still falls back to the previous synthetic behavior when the cached profile does not contain raw TLS behavior information.
|
||||
- Operator-configured `tls_new_session_tickets` still works as an additive fallback when the profile does not provide enough tail records.
|
||||
|
||||
## Practical benefit
|
||||
|
||||
- Reduced distinguishability between profiled origin TLS behavior and emulated TLS behavior.
|
||||
- Lower chance of stable server-flight fingerprints caused by fixed CCS count or synthetic-only tail record sizes.
|
||||
- Better reuse of already captured TLS profile data without changing MTProto logic, KDF routing, or transport architecture.
|
||||
|
||||
## Limitations
|
||||
|
||||
This mechanism does not aim to make Telemt byte-identical to the origin server.
|
||||
|
||||
It also does not change:
|
||||
|
||||
- MTProto business logic;
|
||||
- KDF routing behavior;
|
||||
- the overall transport architecture.
|
||||
|
||||
The practical goal is narrower:
|
||||
|
||||
- reuse more captured profile data;
|
||||
- reduce fixed synthetic behavior in the server flight;
|
||||
- preserve a valid FakeTLS success path while changing the emitted shape on the wire.
|
||||
|
||||
## Validation targets
|
||||
|
||||
- Correct count of emulated `ChangeCipherSpec` records.
|
||||
- Correct replay of observed ticket-tail record sizes.
|
||||
- No regression in existing ALPN and payload-placement behavior.
|
||||
|
||||
## How to validate the result
|
||||
|
||||
Recommended validation consists of two layers:
|
||||
|
||||
- focused unit and security tests for CCS-count replay and ticket-tail replay;
|
||||
- real packet-capture comparison for a selected origin and a successful FakeTLS session.
|
||||
|
||||
When testing on the network, the expected result is:
|
||||
|
||||
- a valid FakeTLS and MTProto success path is preserved;
|
||||
- the early encrypted server flight changes shape when richer profile data is available;
|
||||
- the change is visible on the wire without changing MTProto logic or transport architecture.
|
||||
|
||||
This validation is intended to show better reuse of captured TLS profile data.
|
||||
It is not intended to prove byte-level equivalence with the real origin server.
|
||||
|
||||
## How to test on a real deployment
|
||||
|
||||
The strongest practical validation is a side-by-side trace comparison between:
|
||||
|
||||
- a real TLS origin server used as `mask_host`;
|
||||
- a Telemt FakeTLS success-path connection for the same SNI;
|
||||
- optional captures from different Telemt builds or configurations.
|
||||
|
||||
The purpose of the comparison is to inspect the shape of the server flight:
|
||||
|
||||
- record order;
|
||||
- count of `ChangeCipherSpec` records;
|
||||
- count and grouping of early encrypted `ApplicationData` records;
|
||||
- lengths of tail or continuation `ApplicationData` records.
|
||||
|
||||
## Recommended environment
|
||||
|
||||
Use a Linux host or Docker container for the cleanest reproduction.
|
||||
|
||||
Recommended setup:
|
||||
|
||||
1. One Telemt instance.
|
||||
2. One real HTTPS origin as `mask_host`.
|
||||
3. One Telegram client configured with an `ee` proxy link for the Telemt instance.
|
||||
4. `tcpdump` or Wireshark available for capture analysis.
|
||||
|
||||
## Step-by-step test procedure
|
||||
|
||||
### 1. Prepare the origin
|
||||
|
||||
1. Choose a real HTTPS origin.
|
||||
2. Set both `censorship.tls_domain` and `censorship.mask_host` to that hostname.
|
||||
3. Confirm that a direct TLS request works:
|
||||
|
||||
```bash
|
||||
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
|
||||
```
|
||||
|
||||
### 2. Configure Telemt
|
||||
|
||||
Use a configuration that enables:
|
||||
|
||||
- `censorship.mask = true`
|
||||
- `censorship.tls_emulation = true`
|
||||
- `censorship.mask_host`
|
||||
- `censorship.mask_port`
|
||||
|
||||
Recommended for cleaner testing:
|
||||
|
||||
- keep `censorship.tls_new_session_tickets = 0`, so the result depends primarily on fetched profile data rather than operator-forced synthetic tail records;
|
||||
- keep `censorship.tls_fetch.strict_route = true`, if cleaner provenance for captured profile data is important.
|
||||
|
||||
### 3. Refresh TLS profile data
|
||||
|
||||
1. Start Telemt.
|
||||
2. Let it fetch TLS front profile data for the configured domain.
|
||||
3. If `tls_front_dir` is persisted, confirm that the TLS front cache is populated.
|
||||
|
||||
Persisted cache artifacts are useful, but they are not required if packet captures already demonstrate the runtime result.
|
||||
|
||||
### 4. Capture a direct-origin trace
|
||||
|
||||
From a separate client host, connect directly to the origin:
|
||||
|
||||
```bash
|
||||
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
|
||||
```
|
||||
|
||||
Capture with:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -w origin-direct.pcap host ORIGIN_IP and port 443
|
||||
```
|
||||
|
||||
### 5. Capture a Telemt FakeTLS success-path trace
|
||||
|
||||
Now connect to Telemt with a real Telegram client through an `ee` proxy link that targets the Telemt instance.
|
||||
|
||||
`openssl s_client` is useful for direct-origin capture and fallback sanity checks, but it does not exercise the successful FakeTLS and MTProto path.
|
||||
|
||||
Capture with:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -w telemt-emulated.pcap host TELEMT_IP and port 443
|
||||
```
|
||||
|
||||
### 6. Decode TLS record structure
|
||||
|
||||
Use `tshark` to print record-level structure:
|
||||
|
||||
```bash
|
||||
tshark -r origin-direct.pcap -Y "tls.record" -T fields \
|
||||
-e frame.number \
|
||||
-e ip.src \
|
||||
-e ip.dst \
|
||||
-e tls.record.content_type \
|
||||
-e tls.record.length
|
||||
```
|
||||
|
||||
```bash
|
||||
tshark -r telemt-emulated.pcap -Y "tls.record" -T fields \
|
||||
-e frame.number \
|
||||
-e ip.src \
|
||||
-e ip.dst \
|
||||
-e tls.record.content_type \
|
||||
-e tls.record.length
|
||||
```
|
||||
|
||||
Focus on the server flight after ClientHello:
|
||||
|
||||
- `22` = Handshake
|
||||
- `20` = ChangeCipherSpec
|
||||
- `23` = ApplicationData
|
||||
|
||||
### 7. Build a comparison table
|
||||
|
||||
A compact table like the following is usually enough:
|
||||
|
||||
| Path | CCS count | AppData count in first encrypted flight | Tail AppData lengths |
|
||||
| --- | --- | --- | --- |
|
||||
| Origin | `N` | `M` | `[a, b, ...]` |
|
||||
| Telemt build A | `...` | `...` | `...` |
|
||||
| Telemt build B | `...` | `...` | `...` |
|
||||
|
||||
The comparison should make it easy to see that:
|
||||
|
||||
- the FakeTLS success path remains valid;
|
||||
- the early encrypted server flight changes when richer profile data is reused;
|
||||
- the result is backed by packet evidence.
|
||||
|
||||
## Example capture set
|
||||
|
||||
One practical example of this workflow uses:
|
||||
|
||||
- `origin-direct-nginx.pcap`
|
||||
- `telemt-ee-before-nginx.pcap`
|
||||
- `telemt-ee-after-nginx.pcap`
|
||||
|
||||
Practical notes:
|
||||
|
||||
- `origin` was captured as a direct TLS 1.2 connection to `nginx.org`;
|
||||
- `before` and `after` were captured on the Telemt FakeTLS success path with a real Telegram client;
|
||||
- the first server-side FakeTLS response remains valid in both cases;
|
||||
- the early encrypted server-flight segmentation differs between `before` and `after`, which is consistent with better reuse of captured profile data;
|
||||
- this kind of result shows a wire-visible effect without breaking the success path, but it does not claim full indistinguishability from the origin.
|
||||
|
||||
## Stronger validation
|
||||
|
||||
For broader confidence, repeat the same comparison on:
|
||||
|
||||
1. one CDN-backed origin;
|
||||
2. one regular nginx origin;
|
||||
3. one origin with a multi-record encrypted flight and visible ticket-like tails.
|
||||
|
||||
If the same directional improvement appears across all three, confidence in the result will be much higher than for a single-origin example.
|
||||
@@ -0,0 +1,225 @@
|
||||
# Fidelity TLS Front Profile
|
||||
|
||||
## Обзор
|
||||
|
||||
Этот документ описывает, как Telemt переиспользует захваченное TLS-поведение в FakeTLS server flight и как проверять результат на реальной инсталляции.
|
||||
|
||||
Когда включена TLS front emulation, Telemt может собирать полезное серверное TLS-поведение выбранного origin и использовать его в emulated success path. Цель здесь не в побайтном копировании origin, а в уменьшении устойчивых synthetic признаков и в том, чтобы emitted server flight был структурно ближе к захваченному profile.
|
||||
|
||||
## Зачем нужно это изменение
|
||||
|
||||
Проект уже умеет собирать полезное серверное TLS-поведение в пути TLS front fetch:
|
||||
|
||||
- `change_cipher_spec_count`
|
||||
- `app_data_record_sizes`
|
||||
- `ticket_record_sizes`
|
||||
|
||||
До этого изменения эмулятор использовал только часть этой информации. Из-за этого оставался разрыв между захваченным поведением origin и тем FakeTLS server flight, который реально уходил на провод.
|
||||
|
||||
## Что реализовано
|
||||
|
||||
- Эмулятор теперь воспроизводит наблюдаемое значение `ChangeCipherSpec` из полученного `behavior_profile`.
|
||||
- Эмулятор теперь воспроизводит наблюдаемые размеры ticket-like tail ApplicationData records, когда доступны raw или merged TLS profile data.
|
||||
- Эмулятор теперь сохраняет больше структуры профилированного encrypted flight, а не схлопывает его в более маленькую synthetic форму.
|
||||
- Для профилей без raw TLS behavior по-прежнему сохраняется прежний synthetic fallback.
|
||||
- Операторский `tls_new_session_tickets` по-прежнему работает как дополнительный fallback, если профиль не даёт достаточного количества tail records.
|
||||
|
||||
## Практическая польза
|
||||
|
||||
- Снижается различимость между профилированным origin TLS-поведением и эмулируемым TLS-поведением.
|
||||
- Уменьшается шанс устойчивых server-flight fingerprint, вызванных фиксированным CCS count или полностью synthetic tail record sizes.
|
||||
- Уже собранные TLS profile data используются лучше, без изменения MTProto logic, KDF routing или transport architecture.
|
||||
|
||||
## Ограничения
|
||||
|
||||
Этот механизм не ставит целью сделать Telemt побайтно идентичным origin server.
|
||||
|
||||
Он также не меняет:
|
||||
|
||||
- MTProto business logic;
|
||||
- поведение KDF routing;
|
||||
- общую transport architecture.
|
||||
|
||||
Практическая цель уже:
|
||||
|
||||
- использовать больше уже собранных profile data;
|
||||
- уменьшить fixed synthetic behavior в server flight;
|
||||
- сохранить валидный FakeTLS success path, одновременно меняя форму emitted traffic на проводе.
|
||||
|
||||
## Цели валидации
|
||||
|
||||
- Корректное количество эмулируемых `ChangeCipherSpec` records.
|
||||
- Корректное воспроизведение наблюдаемых ticket-tail record sizes.
|
||||
- Отсутствие регрессии в существующем ALPN и payload-placement behavior.
|
||||
|
||||
## Как проверять результат
|
||||
|
||||
Рекомендуемая валидация состоит из двух слоёв:
|
||||
|
||||
- focused unit и security tests для CCS-count replay и ticket-tail replay;
|
||||
- сравнение реальных packet capture для выбранного origin и успешной FakeTLS session.
|
||||
|
||||
При проверке на сети ожидаемый результат такой:
|
||||
|
||||
- валидный FakeTLS и MTProto success path сохраняется;
|
||||
- форма раннего encrypted server flight меняется, когда доступно более богатое profile data;
|
||||
- изменение видно на проводе без изменения MTProto logic и transport architecture.
|
||||
|
||||
Такая проверка нужна для подтверждения того, что уже собранные TLS profile data используются лучше.
|
||||
Она не предназначена для доказательства побайтной эквивалентности с реальным origin server.
|
||||
|
||||
## Как проверить на реальной инсталляции
|
||||
|
||||
Самая сильная практическая проверка — side-by-side trace comparison между:
|
||||
|
||||
- реальным TLS origin server, используемым как `mask_host`;
|
||||
- Telemt FakeTLS success-path connection для того же SNI;
|
||||
- при необходимости capture от разных Telemt builds или configurations.
|
||||
|
||||
Смысл сравнения состоит в том, чтобы посмотреть на форму server flight:
|
||||
|
||||
- порядок records;
|
||||
- количество `ChangeCipherSpec` records;
|
||||
- количество и группировку ранних encrypted `ApplicationData` records;
|
||||
- размеры tail или continuation `ApplicationData` records.
|
||||
|
||||
## Рекомендуемое окружение
|
||||
|
||||
Для самой чистой проверки лучше использовать Linux host или Docker container.
|
||||
|
||||
Рекомендуемый setup:
|
||||
|
||||
1. Один экземпляр Telemt.
|
||||
2. Один реальный HTTPS origin как `mask_host`.
|
||||
3. Один Telegram client, настроенный на `ee` proxy link для Telemt instance.
|
||||
4. `tcpdump` или Wireshark для анализа capture.
|
||||
|
||||
## Пошаговая процедура проверки
|
||||
|
||||
### 1. Подготовить origin
|
||||
|
||||
1. Выберите реальный HTTPS origin.
|
||||
2. Установите и `censorship.tls_domain`, и `censorship.mask_host` в hostname этого origin.
|
||||
3. Убедитесь, что прямой TLS request работает:
|
||||
|
||||
```bash
|
||||
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
|
||||
```
|
||||
|
||||
### 2. Настроить Telemt
|
||||
|
||||
Используйте config, где включены:
|
||||
|
||||
- `censorship.mask = true`
|
||||
- `censorship.tls_emulation = true`
|
||||
- `censorship.mask_host`
|
||||
- `censorship.mask_port`
|
||||
|
||||
Для более чистой проверки рекомендуется:
|
||||
|
||||
- держать `censorship.tls_new_session_tickets = 0`, чтобы результат в первую очередь зависел от fetched profile data, а не от операторских synthetic tail records;
|
||||
- держать `censorship.tls_fetch.strict_route = true`, если важна более чистая provenance для captured profile data.
|
||||
|
||||
### 3. Обновить TLS profile data
|
||||
|
||||
1. Запустите Telemt.
|
||||
2. Дайте ему получить TLS front profile data для выбранного домена.
|
||||
3. Если `tls_front_dir` хранится persistently, убедитесь, что TLS front cache заполнен.
|
||||
|
||||
Persisted cache artifacts полезны, но не обязательны, если packet capture уже показывают runtime result.
|
||||
|
||||
### 4. Снять direct-origin trace
|
||||
|
||||
С отдельной клиентской машины подключитесь напрямую к origin:
|
||||
|
||||
```bash
|
||||
openssl s_client -connect ORIGIN_IP:443 -servername YOUR_DOMAIN </dev/null
|
||||
```
|
||||
|
||||
Capture:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -w origin-direct.pcap host ORIGIN_IP and port 443
|
||||
```
|
||||
|
||||
### 5. Снять Telemt FakeTLS success-path trace
|
||||
|
||||
Теперь подключитесь к Telemt через реальный Telegram client с `ee` proxy link, который указывает на Telemt instance.
|
||||
|
||||
`openssl s_client` полезен для direct-origin capture и для fallback sanity checks, но он не проходит успешный FakeTLS и MTProto path.
|
||||
|
||||
Capture:
|
||||
|
||||
```bash
|
||||
sudo tcpdump -i any -w telemt-emulated.pcap host TELEMT_IP and port 443
|
||||
```
|
||||
|
||||
### 6. Декодировать структуру TLS records
|
||||
|
||||
Используйте `tshark`, чтобы вывести record-level structure:
|
||||
|
||||
```bash
|
||||
tshark -r origin-direct.pcap -Y "tls.record" -T fields \
|
||||
-e frame.number \
|
||||
-e ip.src \
|
||||
-e ip.dst \
|
||||
-e tls.record.content_type \
|
||||
-e tls.record.length
|
||||
```
|
||||
|
||||
```bash
|
||||
tshark -r telemt-emulated.pcap -Y "tls.record" -T fields \
|
||||
-e frame.number \
|
||||
-e ip.src \
|
||||
-e ip.dst \
|
||||
-e tls.record.content_type \
|
||||
-e tls.record.length
|
||||
```
|
||||
|
||||
Смотрите на server flight после ClientHello:
|
||||
|
||||
- `22` = Handshake
|
||||
- `20` = ChangeCipherSpec
|
||||
- `23` = ApplicationData
|
||||
|
||||
### 7. Собрать сравнительную таблицу
|
||||
|
||||
Обычно достаточно короткой таблицы такого вида:
|
||||
|
||||
| Path | CCS count | AppData count in first encrypted flight | Tail AppData lengths |
|
||||
| --- | --- | --- | --- |
|
||||
| Origin | `N` | `M` | `[a, b, ...]` |
|
||||
| Telemt build A | `...` | `...` | `...` |
|
||||
| Telemt build B | `...` | `...` | `...` |
|
||||
|
||||
По такой таблице должно быть легко увидеть, что:
|
||||
|
||||
- FakeTLS success path остаётся валидным;
|
||||
- ранний encrypted server flight меняется, когда переиспользуется более богатое profile data;
|
||||
- результат подтверждён packet evidence.
|
||||
|
||||
## Пример набора capture
|
||||
|
||||
Один практический пример такой проверки использует:
|
||||
|
||||
- `origin-direct-nginx.pcap`
|
||||
- `telemt-ee-before-nginx.pcap`
|
||||
- `telemt-ee-after-nginx.pcap`
|
||||
|
||||
Практические замечания:
|
||||
|
||||
- `origin` снимался как прямое TLS 1.2 connection к `nginx.org`;
|
||||
- `before` и `after` снимались на Telemt FakeTLS success path с реальным Telegram client;
|
||||
- первый server-side FakeTLS response остаётся валидным в обоих случаях;
|
||||
- сегментация раннего encrypted server flight отличается между `before` и `after`, что согласуется с лучшим использованием captured profile data;
|
||||
- такой результат показывает заметный эффект на проводе без поломки success path, но не заявляет полной неотличимости от origin.
|
||||
|
||||
## Более сильная валидация
|
||||
|
||||
Для более широкой проверки повторите ту же процедуру ещё на:
|
||||
|
||||
1. одном CDN-backed origin;
|
||||
2. одном regular nginx origin;
|
||||
3. одном origin с multi-record encrypted flight и заметными ticket-like tails.
|
||||
|
||||
Если одно и то же направление улучшения повторится на всех трёх, уверенность в результате будет значительно выше, чем для одного origin example.
|
||||
|
Before Width: | Height: | Size: 650 KiB After Width: | Height: | Size: 650 KiB |
|
Before Width: | Height: | Size: 838 KiB After Width: | Height: | Size: 838 KiB |
File diff suppressed because it is too large
Load Diff
3094
docs/Config_params/CONFIG_PARAMS.ru.md
Normal file
3094
docs/Config_params/CONFIG_PARAMS.ru.md
Normal file
File diff suppressed because it is too large
Load Diff
172
docs/FAQ.en.md
172
docs/FAQ.en.md
@@ -1,5 +1,4 @@
|
||||
## How to set up a "proxy sponsor" channel and statistics via the @MTProxybot
|
||||
|
||||
1. Go to the @MTProxybot.
|
||||
2. Enter the `/newproxy` command.
|
||||
3. Send your server's IP address and port. For example: `1.2.3.4:443`.
|
||||
@@ -32,23 +31,161 @@ use_middle_proxy = true
|
||||
hello = "ad_tag"
|
||||
hello2 = "ad_tag2"
|
||||
```
|
||||
## Recognizability for DPI and crawler
|
||||
|
||||
## Why do you need a middle proxy (ME)
|
||||
On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS,
|
||||
based on the ECH extension and the ordering of cipher suites,
|
||||
as well as an overall unique JA3/JA4 fingerprint
|
||||
that does not occur in modern browsers.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> TLS fingerprint has been fixed in latest version of clients for Desktop / Android / iOS.
|
||||
> Please update your client for MTProxy Fake-TLS to work correctly.
|
||||
|
||||
- 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
|
||||
- Here is our evidence:
|
||||
- 212.220.88.77 - "dummy" host, running `telemt`
|
||||
- `petrovich.ru` - `tls` + `masking` host, in HEX: `706574726f766963682e7275`
|
||||
- **No MITM + No Fake Certificates/Crypto** = pure transparent *TCP Splice* to "best" upstream: MTProxy or tls/mask-host:
|
||||
- DPI see legitimate HTTPS to `tls_host`, including *valid chain-of-trust* and entropy
|
||||
- Crawlers completely satisfied receiving responses from `mask_host`
|
||||
### Client WITH secret-key accesses the MTProxy resource:
|
||||
|
||||
<img width="360" height="439" alt="telemt" src="https://github.com/user-attachments/assets/39352afb-4a11-4ecc-9d91-9e8cfb20607d" />
|
||||
|
||||
### Client WITHOUT secret-key gets transparent access to the specified resource:
|
||||
- with trusted certificate
|
||||
- with original handshake
|
||||
- with full request-response way
|
||||
- with low-latency overhead
|
||||
```bash
|
||||
root@debian:~/telemt# curl -v -I --resolve petrovich.ru:443:212.220.88.77 https://petrovich.ru/
|
||||
* Added petrovich.ru:443:212.220.88.77 to DNS cache
|
||||
* Hostname petrovich.ru was found in DNS cache
|
||||
* Trying 212.220.88.77:443...
|
||||
* Connected to petrovich.ru (212.220.88.77) port 443 (#0)
|
||||
* ALPN: offers h2,http/1.1
|
||||
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
|
||||
* CAfile: /etc/ssl/certs/ca-certificates.crt
|
||||
* CApath: /etc/ssl/certs
|
||||
* TLSv1.3 (IN), TLS handshake, Server hello (2):
|
||||
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
|
||||
* TLSv1.3 (IN), TLS handshake, Certificate (11):
|
||||
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
|
||||
* TLSv1.3 (IN), TLS handshake, Finished (20):
|
||||
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
|
||||
* TLSv1.3 (OUT), TLS handshake, Finished (20):
|
||||
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
|
||||
* ALPN: server did not agree on a protocol. Uses default.
|
||||
* Server certificate:
|
||||
* subject: C=RU; ST=Saint Petersburg; L=Saint Petersburg; O=STD Petrovich; CN=*.petrovich.ru
|
||||
* start date: Jan 28 11:21:01 2025 GMT
|
||||
* expire date: Mar 1 11:21:00 2026 GMT
|
||||
* subjectAltName: host "petrovich.ru" matched cert's "petrovich.ru"
|
||||
* issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018
|
||||
* SSL certificate verify ok.
|
||||
* using HTTP/1.x
|
||||
> HEAD / HTTP/1.1
|
||||
> Host: petrovich.ru
|
||||
> User-Agent: curl/7.88.1
|
||||
> Accept: */*
|
||||
>
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* old SSL session ID is stale, removing
|
||||
< HTTP/1.1 200 OK
|
||||
HTTP/1.1 200 OK
|
||||
< Server: Variti/0.9.3a
|
||||
Server: Variti/0.9.3a
|
||||
< Date: Thu, 01 Jan 2026 00:0000 GMT
|
||||
Date: Thu, 01 Jan 2026 00:0000 GMT
|
||||
< Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Origin: *
|
||||
< Content-Type: text/html
|
||||
Content-Type: text/html
|
||||
< Cache-Control: no-store
|
||||
Cache-Control: no-store
|
||||
< Expires: Thu, 01 Jan 2026 00:0000 GMT
|
||||
Expires: Thu, 01 Jan 2026 00:0000 GMT
|
||||
< Pragma: no-cache
|
||||
Pragma: no-cache
|
||||
< Set-Cookie: ipp_uid=XXXXX/XXXXX/XXXXX==; Expires=Tue, 31 Dec 2040 23:59:59 GMT; Domain=.petrovich.ru; Path=/
|
||||
Set-Cookie: ipp_uid=XXXXX/XXXXX/XXXXX==; Expires=Tue, 31 Dec 2040 23:59:59 GMT; Domain=.petrovich.ru; Path=/
|
||||
< Content-Type: text/html
|
||||
Content-Type: text/html
|
||||
< Content-Length: 31253
|
||||
Content-Length: 31253
|
||||
< Connection: keep-alive
|
||||
Connection: keep-alive
|
||||
< Keep-Alive: timeout=60
|
||||
Keep-Alive: timeout=60
|
||||
|
||||
<
|
||||
* Connection #0 to host petrovich.ru left intact
|
||||
|
||||
```
|
||||
- We challenged ourselves, we kept trying and we didn't only *beat the air*: now, we have something to show you
|
||||
- Do not just take our word for it? - This is great and we respect that: you can build your own `telemt` or download a build and check it right now
|
||||
|
||||
|
||||
## F.A.Q.
|
||||
|
||||
### Telegram Calls via MTProxy
|
||||
- Telegram architecture **does NOT allow calls via MTProxy**, but only via SOCKS5, which cannot be obfuscated
|
||||
|
||||
### How does DPI see MTProxy TLS?
|
||||
- DPI sees MTProxy in Fake TLS (ee) mode as TLS 1.3
|
||||
- the SNI you specify sends both the client and the server;
|
||||
- ALPN is similar to HTTP 1.1/2;
|
||||
- high entropy, which is normal for AES-encrypted traffic;
|
||||
|
||||
### Whitelist on IP
|
||||
- MTProxy cannot work when there is:
|
||||
- no IP connectivity to the target host: Russian Whitelist on Mobile Networks - "Белый список"
|
||||
- OR all TCP traffic is blocked
|
||||
- OR high entropy/encrypted traffic is blocked: content filters at universities and critical infrastructure
|
||||
- OR all TLS traffic is blocked
|
||||
- OR specified port is blocked: use 443 to make it "like real"
|
||||
- OR provided SNI is blocked: use "officially approved"/innocuous name
|
||||
- like most protocols on the Internet;
|
||||
- these situations are observed:
|
||||
- in China behind the Great Firewall
|
||||
- in Russia on mobile networks, less in wired networks
|
||||
- in Iran during "activity"
|
||||
|
||||
### Why do you need a middle proxy (ME)
|
||||
https://github.com/telemt/telemt/discussions/167
|
||||
|
||||
## How clients interact with Telegram DCs
|
||||
When you register a Telegram account, it gets permanently bound to one of Telegram's data centers (DCs).
|
||||
It is deciced beforehand by Telegram based on the phone number's region.
|
||||
This DC becomes your **home DC**: all content you upload (photos, videos, files, messages) is stored there.
|
||||
Your client authenticates on it with every connection.
|
||||
|
||||
## How many people can use one link
|
||||
For example, if your account is registered on **DC2**, your client will always connect to DC2 first.
|
||||
When you open a chat with another user whose home DC is **DC5**, your client opens an additional connection to DC5 to download their media.
|
||||
Those cross-DC requests are normal and happen constantly.
|
||||
|
||||
> [!WARNING]
|
||||
> Because every session is anchored to your home DC, an outage there causes other DCs to be unavaliable.
|
||||
> If your home DC is DC2 and DC2 goes down, you **cannot** reach DC5 even though DC5 itself is perfectly healthy.
|
||||
> The client has no valid session to route the request through.
|
||||
|
||||
This is also why an MTProxy only needs to reach Telegram's DC infrastructure as a whole.
|
||||
The proxy itself doesn't care which DC your account lives on. The client negotiates the correct DC through the proxy after connecting.
|
||||
|
||||
### How many people can use one link
|
||||
By default, an unlimited number of people can use a single link.
|
||||
However, you can limit the number of unique IP addresses for each user:
|
||||
```toml
|
||||
[access.user_max_unique_ips]
|
||||
hello = 1
|
||||
```
|
||||
This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect. At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network).
|
||||
|
||||
## How to create multiple different links
|
||||
This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect.
|
||||
At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network).
|
||||
|
||||
### How to create multiple different links
|
||||
1. Generate the required number of secrets using the command: `openssl rand -hex 16`.
|
||||
2. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
||||
3. Add new users to the `[access.users]` section:
|
||||
@@ -64,7 +201,7 @@ user3 = "00000000000000000000000000000003"
|
||||
curl -s http://127.0.0.1:9091/v1/users | jq
|
||||
```
|
||||
|
||||
## "Unknown TLS SNI" error
|
||||
### "Unknown TLS SNI" error
|
||||
Usually, this error occurs if you have changed the `tls_domain` parameter, but users continue to connect using old links with the previous domain.
|
||||
|
||||
If you need to allow connections with any domains (ignoring SNI mismatches), add the following parameters:
|
||||
@@ -73,7 +210,7 @@ If you need to allow connections with any domains (ignoring SNI mismatches), add
|
||||
unknown_sni_action = "mask"
|
||||
```
|
||||
|
||||
## How to view metrics
|
||||
### How to view metrics
|
||||
|
||||
1. Open the configuration file: `nano /etc/telemt/telemt.toml`.
|
||||
2. Add the following parameters:
|
||||
@@ -87,6 +224,25 @@ metrics_whitelist = ["127.0.0.1/32", "::1/128", "0.0.0.0/0"]
|
||||
> [!WARNING]
|
||||
> The value `"0.0.0.0/0"` in `metrics_whitelist` opens access to metrics from any IP address. It is recommended to replace it with your personal IP, for example: `"1.2.3.4/32"`.
|
||||
|
||||
### Too many open files
|
||||
- On a fresh Linux install the default open file limit is low; under load `telemt` may fail with `Accept error: Too many open files`
|
||||
- **Systemd**: add `LimitNOFILE=65536` to the `[Service]` section (already included in the example above)
|
||||
- **Docker**: add `--ulimit nofile=65536:65536` to your `docker run` command, or in `docker-compose.yml`:
|
||||
```yaml
|
||||
ulimits:
|
||||
nofile:
|
||||
soft: 65536
|
||||
hard: 65536
|
||||
```
|
||||
- **System-wide** (optional): add to `/etc/security/limits.conf`:
|
||||
```
|
||||
* soft nofile 1048576
|
||||
* hard nofile 1048576
|
||||
root soft nofile 1048576
|
||||
root hard nofile 1048576
|
||||
```
|
||||
|
||||
|
||||
## Additional parameters
|
||||
|
||||
### Domain in the link instead of IP
|
||||
|
||||
172
docs/FAQ.ru.md
172
docs/FAQ.ru.md
@@ -32,10 +32,164 @@ use_middle_proxy = true
|
||||
hello = "ad_tag"
|
||||
hello2 = "ad_tag2"
|
||||
```
|
||||
## Распознаваемость для DPI и сканеров
|
||||
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
|
||||
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Проблема с TLS отпечатком исправлена в последних версиях клиентов Telegram для Desktop / Android / iOS.
|
||||
> Обновите свой клиент для корректной работы с MTProxy Fake-TLS!
|
||||
|
||||
- Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов;
|
||||
- Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом;
|
||||
- Вот наши доказательства:
|
||||
- 212.220.88.77 — «фиктивный» хост, на котором запущен `telemt`;
|
||||
- `petrovich.ru` — хост с `tls` + `masking`, в HEX: `706574726f766963682e7275`;
|
||||
- **Без MITM + без поддельных сертификатов/шифрования** = чистое прозрачное *TCP Splice* к «лучшему» исходному серверу: MTProxy или tls/mask-host:
|
||||
- DPI видит легитимный HTTPS к `tls_host`, включая *достоверную цепочку доверия* и энтропию;
|
||||
- Краулеры полностью удовлетворены получением ответов от `mask_host`.
|
||||
|
||||
### Клиент С секретным ключом получает доступ к ресурсу MTProxy:
|
||||
|
||||
<img width="360" height="439" alt="telemt" src="https://github.com/user-attachments/assets/39352afb-4a11-4ecc-9d91-9e8cfb20607d" />
|
||||
|
||||
### Клиент БЕЗ секретного ключа получает прозрачный доступ к указанному ресурсу:
|
||||
- с доверенным сертификатом;
|
||||
- с исходным «рукопожатием»;
|
||||
- с полным циклом запрос-ответ;
|
||||
- с низкой задержкой.
|
||||
|
||||
```bash
|
||||
root@debian:~/telemt# curl -v -I --resolve petrovich.ru:443:212.220.88.77 https://petrovich.ru/
|
||||
* Added petrovich.ru:443:212.220.88.77 to DNS cache
|
||||
* Hostname petrovich.ru was found in DNS cache
|
||||
* Trying 212.220.88.77:443...
|
||||
* Connected to petrovich.ru (212.220.88.77) port 443 (#0)
|
||||
* ALPN: offers h2,http/1.1
|
||||
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
|
||||
* CAfile: /etc/ssl/certs/ca-certificates.crt
|
||||
* CApath: /etc/ssl/certs
|
||||
* TLSv1.3 (IN), TLS handshake, Server hello (2):
|
||||
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
|
||||
* TLSv1.3 (IN), TLS handshake, Certificate (11):
|
||||
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
|
||||
* TLSv1.3 (IN), TLS handshake, Finished (20):
|
||||
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
|
||||
* TLSv1.3 (OUT), TLS handshake, Finished (20):
|
||||
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
|
||||
* ALPN: server did not agree on a protocol. Uses default.
|
||||
* Server certificate:
|
||||
* subject: C=RU; ST=Saint Petersburg; L=Saint Petersburg; O=STD Petrovich; CN=*.petrovich.ru
|
||||
* start date: Jan 28 11:21:01 2025 GMT
|
||||
* expire date: Mar 1 11:21:00 2026 GMT
|
||||
* subjectAltName: host "petrovich.ru" matched cert's "petrovich.ru"
|
||||
* issuer: C=BE; O=GlobalSign nv-sa; CN=GlobalSign RSA OV SSL CA 2018
|
||||
* SSL certificate verify ok.
|
||||
* using HTTP/1.x
|
||||
> HEAD / HTTP/1.1
|
||||
> Host: petrovich.ru
|
||||
> User-Agent: curl/7.88.1
|
||||
> Accept: */*
|
||||
>
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
|
||||
* old SSL session ID is stale, removing
|
||||
< HTTP/1.1 200 OK
|
||||
HTTP/1.1 200 OK
|
||||
< Server: Variti/0.9.3a
|
||||
Server: Variti/0.9.3a
|
||||
< Date: Thu, 01 Jan 2026 00:0000 GMT
|
||||
Date: Thu, 01 Jan 2026 00:0000 GMT
|
||||
< Access-Control-Allow-Origin: *
|
||||
Access-Control-Allow-Origin: *
|
||||
< Content-Type: text/html
|
||||
Content-Type: text/html
|
||||
< Cache-Control: no-store
|
||||
Cache-Control: no-store
|
||||
< Expires: Thu, 01 Jan 2026 00:0000 GMT
|
||||
Expires: Thu, 01 Jan 2026 00:0000 GMT
|
||||
< Pragma: no-cache
|
||||
Pragma: no-cache
|
||||
< Set-Cookie: ipp_uid=XXXXX/XXXXX/XXXXX==; Expires=Tue, 31 Dec 2040 23:59:59 GMT; Domain=.petrovich.ru; Path=/
|
||||
Set-Cookie: ipp_uid=XXXXX/XXXXX/XXXXX==; Expires=Tue, 31 Dec 2040 23:59:59 GMT; Domain=.petrovich.ru; Path=/
|
||||
< Content-Type: text/html
|
||||
Content-Type: text/html
|
||||
< Content-Length: 31253
|
||||
Content-Length: 31253
|
||||
< Connection: keep-alive
|
||||
Connection: keep-alive
|
||||
< Keep-Alive: timeout=60
|
||||
Keep-Alive: timeout=60
|
||||
|
||||
<
|
||||
* Connection #0 to host petrovich.ru left intact
|
||||
|
||||
```
|
||||
- Мы поставили перед собой задачу, не сдавались и не просто «бились в пустоту»: теперь у нас есть что вам показать.
|
||||
- Не верите нам на слово? — Это прекрасно, и мы уважаем ваше решение: вы можете собрать свой собственный `telemt` или скачать готовую сборку и проверить её прямо сейчас.
|
||||
|
||||
### Звонки в Telegram через MTProxy
|
||||
- Архитектура Telegram **НЕ поддерживает звонки через MTProxy**, а только через SOCKS5, который невозможно замаскировать
|
||||
|
||||
### Как DPI распознает TLS-соединение MTProxy?
|
||||
- DPI распознает MTProxy в режиме Fake TLS (ee) как TLS 1.3
|
||||
- указанный вами SNI отправляется как клиентом, так и сервером;
|
||||
- ALPN аналогичен HTTP 1.1/2;
|
||||
- высокая энтропия, что нормально для трафика, зашифрованного AES;
|
||||
|
||||
### Белый список по IP
|
||||
- MTProxy не может работать, если:
|
||||
- отсутствует IP-связь с целевым хостом: российский белый список в мобильных сетях — «Белый список»;
|
||||
- ИЛИ весь TCP-трафик заблокирован;
|
||||
- ИЛИ трафик с высокой энтропией/зашифрованный трафик заблокирован: контент-фильтры в университетах и критически важной инфраструктуре;
|
||||
- ИЛИ весь TLS-трафик заблокирован;
|
||||
- ИЛИ заблокирован указанный порт: используйте 443, чтобы сделать его «как настоящий»;
|
||||
- ИЛИ заблокирован предоставленный SNI: используйте «официально одобренное»/безобидное имя;
|
||||
- как и большинство протоколов в Интернете;
|
||||
- такие ситуации наблюдаются:
|
||||
- в Китае за Великим файрволом;
|
||||
- в России в мобильных сетях, реже в проводных сетях;
|
||||
- в Иране во время «активности».
|
||||
|
||||
|
||||
## Зачем нужен middle proxy (ME)
|
||||
https://github.com/telemt/telemt/discussions/167
|
||||
|
||||
## Как клиенты взаимодействуют с дата-центрами Telegram
|
||||
При регистрации аккаунта Telegram он навсегда привязывается к одному из дата-центров (DC).
|
||||
Telegram заранее определяет к какому DC привязать аккаунт исходя из региона, к которому относиться номер телефона.
|
||||
Этот DC становится вашим **домашним**: именно там хранится весь контент, который вы загружаете (фото, видео, файлы, сообщения).
|
||||
И именно на нем клиент авторизуется при каждом подключении.
|
||||
|
||||
Например, если ваш аккаунт зарегистрирован на **DC2**, клиент всегда будет подключаться в первую очередь к DC2.
|
||||
Когда вы открываете переписку с пользователем, чей домашний DC — **DC5**, клиент устанавливает доп. соединение с DC5, чтобы загрузить его контент.
|
||||
Такие кросс-запросы к DC — это нормальная часть работы Telegram.
|
||||
|
||||
> [!WARNING]
|
||||
> Поскольку аккаунт всегда привязан к домашнему DC, при его падении контент с других DC будет недоступен.
|
||||
> Если ваш домашний DC — DC2, и DC2 лежит, вы **не сможете** достучаться и до DC5, даже если сам DC5 полностью исправен.
|
||||
> У клиента просто нет валидной сессии, через которую можно было бы направить запрос.
|
||||
|
||||
По той же причине MTProxy достаточно иметь доступ к инфраструктуре Telegram в целом.
|
||||
Cамому MTProxy всё равно, на каком DC живёт ваш аккаунт. Клиент cам договаривается о нужном DC через прокси уже после подключения.
|
||||
|
||||
## Что такое dd и ee в контексте MTProxy?
|
||||
|
||||
Это два разных режима работы прокси. Понять, какой режим используется, можно взглянув на начало секрета — там будет dd или ee, вот пример:
|
||||
tg://proxy?server=s1.dimasssss.space&port=443&secret=eebe3007e927acd147dde12bee8b1a7c9364726976652e676f6f676c652e636f6d
|
||||
|
||||
dd — режим с мусорным трафиком, обфускацией данных, похожий на shadowsocks. У такого трафика есть заметный паттерн, который DPI умеют распознавать и впоследствии блокировать. Использовать этот режим на текущий момент не рекомендуется.
|
||||
|
||||
ee — режим маскировки под существующий домен (FakeTLS), словно вы сёрфите в интернете через браузер. На текущий момент не попадает под блокировку.
|
||||
|
||||
### Где эти режимы настраиваются?
|
||||
|
||||
```toml
|
||||
В конфиге telemt.toml в разделе [general.modes]:
|
||||
classic = false # классический режим, давно стал бесполезным
|
||||
secure = false # переменная dd-режима
|
||||
tls = true # переменная ee-режима
|
||||
```
|
||||
|
||||
## Сколько человек может пользоваться одной ссылкой
|
||||
|
||||
@@ -104,7 +258,7 @@ max_connections = 10000 # 0 - без ограничений, 10000 - по у
|
||||
```
|
||||
|
||||
### Upstream Manager
|
||||
Для настройки исходящих подключений (апстримов) добавьте соответствующие параметры в секцию `[[upstreams]]` файла конфигурации:
|
||||
Для настройки исходящих подключений (Upstreams) добавьте соответствующие параметры в секцию `[[upstreams]]` файла конфигурации:
|
||||
|
||||
#### Привязка к исходящему IP-адресу
|
||||
```toml
|
||||
@@ -119,20 +273,20 @@ interface = "192.168.1.100" # Замените на ваш исходящий IP
|
||||
- Без авторизации:
|
||||
```toml
|
||||
[[upstreams]]
|
||||
type = "socks5" # Specify SOCKS4 or SOCKS5
|
||||
address = "1.2.3.4:1234" # SOCKS-server Address
|
||||
weight = 1 # Set Weight for Scenarios
|
||||
type = "socks5" # выбор типа SOCKS4 или SOCKS5
|
||||
address = "1.2.3.4:1234" # адрес сервера SOCKS
|
||||
weight = 1 # вес
|
||||
enabled = true
|
||||
```
|
||||
|
||||
- С авторизацией:
|
||||
```toml
|
||||
[[upstreams]]
|
||||
type = "socks5" # Specify SOCKS4 or SOCKS5
|
||||
address = "1.2.3.4:1234" # SOCKS-server Address
|
||||
username = "user" # Username for Auth on SOCKS-server
|
||||
password = "pass" # Password for Auth on SOCKS-server
|
||||
weight = 1 # Set Weight for Scenarios
|
||||
type = "socks5" # выбор типа SOCKS4 или SOCKS5
|
||||
address = "1.2.3.4:1234" # адрес сервера SOCKS
|
||||
username = "user" # имя пользователя
|
||||
password = "pass" # пароль
|
||||
weight = 1 # вес
|
||||
enabled = true
|
||||
```
|
||||
|
||||
|
||||
@@ -27,7 +27,8 @@ cargo build --release
|
||||
./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
|
||||
|
||||
@@ -1,3 +1,46 @@
|
||||
# Installation Options
|
||||
There are three options for installing Telemt:
|
||||
- [Automated installation using a script](#very-quick-start).
|
||||
- [Manual installation of Telemt as a service](#telemt-via-systemd).
|
||||
- [Installation using Docker Compose](#telemt-via-docker-compose).
|
||||
|
||||
# Very quick start
|
||||
|
||||
### One-command installation / update on re-run
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
|
||||
```
|
||||
|
||||
After starting, the script will prompt for:
|
||||
- Your language (1 - English, 2 - Russian);
|
||||
- Your TLS domain (press Enter for petrovich.ru).
|
||||
|
||||
The script checks if the port (default **443**) is free. If the port is already in use, installation will fail. You need to free up the port or use the **-p** flag with a different port to retry the installation.
|
||||
|
||||
To modify the script’s startup parameters, you can use the following flags:
|
||||
- **-d, --domain** - TLS domain;
|
||||
- **-p, --port** - server port (1–65535);
|
||||
- **-s, --secret** - 32 hex secret;
|
||||
- **-a, --ad-tag** - ad_tag;
|
||||
- **-l, --lan**g - language (1/en or 2/ru);
|
||||
|
||||
Providing all options skips interactive prompts.
|
||||
|
||||
After completion, the script will provide a link for client connections:
|
||||
```bash
|
||||
tg://proxy?server=IP&port=PORT&secret=SECRET
|
||||
```
|
||||
|
||||
### Installing a specific version
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
|
||||
```
|
||||
|
||||
### Uninstall with full cleanup
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- purge
|
||||
```
|
||||
|
||||
# Telemt via Systemd
|
||||
|
||||
## Installation
|
||||
@@ -62,28 +105,60 @@ nano /etc/telemt/telemt.toml
|
||||
Insert your configuration:
|
||||
|
||||
```toml
|
||||
### Telemt Based Config.toml
|
||||
# We believe that these settings are sufficient for most scenarios
|
||||
# where cutting-egde methods and parameters or special solutions are not needed
|
||||
|
||||
# === General Settings ===
|
||||
[general]
|
||||
use_middle_proxy = true
|
||||
# Global ad_tag fallback when user has no per-user tag in [access.user_ad_tags]
|
||||
# ad_tag = "00000000000000000000000000000000"
|
||||
use_middle_proxy = false
|
||||
# Per-user ad_tag in [access.user_ad_tags] (32 hex from @MTProxybot)
|
||||
|
||||
# === Log Level ===
|
||||
# Log level: debug | verbose | normal | silent
|
||||
# Can be overridden with --silent or --log-level CLI flags
|
||||
# RUST_LOG env var takes absolute priority over all of these
|
||||
log_level = "normal"
|
||||
|
||||
[general.modes]
|
||||
classic = false
|
||||
secure = false
|
||||
tls = true
|
||||
|
||||
[general.links]
|
||||
show = "*"
|
||||
# show = ["alice", "bob"] # Only show links for alice and bob
|
||||
# show = "*" # Show links for all users
|
||||
# public_host = "proxy.example.com" # Host (IP or domain) for tg:// links
|
||||
# public_port = 443 # Port for tg:// links (default: server.port)
|
||||
|
||||
# === Server Binding ===
|
||||
[server]
|
||||
port = 443
|
||||
# proxy_protocol = false # Enable if behind HAProxy/nginx with PROXY protocol
|
||||
# metrics_port = 9090
|
||||
# metrics_listen = "127.0.0.1:9090" # Listen address for metrics (overrides metrics_port)
|
||||
# metrics_whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
|
||||
[server.api]
|
||||
enabled = true
|
||||
# listen = "127.0.0.1:9091"
|
||||
# whitelist = ["127.0.0.1/32"]
|
||||
# read_only = true
|
||||
listen = "127.0.0.1:9091"
|
||||
whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
minimal_runtime_enabled = false
|
||||
minimal_runtime_cache_ttl_ms = 1000
|
||||
|
||||
# Listen on multiple interfaces/IPs - IPv4
|
||||
[[server.listeners]]
|
||||
ip = "0.0.0.0"
|
||||
|
||||
# === Anti-Censorship & Masking ===
|
||||
[censorship]
|
||||
tls_domain = "petrovich.ru"
|
||||
tls_domain = "petrovich.ru" # Fake-TLS / SNI masking domain used in generated ee-links
|
||||
mask = true
|
||||
tls_emulation = true # Fetch real cert lengths and emulate TLS records
|
||||
tls_front_dir = "tlsfront" # Cache directory for TLS emulation
|
||||
|
||||
[access.users]
|
||||
# format: "username" = "32_hex_chars_secret"
|
||||
@@ -93,9 +168,9 @@ hello = "00000000000000000000000000000000"
|
||||
then Ctrl+S -> Ctrl+X to save
|
||||
|
||||
> [!WARNING]
|
||||
> Replace the value of the hello parameter with the value you obtained in step 0.
|
||||
> Additionally, change the value of the tls_domain parameter to a different website.
|
||||
> Changing the tls_domain parameter will break all links that use the old domain!
|
||||
> Replace the value of the `hello` parameter with the value you obtained in step 0.
|
||||
> Additionally, change the value of the `tls_domain` parameter to a different website.
|
||||
> Changing the `tls_domain` parameter will break all links that use the old domain!
|
||||
|
||||
---
|
||||
|
||||
@@ -1,4 +1,46 @@
|
||||
# Telemt через Systemd
|
||||
# Варианты установки
|
||||
Имеется три варианта установки Telemt:
|
||||
- [Автоматизированная установка с помощью скрипта](#очень-быстрый-старт).
|
||||
- [Ручная установка Telemt в качестве службы](#telemt-через-systemd-вручную).
|
||||
- [Установка через Docker Compose](#telemt-через-docker-compose).
|
||||
|
||||
# Очень быстрый старт
|
||||
|
||||
### Установка одной командой / обновление при повторном запуске
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
|
||||
```
|
||||
После запуска скрипт запросит:
|
||||
- ваш язык (1 - English, 2 - Русский);
|
||||
- ваш TLS-домен (нажмите Enter для petrovich.ru).
|
||||
|
||||
Во время установки скрипт проверяет, свободен ли порт (по умолчанию **443**). Если порт занят другим процессом - установка завершится с ошибкой. Для повторной установки необходимо освободить порт или указать другой через флаг **-p**.
|
||||
|
||||
Для изменения параметров запуска скрипта можно использовать следующие флаги:
|
||||
- **-d, --domain** - TLS-домен;
|
||||
- **-p, --port** - порт (1–65535);
|
||||
- **-s, --secret** - секрет (32 hex символа);
|
||||
- **-a, --ad-tag** - ad_tag;
|
||||
- **-l, --lang** - язык (1/en или 2/ru).
|
||||
|
||||
Если заданы флаги для языка и домена, интерактивных вопросов не будет.
|
||||
|
||||
После завершения установки скрипт выдаст ссылку для подключения клиентов:
|
||||
```bash
|
||||
tg://proxy?server=IP&port=PORT&secret=SECRET
|
||||
```
|
||||
|
||||
### Установка нужной версии
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
|
||||
```
|
||||
|
||||
### Удаление с полной очисткой
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- purge
|
||||
```
|
||||
|
||||
# Telemt через Systemd вручную
|
||||
|
||||
## Установка
|
||||
|
||||
@@ -62,40 +104,72 @@ nano /etc/telemt/telemt.toml
|
||||
Вставьте свою конфигурацию
|
||||
|
||||
```toml
|
||||
# === General Settings ===
|
||||
### Конфигурационный файл на основе Telemt
|
||||
# Мы полагаем, что этих настроек достаточно для большинства сценариев,
|
||||
# где не требуются передовые методы, параметры или специальные решения
|
||||
|
||||
# === Общие настройки ===
|
||||
[general]
|
||||
use_middle_proxy = true
|
||||
# Глобальный ad_tag, если у пользователя нет индивидуального тега в [access.user_ad_tags]
|
||||
# ad_tag = "00000000000000000000000000000000"
|
||||
use_middle_proxy = false
|
||||
# Индивидуальный ad_tag в [access.user_ad_tags] (32 шестнадцатеричных символа от @MTProxybot)
|
||||
|
||||
# === Уровень логирования ===
|
||||
# Уровень логирования: debug | verbose | normal | silent
|
||||
# Можно переопределить с помощью флагов командной строки --silent или --log-level
|
||||
# Переменная окружения RUST_LOG имеет абсолютный приоритет над всеми этими настройками
|
||||
log_level = "normal"
|
||||
|
||||
[general.modes]
|
||||
classic = false
|
||||
secure = false
|
||||
tls = true
|
||||
|
||||
[general.links]
|
||||
show = "*"
|
||||
# show = ["alice", "bob"] # Показывать ссылки только для alice и bob
|
||||
# show = "*" # Показывать ссылки для всех пользователей
|
||||
# public_host = "proxy.example.com" # Хост (IP-адрес или домен) для ссылок tg://
|
||||
# public_port = 443 # Порт для ссылок tg:// (по умолчанию: server.port)
|
||||
|
||||
# === Привязка сервера ===
|
||||
[server]
|
||||
port = 443
|
||||
# proxy_protocol = false # Включите, если сервер находится за HAProxy/nginx с протоколом PROXY
|
||||
# metrics_port = 9090
|
||||
# metrics_listen = "127.0.0.1:9090" # Адрес прослушивания для метрик (переопределяет metrics_port)
|
||||
# metrics_whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
|
||||
[server.api]
|
||||
enabled = true
|
||||
# listen = "127.0.0.1:9091"
|
||||
# whitelist = ["127.0.0.1/32"]
|
||||
# read_only = true
|
||||
listen = "127.0.0.1:9091"
|
||||
whitelist = ["127.0.0.1/32", "::1/128"]
|
||||
minimal_runtime_enabled = false
|
||||
minimal_runtime_cache_ttl_ms = 1000
|
||||
|
||||
# === Anti-Censorship & Masking ===
|
||||
# Прослушивание на нескольких интерфейсах/IP-адресах - IPv4
|
||||
[[server.listeners]]
|
||||
ip = "0.0.0.0"
|
||||
|
||||
# === Обход блокировок и маскировка ===
|
||||
[censorship]
|
||||
tls_domain = "petrovich.ru"
|
||||
tls_domain = "petrovich.ru" # Домен Fake-TLS / SNI, который будет использоваться в сгенерированных ee-ссылках
|
||||
mask = true
|
||||
tls_emulation = true # Получить реальную длину сертификата и эмулировать запись TLS
|
||||
tls_front_dir = "tlsfront" # Директория кэша для эмуляции TLS
|
||||
|
||||
[access.users]
|
||||
# format: "username" = "32_hex_chars_secret"
|
||||
# формат: "имя_пользователя" = "секрет_из_32_шестнадцатеричных_символов"
|
||||
hello = "00000000000000000000000000000000"
|
||||
```
|
||||
|
||||
Затем нажмите Ctrl+S -> Ctrl+X, чтобы сохранить
|
||||
|
||||
> [!WARNING]
|
||||
> Замените значение параметра hello на значение, которое вы получили в пункте 0.
|
||||
> Так же замените значение параметра tls_domain на другой сайт.
|
||||
> Изменение параметра tls_domain сделает нерабочими все ссылки, использующие старый домен!
|
||||
> Замените значение параметра `hello` на значение, которое вы получили в пункте 0.
|
||||
> Так же замените значение параметра `tls_domain` на другой сайт.
|
||||
> Изменение параметра `tls_domain` сделает нерабочими все ссылки, использующие старый домен!
|
||||
|
||||
---
|
||||
|
||||
@@ -163,7 +163,7 @@ PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
|
||||
---
|
||||
|
||||
## Step 2. Installing telemt on Server B (conditionally Netherlands)
|
||||
Installation and configuration are described [here](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_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`.
|
||||
|
||||
In the telemt config, you must enable the `Proxy` protocol and restrict connections to it only through the tunnel.
|
||||
@@ -166,7 +166,7 @@ PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
|
||||
|
||||
## Шаг 2. Установка telemt на Сервере B (_условно Нидерланды_)
|
||||
|
||||
Установка и настройка описаны [здесь](https://github.com/telemt/telemt/blob/main/docs/QUICK_START_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 необходимо включить протокол `Proxy` и ограничить подключения к нему только через туннель.
|
||||
273
docs/Setup_examples/XRAY_DOUBLE_HOP.en.md
Normal file
273
docs/Setup_examples/XRAY_DOUBLE_HOP.en.md
Normal 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 8
|
||||
# Save the output (e.g.: abc123def456) — this is <SHORT_ID>
|
||||
```
|
||||
4. **Random Path (for xhttp):**
|
||||
```bash
|
||||
openssl rand -hex 16
|
||||
# Save the output (e.g., 0123456789abcdef0123456789abcdef) to replace <YOUR_RANDOM_PATH> in configs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Configuration for Server B (_EU_):
|
||||
|
||||
Create or edit the file `/usr/local/etc/xray/config.json`.
|
||||
This Xray instance will listen on the public `443` port and proxy valid Reality traffic, while routing "disguised" traffic (e.g., direct web browser scans) to `yahoo.com`.
|
||||
|
||||
```bash
|
||||
nano /usr/local/etc/xray/config.json
|
||||
```
|
||||
|
||||
File content:
|
||||
```json
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "error",
|
||||
"access": "none"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "vless-in",
|
||||
"port": 443,
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
"id": "<XRAY_UUID>"
|
||||
}
|
||||
],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "xhttp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"dest": "yahoo.com:443",
|
||||
"serverNames": [
|
||||
"yahoo.com"
|
||||
],
|
||||
"privateKey": "<SERVER_B_PRIVATE_KEY>",
|
||||
"shortIds": [
|
||||
"<SHORT_ID>"
|
||||
]
|
||||
},
|
||||
"xhttpSettings": {
|
||||
"path": "/<YOUR_RANDOM_PATH>",
|
||||
"mode": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "tunnel-to-telemt",
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"destination": "127.0.0.1:8443"
|
||||
}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": [
|
||||
"vless-in"
|
||||
],
|
||||
"outboundTag": "tunnel-to-telemt"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Open the firewall port (if enabled):
|
||||
```bash
|
||||
sudo ufw allow 443/tcp
|
||||
```
|
||||
Restart and setup Xray to run at boot:
|
||||
```bash
|
||||
sudo systemctl restart xray
|
||||
sudo systemctl enable xray
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Configuration for Server A (_RU_):
|
||||
|
||||
Similarly, edit `/usr/local/etc/xray/config.json`.
|
||||
Here Xray acts as the public entry point: it listens on `443\tcp`, uses a local loopback (via internal port `10444`) to prepend the `PROXYv2` header, and encapsulates the payload via Reality to Server B, instructing Server B to deliver it to its *local* `127.0.0.1:8443` port (where telemt will listen).
|
||||
|
||||
```bash
|
||||
nano /usr/local/etc/xray/config.json
|
||||
```
|
||||
|
||||
File content:
|
||||
```json
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "error",
|
||||
"access": "none"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "public-in",
|
||||
"port": 443,
|
||||
"listen": "0.0.0.0",
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1",
|
||||
"port": 10444,
|
||||
"network": "tcp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "tunnel-in",
|
||||
"port": 10444,
|
||||
"listen": "127.0.0.1",
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1",
|
||||
"port": 8443,
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "local-injector",
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"proxyProtocol": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "vless-out",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "<PUBLIC_IP_SERVER_B>",
|
||||
"port": 443,
|
||||
"users": [
|
||||
{
|
||||
"id": "<XRAY_UUID>",
|
||||
"encryption": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "xhttp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"serverName": "yahoo.com",
|
||||
"publicKey": "<SERVER_B_PUBLIC_KEY>",
|
||||
"shortId": "<SHORT_ID>",
|
||||
"spiderX": "/",
|
||||
"fingerprint": "chrome"
|
||||
},
|
||||
"xhttpSettings": {
|
||||
"path": "/<YOUR_RANDOM_PATH>"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["public-in"],
|
||||
"outboundTag": "local-injector"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["tunnel-in"],
|
||||
"outboundTag": "vless-out"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
*Replace `<PUBLIC_IP_SERVER_B>` with the public IP address of Server B.*
|
||||
|
||||
Open the firewall port for clients (if enabled):
|
||||
```bash
|
||||
sudo ufw allow 443/tcp
|
||||
```
|
||||
|
||||
Restart and setup Xray to run at boot:
|
||||
```bash
|
||||
sudo systemctl restart xray
|
||||
sudo systemctl enable xray
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2. Install telemt on Server B (_EU_)
|
||||
|
||||
telemt installation is heavily covered in the [Quick Start Guide](../Quick_start/QUICK_START_GUIDE.en.md).
|
||||
By contrast to standard setups, telemt must listen strictly _locally_ (since Xray occupies the public `443` interface) and must expect `PROXYv2` packets.
|
||||
|
||||
Edit the configuration file (`config.toml`) on Server B accordingly:
|
||||
|
||||
```toml
|
||||
[server]
|
||||
port = 8443
|
||||
listen_addr_ipv4 = "127.0.0.1"
|
||||
proxy_protocol = true
|
||||
|
||||
[general.links]
|
||||
show = "*"
|
||||
public_host = "<FQDN_OR_IP_SERVER_A>"
|
||||
public_port = 443
|
||||
```
|
||||
|
||||
- Address `127.0.0.1` and `port = 8443` instructs the core proxy router to process connections unpacked locally via Xray-server.
|
||||
- `proxy_protocol = true` commands telemt to parse the injected PROXY header (from Server A's Xray local loopback) and log genuine end-user IPs.
|
||||
- Under `public_host`, place Server A's public IP address or FQDN to ensure working links are generated for Telegram users.
|
||||
|
||||
Restart `telemt`. Your server is now robust against DPI scanners, passing traffic optimally.
|
||||
|
||||
272
docs/Setup_examples/XRAY_DOUBLE_HOP.ru.md
Normal file
272
docs/Setup_examples/XRAY_DOUBLE_HOP.ru.md
Normal 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 8
|
||||
# Сохраните вывод (например: abc123def456) — это <SHORT_ID>
|
||||
```
|
||||
4. **Random Path (путь для xhttp):**
|
||||
```bash
|
||||
openssl rand -hex 16
|
||||
# Сохраните вывод (например, 0123456789abcdef0123456789abcdef), чтобы заменить <YOUR_RANDOM_PATH> в конфигах
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Конфигурация Сервера B (_Нидерланды_):
|
||||
|
||||
Создаем или редактируем файл `/usr/local/etc/xray/config.json`.
|
||||
Этот Xray-сервер будет слушать порт `443` и прозрачно пропускать валидный Reality трафик дальше, а "замаскированный" трафик (например, если кто-то стучится в лоб веб-браузером) пойдет на `yahoo.com`.
|
||||
|
||||
```bash
|
||||
nano /usr/local/etc/xray/config.json
|
||||
```
|
||||
|
||||
Содержимое файла:
|
||||
```json
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "error",
|
||||
"access": "none"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "vless-in",
|
||||
"port": 443,
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [
|
||||
{
|
||||
"id": "<XRAY_UUID>"
|
||||
}
|
||||
],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "xhttp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"dest": "yahoo.com:443",
|
||||
"serverNames": [
|
||||
"yahoo.com"
|
||||
],
|
||||
"privateKey": "<SERVER_B_PRIVATE_KEY>",
|
||||
"shortIds": [
|
||||
"<SHORT_ID>"
|
||||
]
|
||||
},
|
||||
"xhttpSettings": {
|
||||
"path": "/<YOUR_RANDOM_PATH>",
|
||||
"mode": "auto"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "tunnel-to-telemt",
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"destination": "127.0.0.1:8443"
|
||||
}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": [
|
||||
"vless-in"
|
||||
],
|
||||
"outboundTag": "tunnel-to-telemt"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Открываем порт на фаерволе (если включен):
|
||||
```bash
|
||||
sudo ufw allow 443/tcp
|
||||
```
|
||||
Перезапускаем Xray:
|
||||
```bash
|
||||
sudo systemctl restart xray
|
||||
sudo systemctl enable xray
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Конфигурация Сервера A (_РФ_):
|
||||
|
||||
Аналогично, редактируем `/usr/local/etc/xray/config.json`.
|
||||
Здесь Xray выступает публичной точкой: он принимает трафик на внешний порт `443\tcp`, пропускает через локальный loopback (порт `10444`) для добавления PROXYv2-заголовка, и упаковывает в Reality до Сервера B, прося тот доставить данные на *свой локальный* порт `127.0.0.1:8443` (именно там будет слушать telemt).
|
||||
|
||||
```bash
|
||||
nano /usr/local/etc/xray/config.json
|
||||
```
|
||||
|
||||
Содержимое файла:
|
||||
```json
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "error",
|
||||
"access": "none"
|
||||
},
|
||||
"inbounds": [
|
||||
{
|
||||
"tag": "public-in",
|
||||
"port": 443,
|
||||
"listen": "0.0.0.0",
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1",
|
||||
"port": 10444,
|
||||
"network": "tcp"
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "tunnel-in",
|
||||
"port": 10444,
|
||||
"listen": "127.0.0.1",
|
||||
"protocol": "dokodemo-door",
|
||||
"settings": {
|
||||
"address": "127.0.0.1",
|
||||
"port": 8443,
|
||||
"network": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "local-injector",
|
||||
"protocol": "freedom",
|
||||
"settings": {
|
||||
"proxyProtocol": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"tag": "vless-out",
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"vnext": [
|
||||
{
|
||||
"address": "<PUBLIC_IP_SERVER_B>",
|
||||
"port": 443,
|
||||
"users": [
|
||||
{
|
||||
"id": "<XRAY_UUID>",
|
||||
"encryption": "none"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "xhttp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"serverName": "yahoo.com",
|
||||
"publicKey": "<SERVER_B_PUBLIC_KEY>",
|
||||
"shortId": "<SHORT_ID>",
|
||||
"spiderX": "/",
|
||||
"fingerprint": "chrome"
|
||||
},
|
||||
"xhttpSettings": {
|
||||
"path": "/<YOUR_RANDOM_PATH>"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"routing": {
|
||||
"domainStrategy": "AsIs",
|
||||
"rules": [
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["public-in"],
|
||||
"outboundTag": "local-injector"
|
||||
},
|
||||
{
|
||||
"type": "field",
|
||||
"inboundTag": ["tunnel-in"],
|
||||
"outboundTag": "vless-out"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
*Замените `<PUBLIC_IP_SERVER_B>` на внешний IP-адрес Сервера B.*
|
||||
|
||||
Открываем порт на фаерволе для клиентов:
|
||||
```bash
|
||||
sudo ufw allow 443/tcp
|
||||
```
|
||||
|
||||
Перезапускаем Xray:
|
||||
```bash
|
||||
sudo systemctl restart xray
|
||||
sudo systemctl enable xray
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Шаг 2. Установка и настройка telemt на Сервере B (_Нидерланды_)
|
||||
|
||||
Установка telemt описана [в основной инструкции](../Quick_start/QUICK_START_GUIDE.ru.md).
|
||||
Отличие в том, что telemt должен слушать *внутренний* порт (так как 443 занят Xray-сервером), а также ожидать `PROXY` протокол из Xray туннеля.
|
||||
|
||||
В конфиге `config.toml` прокси (на Сервере B) укажите:
|
||||
```toml
|
||||
[server]
|
||||
port = 8443
|
||||
listen_addr_ipv4 = "127.0.0.1"
|
||||
proxy_protocol = true
|
||||
|
||||
[general.links]
|
||||
show = "*"
|
||||
public_host = "<FQDN_OR_IP_SERVER_A>"
|
||||
public_port = 443
|
||||
```
|
||||
|
||||
- `port = 8443` и `listen_addr_ipv4 = "127.0.0.1"` означают, что telemt принимает подключения только изнутри (приходящие от локального Xray-процесса).
|
||||
- `proxy_protocol = true` заставляет telemt парсить PROXYv2-заголовок (который добавил Xray на Сервере A через loopback), восстанавливая IP-адрес конечного пользователя (РФ).
|
||||
- В `public_host` укажите публичный IP-адрес или домен Сервера A, чтобы ссылки на подключение генерировались корректно.
|
||||
|
||||
Перезапустите `telemt`, и клиенты смогут подключаться по выданным ссылкам.
|
||||
|
||||
1
docs/assets/telegram_button.svg
Normal file
1
docs/assets/telegram_button.svg
Normal file
@@ -0,0 +1 @@
|
||||
<?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>
|
||||
|
After Width: | Height: | Size: 804 B |
BIN
docs/assets/telemt.png
Normal file
BIN
docs/assets/telemt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 161 KiB |
482
install.sh
482
install.sh
@@ -21,47 +21,212 @@ PORT_PROVIDED=0
|
||||
SECRET_PROVIDED=0
|
||||
AD_TAG_PROVIDED=0
|
||||
DOMAIN_PROVIDED=0
|
||||
LANG_PROVIDED=0
|
||||
|
||||
ACTION="install"
|
||||
TARGET_VERSION="${VERSION:-latest}"
|
||||
LANG_CHOICE="en"
|
||||
|
||||
set_language() {
|
||||
case "$1" in
|
||||
ru)
|
||||
L_ERR_DOMAIN_REQ="требует аргумент (домен)."
|
||||
L_ERR_PORT_REQ="требует аргумент (порт)."
|
||||
L_ERR_PORT_NUM="Порт должен быть числом."
|
||||
L_ERR_PORT_RANGE="Порт должен быть от 1 до 65535."
|
||||
L_ERR_SECRET_REQ="требует аргумент (секрет)."
|
||||
L_ERR_SECRET_HEX="Секрет должен содержать только HEX символы."
|
||||
L_ERR_SECRET_LEN="Секрет должен состоять ровно из 32 символов."
|
||||
L_ERR_ADTAG_REQ="требует аргумент (ad_tag)."
|
||||
L_ERR_UNKNOWN_OPT="Неизвестная опция:"
|
||||
L_WARN_EXTRA_ARG="Игнорируется лишний аргумент:"
|
||||
L_ERR_REQ_ARG="требует аргумент (1, 2, en или ru)."
|
||||
L_ERR_EMPTY_VAR="не может быть пустым."
|
||||
L_ERR_INV_VER="Недопустимые символы в версии."
|
||||
L_ERR_INV_BIN="Недопустимые символы в BIN_NAME."
|
||||
L_ERR_ROOT="Для работы скрипта требуются права root или sudo."
|
||||
L_ERR_SUDO_TTY="sudo требует пароль, но терминал (TTY) не обнаружен."
|
||||
L_ERR_DIR_CHECK="Ошибка: конфиг является директорией."
|
||||
L_ERR_CMD_NOT_FOUND="Необходимая команда не найдена:"
|
||||
L_ERR_NO_DL_TOOL="Не установлен curl или wget."
|
||||
L_ERR_NO_CP_TOOL="Необходима утилита cp или install."
|
||||
L_WARN_NO_NET_TOOL="Утилиты сети не найдены. Проверка порта пропущена."
|
||||
L_INFO_PORT_IGNORE="Порт занят текущим процессом телеметрии. Игнорируем."
|
||||
L_ERR_PORT_IN_USE="Порт уже занят другим процессом:"
|
||||
L_ERR_PORT_FREE="Освободите порт или укажите другой и попробуйте снова."
|
||||
L_ERR_UNSUP_ARCH="Неподдерживаемая архитектура:"
|
||||
L_ERR_CREATE_GRP="Не удалось создать группу"
|
||||
L_ERR_CREATE_USR="Не удалось создать пользователя"
|
||||
L_ERR_MKDIR="Не удалось создать директории"
|
||||
L_ERR_INSTALL_DIR="не является директорией."
|
||||
L_ERR_BIN_INSTALL="Не удалось установить бинарный файл"
|
||||
L_ERR_BIN_COPY="Не удалось скопировать бинарный файл"
|
||||
L_ERR_BIN_EXEC="Бинарный файл не исполняемый."
|
||||
L_ERR_GEN_SEC="Не удалось сгенерировать секрет."
|
||||
L_INFO_CONF_EXISTS="Конфиг уже существует. Обновление параметров..."
|
||||
L_INFO_UPD_PORT="Обновлен порт:"
|
||||
L_INFO_UPD_SEC="Обновлен секрет для пользователя 'hello'"
|
||||
L_INFO_UPD_DOM="Обновлен tls_domain:"
|
||||
L_INFO_UPD_TAG="Обновлен ad_tag"
|
||||
L_ERR_CONF_INST="Не удалось установить конфиг"
|
||||
L_INFO_CONF_OK="Конфиг успешно создан."
|
||||
L_INFO_CONF_SEC="Настроен секрет для пользователя 'hello':"
|
||||
L_WARN_SVC_FAIL="Не удалось запустить службу"
|
||||
L_INFO_MANUAL_START="Менеджер служб не найден. Запустите вручную:"
|
||||
L_INFO_UNINST_START="Начинается удаление"
|
||||
L_U_STAGE_1=">>> Этап 1: Остановка служб"
|
||||
L_U_STAGE_2=">>> Этап 2: Удаление конфигурации службы"
|
||||
L_U_STAGE_3=">>> Этап 3: Завершение процессов пользователя"
|
||||
L_U_STAGE_4=">>> Этап 4: Удаление бинарного файла"
|
||||
L_U_STAGE_5=">>> Этап 5: Полная очистка (конфиг, данные, пользователь)"
|
||||
L_INFO_KEEP_CONF="Примечание: Конфигурация сохранена. Используйте 'purge' для очистки."
|
||||
L_INFO_I_START="Начинается установка"
|
||||
L_I_STAGE_1=">>> Этап 1: Проверка окружения и зависимостей"
|
||||
L_I_STAGE_1_5=">>> Этап 1.5: Интерактивная настройка"
|
||||
L_I_PROMPT_DOM="\nПожалуйста, укажите домен TLS\nНажмите Enter, чтобы оставить по умолчанию [%s]: "
|
||||
L_WARN_NO_TTY="Интерактивный режим недоступен (нет TTY). Используется:"
|
||||
L_I_STAGE_2=">>> Этап 2: Загрузка архива"
|
||||
L_ERR_TMP_DIR="Не удалось создать временную директорию"
|
||||
L_ERR_TMP_INV="Временная директория недействительна"
|
||||
L_INFO_FALLBACK="Сборка x86_64-v3 не найдена, откат к стандартной x86_64..."
|
||||
L_ERR_DL_FAIL="Ошибка загрузки архива"
|
||||
L_I_STAGE_3=">>> Этап 3: Распаковка архива"
|
||||
L_ERR_EXTRACT="Ошибка распаковки архива."
|
||||
L_ERR_BIN_NOT_FOUND="Бинарный файл не найден в архиве"
|
||||
L_I_STAGE_4=">>> Этап 4: Настройка окружения (Юзер, Группа, Папки)"
|
||||
L_I_STAGE_5=">>> Этап 5: Установка бинарного файла"
|
||||
L_I_STAGE_6=">>> Этап 6: Генерация/Обновление конфигурации"
|
||||
L_I_STAGE_7=">>> Этап 7: Установка и запуск службы"
|
||||
L_OUT_WARN_H="УСТАНОВКА ЗАВЕРШЕНА С ПРЕДУПРЕЖДЕНИЯМИ"
|
||||
L_OUT_WARN_D="Служба установлена, но не запустилась.\nПожалуйста, проверьте логи.\n"
|
||||
L_OUT_SUCC_H="УСТАНОВКА УСПЕШНО ЗАВЕРШЕНА"
|
||||
L_OUT_UNINST_H="УДАЛЕНИЕ ЗАВЕРШЕНО"
|
||||
L_OUT_LINK="Ваша ссылка для подключения к Telegram Proxy:\n"
|
||||
;;
|
||||
*)
|
||||
L_ERR_DOMAIN_REQ="requires a domain argument."
|
||||
L_ERR_PORT_REQ="requires a port argument."
|
||||
L_ERR_PORT_NUM="Port must be a valid number."
|
||||
L_ERR_PORT_RANGE="Port must be between 1 and 65535."
|
||||
L_ERR_SECRET_REQ="requires a secret argument."
|
||||
L_ERR_SECRET_HEX="Secret must contain only hex characters."
|
||||
L_ERR_SECRET_LEN="Secret must be exactly 32 chars."
|
||||
L_ERR_ADTAG_REQ="requires an ad_tag argument."
|
||||
L_ERR_UNKNOWN_OPT="Unknown option:"
|
||||
L_WARN_EXTRA_ARG="Ignoring extra argument:"
|
||||
L_ERR_REQ_ARG="requires an argument (1, 2, en, ru)."
|
||||
L_ERR_EMPTY_VAR="cannot be empty."
|
||||
L_ERR_INV_VER="Invalid characters in version."
|
||||
L_ERR_INV_BIN="Invalid characters in BIN_NAME."
|
||||
L_ERR_ROOT="This script requires root or sudo."
|
||||
L_ERR_SUDO_TTY="sudo requires a password, but no TTY detected."
|
||||
L_ERR_DIR_CHECK="Safety check failed: Config is a directory."
|
||||
L_ERR_CMD_NOT_FOUND="Required command not found:"
|
||||
L_ERR_NO_DL_TOOL="Neither curl nor wget is installed."
|
||||
L_ERR_NO_CP_TOOL="Need cp or install."
|
||||
L_WARN_NO_NET_TOOL="Network tools not found. Skipping port check."
|
||||
L_INFO_PORT_IGNORE="Port is in use by telemt. Ignoring as it will be restarted."
|
||||
L_ERR_PORT_IN_USE="Port is already in use by another process:"
|
||||
L_ERR_PORT_FREE="Please free the port or change it and try again."
|
||||
L_ERR_UNSUP_ARCH="Unsupported architecture:"
|
||||
L_ERR_CREATE_GRP="Cannot create group"
|
||||
L_ERR_CREATE_USR="Cannot create user"
|
||||
L_ERR_MKDIR="Failed to create directories"
|
||||
L_ERR_INSTALL_DIR="is not a directory."
|
||||
L_ERR_BIN_INSTALL="Failed to install binary"
|
||||
L_ERR_BIN_COPY="Failed to copy binary"
|
||||
L_ERR_BIN_EXEC="Binary not executable."
|
||||
L_ERR_GEN_SEC="Failed to generate secret."
|
||||
L_INFO_CONF_EXISTS="Config already exists. Updating parameters..."
|
||||
L_INFO_UPD_PORT="Updated port:"
|
||||
L_INFO_UPD_SEC="Updated secret for user 'hello'"
|
||||
L_INFO_UPD_DOM="Updated tls_domain:"
|
||||
L_INFO_UPD_TAG="Updated ad_tag"
|
||||
L_ERR_CONF_INST="Failed to install config"
|
||||
L_INFO_CONF_OK="Config created successfully."
|
||||
L_INFO_CONF_SEC="Configured secret for user 'hello':"
|
||||
L_WARN_SVC_FAIL="Failed to start service"
|
||||
L_INFO_MANUAL_START="Service manager not found. Start manually:"
|
||||
L_INFO_UNINST_START="Starting uninstallation of"
|
||||
L_U_STAGE_1=">>> Stage 1: Stopping services"
|
||||
L_U_STAGE_2=">>> Stage 2: Removing service configuration"
|
||||
L_U_STAGE_3=">>> Stage 3: Terminating user processes"
|
||||
L_U_STAGE_4=">>> Stage 4: Removing binary"
|
||||
L_U_STAGE_5=">>> Stage 5: Purging configuration, data, and user"
|
||||
L_INFO_KEEP_CONF="Note: Configuration kept. Run with 'purge' to remove completely."
|
||||
L_INFO_I_START="Starting installation of"
|
||||
L_I_STAGE_1=">>> Stage 1: Verifying environment and dependencies"
|
||||
L_I_STAGE_1_5=">>> Stage 1.5: Interactive Setup"
|
||||
L_I_PROMPT_DOM="\nPlease specify the TLS Domain\nPress Enter to keep default [%s]: "
|
||||
L_WARN_NO_TTY="Interactive mode unavailable (no TTY). Using:"
|
||||
L_I_STAGE_2=">>> Stage 2: Downloading archive"
|
||||
L_ERR_TMP_DIR="Temp directory creation failed"
|
||||
L_ERR_TMP_INV="Temp directory is invalid or was not created"
|
||||
L_INFO_FALLBACK="x86_64-v3 build not found, falling back to standard x86_64..."
|
||||
L_ERR_DL_FAIL="Download failed"
|
||||
L_I_STAGE_3=">>> Stage 3: Extracting archive"
|
||||
L_ERR_EXTRACT="Extraction failed."
|
||||
L_ERR_BIN_NOT_FOUND="Binary not found in archive"
|
||||
L_I_STAGE_4=">>> Stage 4: Setting up environment (User, Group, Directories)"
|
||||
L_I_STAGE_5=">>> Stage 5: Installing binary"
|
||||
L_I_STAGE_6=">>> Stage 6: Generating/Updating configuration"
|
||||
L_I_STAGE_7=">>> Stage 7: Installing and starting service"
|
||||
L_OUT_WARN_H="INSTALLATION COMPLETED WITH WARNINGS"
|
||||
L_OUT_WARN_D="The service was installed but failed to start.\nPlease check the logs to determine the issue.\n"
|
||||
L_OUT_SUCC_H="INSTALLATION SUCCESS"
|
||||
L_OUT_UNINST_H="UNINSTALLATION COMPLETE"
|
||||
L_OUT_LINK="Your Telegram Proxy connection link:\n"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
set_language "$LANG_CHOICE"
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
-h|--help) ACTION="help"; shift ;;
|
||||
-l|--lang)
|
||||
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
|
||||
printf '[ERROR] %s %s\n' "$1" "$L_ERR_REQ_ARG" >&2; exit 1
|
||||
fi
|
||||
case "$2" in
|
||||
ru|2) LANG_CHOICE="ru"; set_language "$LANG_CHOICE"; LANG_PROVIDED=1 ;;
|
||||
en|1) LANG_CHOICE="en"; set_language "$LANG_CHOICE"; LANG_PROVIDED=1 ;;
|
||||
*) printf '[ERROR] %s %s\n' "$1" "$L_ERR_REQ_ARG" >&2; exit 1 ;;
|
||||
esac
|
||||
shift 2 ;;
|
||||
-d|--domain)
|
||||
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
|
||||
printf '[ERROR] %s requires a domain argument.\n' "$1" >&2
|
||||
exit 1
|
||||
printf '[ERROR] %s %s\n' "$1" "$L_ERR_DOMAIN_REQ" >&2; exit 1
|
||||
fi
|
||||
TLS_DOMAIN="$2"; DOMAIN_PROVIDED=1; shift 2 ;;
|
||||
-p|--port)
|
||||
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
|
||||
printf '[ERROR] %s requires a port argument.\n' "$1" >&2; exit 1
|
||||
printf '[ERROR] %s %s\n' "$1" "$L_ERR_PORT_REQ" >&2; exit 1
|
||||
fi
|
||||
case "$2" in
|
||||
*[!0-9]*) printf '[ERROR] Port must be a valid number.\n' >&2; exit 1 ;;
|
||||
*[!0-9]*) printf '[ERROR] %s\n' "$L_ERR_PORT_NUM" >&2; exit 1 ;;
|
||||
esac
|
||||
port_num="$(printf '%s\n' "$2" | sed 's/^0*//')"
|
||||
[ -z "$port_num" ] && port_num="0"
|
||||
if [ "${#port_num}" -gt 5 ] || [ "$port_num" -lt 1 ] || [ "$port_num" -gt 65535 ]; then
|
||||
printf '[ERROR] Port must be between 1 and 65535.\n' >&2; exit 1
|
||||
printf '[ERROR] %s\n' "$L_ERR_PORT_RANGE" >&2; exit 1
|
||||
fi
|
||||
SERVER_PORT="$port_num"; PORT_PROVIDED=1; shift 2 ;;
|
||||
-s|--secret)
|
||||
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
|
||||
printf '[ERROR] %s requires a secret argument.\n' "$1" >&2; exit 1
|
||||
printf '[ERROR] %s %s\n' "$1" "$L_ERR_SECRET_REQ" >&2; exit 1
|
||||
fi
|
||||
case "$2" in
|
||||
*[!0-9a-fA-F]*)
|
||||
printf '[ERROR] Secret must contain only hex characters.\n' >&2; exit 1 ;;
|
||||
*[!0-9a-fA-F]*) printf '[ERROR] %s\n' "$L_ERR_SECRET_HEX" >&2; exit 1 ;;
|
||||
esac
|
||||
if [ "${#2}" -ne 32 ]; then
|
||||
printf '[ERROR] Secret must be exactly 32 chars.\n' >&2; exit 1
|
||||
printf '[ERROR] %s\n' "$L_ERR_SECRET_LEN" >&2; exit 1
|
||||
fi
|
||||
USER_SECRET="$2"; SECRET_PROVIDED=1; shift 2 ;;
|
||||
-a|--ad-tag|--ad_tag)
|
||||
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
|
||||
printf '[ERROR] %s requires an ad_tag argument.\n' "$1" >&2; exit 1
|
||||
printf '[ERROR] %s %s\n' "$1" "$L_ERR_ADTAG_REQ" >&2; exit 1
|
||||
fi
|
||||
AD_TAG="$2"; AD_TAG_PROVIDED=1; shift 2 ;;
|
||||
uninstall|--uninstall)
|
||||
@@ -69,14 +234,31 @@ while [ $# -gt 0 ]; do
|
||||
shift ;;
|
||||
purge|--purge) ACTION="purge"; shift ;;
|
||||
install|--install) ACTION="install"; shift ;;
|
||||
-*) printf '[ERROR] Unknown option: %s\n' "$1" >&2; exit 1 ;;
|
||||
-*) printf '[ERROR] %s %s\n' "$L_ERR_UNKNOWN_OPT" "$1" >&2; exit 1 ;;
|
||||
*)
|
||||
if [ "$ACTION" = "install" ]; then TARGET_VERSION="$1"
|
||||
else printf '[WARNING] Ignoring extra argument: %s\n' "$1" >&2; fi
|
||||
else printf '[WARNING] %s %s\n' "$L_WARN_EXTRA_ARG" "$1" >&2; fi
|
||||
shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$ACTION" != "help" ] && [ "$LANG_PROVIDED" -eq 0 ]; then
|
||||
if [ -t 0 ] || [ -c /dev/tty ]; then
|
||||
printf "\nSelect language / Выберите язык:\n"
|
||||
printf " 1) English (default)\n"
|
||||
printf " 2) Русский\n"
|
||||
printf "Your choice / Ваш выбор [1/2]: "
|
||||
read -r input_lang </dev/tty || input_lang=""
|
||||
case "$input_lang" in
|
||||
2) LANG_CHOICE="ru" ;;
|
||||
*) LANG_CHOICE="en" ;;
|
||||
esac
|
||||
else
|
||||
LANG_CHOICE="en"
|
||||
fi
|
||||
set_language "$LANG_CHOICE"
|
||||
fi
|
||||
|
||||
say() {
|
||||
if [ "$#" -eq 0 ] || [ -z "${1:-}" ]; then
|
||||
printf '\n'
|
||||
@@ -96,17 +278,33 @@ cleanup() {
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
show_help() {
|
||||
say "Usage: $0 [ <version> | install | uninstall | purge ] [ options ]"
|
||||
say " <version> Install specific version (e.g. 3.3.15, default: latest)"
|
||||
say " install Install the latest version"
|
||||
say " uninstall Remove the binary and service"
|
||||
say " purge Remove everything including configuration, data, and user"
|
||||
say ""
|
||||
say "Options:"
|
||||
say " -d, --domain Set TLS domain (default: petrovich.ru)"
|
||||
say " -p, --port Set server port (default: 443)"
|
||||
say " -s, --secret Set specific user secret (32 hex characters)"
|
||||
say " -a, --ad-tag Set ad_tag"
|
||||
if [ "$LANG_CHOICE" = "ru" ]; then
|
||||
say "Использование: $0 [ <версия> | install | uninstall | purge ] [ опции ]"
|
||||
say " <версия> Установить конкретную версию (например, 3.3.15, по умолчанию: latest)"
|
||||
say " install Установить последнюю версию"
|
||||
say " uninstall Удалить бинарный файл и службу"
|
||||
say " purge Полностью удалить вместе с конфигурацией, данными и пользователем"
|
||||
say ""
|
||||
say "Опции:"
|
||||
say " -d, --domain Указать домен TLS (по умолчанию: petrovich.ru)"
|
||||
say " -p, --port Указать порт сервера (по умолчанию: 443)"
|
||||
say " -s, --secret Указать секрет пользователя (32 hex символа)"
|
||||
say " -a, --ad-tag Указать ad_tag"
|
||||
say " -l, --lang Выбрать язык вывода (1/en или 2/ru)"
|
||||
else
|
||||
say "Usage: $0 [ <version> | install | uninstall | purge ] [ options ]"
|
||||
say " <version> Install specific version (e.g. 3.3.15, default: latest)"
|
||||
say " install Install the latest version"
|
||||
say " uninstall Remove the binary and service"
|
||||
say " purge Remove everything including configuration, data, and user"
|
||||
say ""
|
||||
say "Options:"
|
||||
say " -d, --domain Set TLS domain (default: petrovich.ru)"
|
||||
say " -p, --port Set server port (default: 443)"
|
||||
say " -s, --secret Set specific user secret (32 hex characters)"
|
||||
say " -a, --ad-tag Set ad_tag"
|
||||
say " -l, --lang Set output language (1/en or 2/ru)"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -171,17 +369,13 @@ is_config_exists() {
|
||||
}
|
||||
|
||||
verify_common() {
|
||||
[ -n "$BIN_NAME" ] || die "BIN_NAME cannot be empty."
|
||||
[ -n "$INSTALL_DIR" ] || die "INSTALL_DIR cannot be empty."
|
||||
[ -n "$CONFIG_DIR" ] || die "CONFIG_DIR cannot be empty."
|
||||
[ -n "$CONFIG_FILE" ] || die "CONFIG_FILE cannot be empty."
|
||||
[ -n "$BIN_NAME" ] || die "BIN_NAME $L_ERR_EMPTY_VAR"
|
||||
[ -n "$INSTALL_DIR" ] || die "INSTALL_DIR $L_ERR_EMPTY_VAR"
|
||||
[ -n "$CONFIG_DIR" ] || die "CONFIG_DIR $L_ERR_EMPTY_VAR"
|
||||
[ -n "$CONFIG_FILE" ] || die "CONFIG_FILE $L_ERR_EMPTY_VAR"
|
||||
|
||||
case "${INSTALL_DIR}${CONFIG_DIR}${WORK_DIR}${CONFIG_FILE}" in
|
||||
*[!a-zA-Z0-9_./-]*) die "Invalid characters in paths." ;;
|
||||
esac
|
||||
|
||||
case "$TARGET_VERSION" in *[!a-zA-Z0-9_.-]*) die "Invalid characters in version." ;; esac
|
||||
case "$BIN_NAME" in *[!a-zA-Z0-9_-]*) die "Invalid characters in BIN_NAME." ;; esac
|
||||
case "$TARGET_VERSION" in *[!a-zA-Z0-9_.-]*) die "$L_ERR_INV_VER" ;; esac
|
||||
case "$BIN_NAME" in *[!a-zA-Z0-9_-]*) die "$L_ERR_INV_BIN" ;; esac
|
||||
|
||||
INSTALL_DIR="$(get_realpath "$INSTALL_DIR")"
|
||||
CONFIG_DIR="$(get_realpath "$CONFIG_DIR")"
|
||||
@@ -195,42 +389,42 @@ verify_common() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
SUDO=""
|
||||
else
|
||||
command -v sudo >/dev/null 2>&1 || die "This script requires root or sudo."
|
||||
command -v sudo >/dev/null 2>&1 || die "$L_ERR_ROOT"
|
||||
SUDO="sudo"
|
||||
if ! sudo -n true 2>/dev/null; then
|
||||
if ! [ -t 0 ]; then
|
||||
die "sudo requires a password, but no TTY detected."
|
||||
die "$L_ERR_SUDO_TTY"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -n "$SUDO" ]; then
|
||||
if $SUDO sh -c '[ -d "$1" ]' _ "$CONFIG_FILE"; then
|
||||
die "Safety check failed: CONFIG_FILE '$CONFIG_FILE' is a directory."
|
||||
die "$L_ERR_DIR_CHECK"
|
||||
fi
|
||||
elif [ -d "$CONFIG_FILE" ]; then
|
||||
die "Safety check failed: CONFIG_FILE '$CONFIG_FILE' is a directory."
|
||||
die "$L_ERR_DIR_CHECK"
|
||||
fi
|
||||
|
||||
for cmd in id uname awk grep find rm chown chmod mv mktemp mkdir tr dd sed ps head sleep cat tar gzip; do
|
||||
command -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd"
|
||||
command -v "$cmd" >/dev/null 2>&1 || die "$L_ERR_CMD_NOT_FOUND $cmd"
|
||||
done
|
||||
}
|
||||
|
||||
verify_install_deps() {
|
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || die "Neither curl nor wget is installed."
|
||||
command -v cp >/dev/null 2>&1 || command -v install >/dev/null 2>&1 || die "Need cp or install"
|
||||
command -v curl >/dev/null 2>&1 || command -v wget >/dev/null 2>&1 || die "$L_ERR_NO_DL_TOOL"
|
||||
command -v cp >/dev/null 2>&1 || command -v install >/dev/null 2>&1 || die "$L_ERR_NO_CP_TOOL"
|
||||
|
||||
if ! command -v setcap >/dev/null 2>&1 || ! command -v conntrack >/dev/null 2>&1; then
|
||||
if ! command -v setcap >/dev/null 2>&1; then
|
||||
if command -v apk >/dev/null 2>&1; then
|
||||
$SUDO apk add --no-cache libcap-utils libcap conntrack-tools >/dev/null 2>&1 || true
|
||||
$SUDO apk add --no-cache libcap-utils libcap >/dev/null 2>&1 || true
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -q libcap2-bin conntrack >/dev/null 2>&1 || {
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -q libcap2-bin >/dev/null 2>&1 || {
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get update -q >/dev/null 2>&1 || true
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -q libcap2-bin conntrack >/dev/null 2>&1 || true
|
||||
$SUDO env DEBIAN_FRONTEND=noninteractive apt-get install -y -q libcap2-bin >/dev/null 2>&1 || true
|
||||
}
|
||||
elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y -q libcap conntrack-tools >/dev/null 2>&1 || true
|
||||
elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y -q libcap conntrack-tools >/dev/null 2>&1 || true
|
||||
elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y -q libcap >/dev/null 2>&1 || true
|
||||
elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y -q libcap >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -245,17 +439,17 @@ check_port_availability() {
|
||||
elif command -v lsof >/dev/null 2>&1; then
|
||||
port_info=$($SUDO lsof -i :${SERVER_PORT} 2>/dev/null | grep LISTEN || true)
|
||||
else
|
||||
say "[WARNING] Network diagnostic tools (ss, netstat, lsof) not found. Skipping port check."
|
||||
say "[WARNING] $L_WARN_NO_NET_TOOL"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "$port_info" ]; then
|
||||
if printf '%s\n' "$port_info" | grep -q "${BIN_NAME}"; then
|
||||
say " -> Port ${SERVER_PORT} is in use by ${BIN_NAME}. Ignoring as it will be restarted."
|
||||
say " -> $L_INFO_PORT_IGNORE"
|
||||
else
|
||||
say "[ERROR] Port ${SERVER_PORT} is already in use by another process:"
|
||||
say "[ERROR] $L_ERR_PORT_IN_USE $SERVER_PORT:"
|
||||
printf ' %s\n' "$port_info"
|
||||
die "Please free the port ${SERVER_PORT} or change it and try again."
|
||||
die "$L_ERR_PORT_FREE"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -271,7 +465,7 @@ detect_arch() {
|
||||
fi
|
||||
;;
|
||||
aarch64|arm64) echo "aarch64" ;;
|
||||
*) die "Unsupported architecture: $sys_arch" ;;
|
||||
*) die "$L_ERR_UNSUP_ARCH $sys_arch" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -295,7 +489,7 @@ ensure_user_group() {
|
||||
if ! check_os_entity group telemt; then
|
||||
if command -v groupadd >/dev/null 2>&1; then $SUDO groupadd -r telemt
|
||||
elif command -v addgroup >/dev/null 2>&1; then $SUDO addgroup -S telemt
|
||||
else die "Cannot create group"; fi
|
||||
else die "$L_ERR_CREATE_GRP" ; fi
|
||||
fi
|
||||
|
||||
if ! check_os_entity passwd telemt; then
|
||||
@@ -307,12 +501,12 @@ ensure_user_group() {
|
||||
else
|
||||
$SUDO adduser --system --home "$WORK_DIR" --shell "$nologin_bin" --no-create-home --ingroup telemt --disabled-password telemt
|
||||
fi
|
||||
else die "Cannot create user"; fi
|
||||
else die "$L_ERR_CREATE_USR"; fi
|
||||
fi
|
||||
}
|
||||
|
||||
setup_dirs() {
|
||||
$SUDO mkdir -p "$WORK_DIR" "$CONFIG_DIR" "$CONFIG_PARENT_DIR" || die "Failed to create directories"
|
||||
$SUDO mkdir -p "$WORK_DIR" "$CONFIG_DIR" "$CONFIG_PARENT_DIR" || die "$L_ERR_MKDIR"
|
||||
|
||||
$SUDO chown telemt:telemt "$WORK_DIR" && $SUDO chmod 750 "$WORK_DIR"
|
||||
$SUDO chown telemt:telemt "$CONFIG_DIR" && $SUDO chmod 750 "$CONFIG_DIR"
|
||||
@@ -334,20 +528,20 @@ stop_service() {
|
||||
install_binary() {
|
||||
bin_src="$1"; bin_dst="$2"
|
||||
if [ -e "$INSTALL_DIR" ] && [ ! -d "$INSTALL_DIR" ]; then
|
||||
die "'$INSTALL_DIR' is not a directory."
|
||||
die "'$INSTALL_DIR' $L_ERR_INSTALL_DIR"
|
||||
fi
|
||||
|
||||
$SUDO mkdir -p "$INSTALL_DIR" || die "Failed to create install directory"
|
||||
$SUDO mkdir -p "$INSTALL_DIR" || die "$L_ERR_MKDIR"
|
||||
|
||||
$SUDO rm -f "$bin_dst" 2>/dev/null || true
|
||||
|
||||
if command -v install >/dev/null 2>&1; then
|
||||
$SUDO install -m 0755 "$bin_src" "$bin_dst" || die "Failed to install binary"
|
||||
$SUDO install -m 0755 "$bin_src" "$bin_dst" || die "$L_ERR_BIN_INSTALL"
|
||||
else
|
||||
$SUDO cp "$bin_src" "$bin_dst" && $SUDO chmod 0755 "$bin_dst" || die "Failed to copy binary"
|
||||
$SUDO cp "$bin_src" "$bin_dst" && $SUDO chmod 0755 "$bin_dst" || die "$L_ERR_BIN_COPY"
|
||||
fi
|
||||
|
||||
$SUDO sh -c '[ -x "$1" ]' _ "$bin_dst" || die "Binary not executable: $bin_dst"
|
||||
$SUDO sh -c '[ -x "$1" ]' _ "$bin_dst" || die "$L_ERR_BIN_EXEC $bin_dst"
|
||||
|
||||
if command -v setcap >/dev/null 2>&1; then
|
||||
$SUDO setcap cap_net_bind_service,cap_net_admin=+ep "$bin_dst" 2>/dev/null || true
|
||||
@@ -404,40 +598,32 @@ EOF
|
||||
|
||||
install_config() {
|
||||
if is_config_exists; then
|
||||
say " -> Config already exists at $CONFIG_FILE. Updating parameters..."
|
||||
say " -> $L_INFO_CONF_EXISTS"
|
||||
|
||||
tmp_conf="${TEMP_DIR}/config.tmp"
|
||||
$SUDO cat "$CONFIG_FILE" > "$tmp_conf"
|
||||
|
||||
escaped_domain="$(printf '%s\n' "$TLS_DOMAIN" | tr -d '[:cntrl:]' | sed 's/\\/\\\\/g; s/"/\\"/g')"
|
||||
|
||||
export AWK_PORT="$SERVER_PORT"
|
||||
export AWK_SECRET="$USER_SECRET"
|
||||
export AWK_DOMAIN="$escaped_domain"
|
||||
export AWK_AD_TAG="$AD_TAG"
|
||||
export AWK_FLAG_P="$PORT_PROVIDED"
|
||||
export AWK_FLAG_S="$SECRET_PROVIDED"
|
||||
export AWK_FLAG_D="$DOMAIN_PROVIDED"
|
||||
export AWK_FLAG_A="$AD_TAG_PROVIDED"
|
||||
|
||||
awk '
|
||||
awk -v port="$SERVER_PORT" -v secret="$USER_SECRET" -v domain="$escaped_domain" -v ad_tag="$AD_TAG" \
|
||||
-v flag_p="$PORT_PROVIDED" -v flag_s="$SECRET_PROVIDED" -v flag_d="$DOMAIN_PROVIDED" -v flag_a="$AD_TAG_PROVIDED" '
|
||||
BEGIN { ad_tag_handled = 0 }
|
||||
|
||||
ENVIRON["AWK_FLAG_P"] == "1" && /^[ \t]*port[ \t]*=/ { print "port = " ENVIRON["AWK_PORT"]; next }
|
||||
ENVIRON["AWK_FLAG_S"] == "1" && /^[ \t]*hello[ \t]*=/ { print "hello = \"" ENVIRON["AWK_SECRET"] "\""; next }
|
||||
ENVIRON["AWK_FLAG_D"] == "1" && /^[ \t]*tls_domain[ \t]*=/ { print "tls_domain = \"" ENVIRON["AWK_DOMAIN"] "\""; next }
|
||||
flag_p == "1" && /^[ \t]*port[ \t]*=/ { print "port = " port; next }
|
||||
flag_s == "1" && /^[ \t]*hello[ \t]*=/ { print "hello = \"" secret "\""; next }
|
||||
flag_d == "1" && /^[ \t]*tls_domain[ \t]*=/ { print "tls_domain = \"" domain "\""; next }
|
||||
|
||||
ENVIRON["AWK_FLAG_A"] == "1" && /^[ \t]*ad_tag[ \t]*=/ {
|
||||
flag_a == "1" && /^[ \t]*ad_tag[ \t]*=/ {
|
||||
if (!ad_tag_handled) {
|
||||
print "ad_tag = \"" ENVIRON["AWK_AD_TAG"] "\"";
|
||||
print "ad_tag = \"" ad_tag "\"";
|
||||
ad_tag_handled = 1;
|
||||
}
|
||||
next
|
||||
}
|
||||
ENVIRON["AWK_FLAG_A"] == "1" && /^\[general\]/ {
|
||||
flag_a == "1" && /^\[general\]/ {
|
||||
print;
|
||||
if (!ad_tag_handled) {
|
||||
print "ad_tag = \"" ENVIRON["AWK_AD_TAG"] "\"";
|
||||
print "ad_tag = \"" ad_tag "\"";
|
||||
ad_tag_handled = 1;
|
||||
}
|
||||
next
|
||||
@@ -446,10 +632,10 @@ install_config() {
|
||||
{ print }
|
||||
' "$tmp_conf" > "${tmp_conf}.new" && mv "${tmp_conf}.new" "$tmp_conf"
|
||||
|
||||
[ "$PORT_PROVIDED" -eq 1 ] && say " -> Updated port: $SERVER_PORT"
|
||||
[ "$SECRET_PROVIDED" -eq 1 ] && say " -> Updated secret for user 'hello'"
|
||||
[ "$DOMAIN_PROVIDED" -eq 1 ] && say " -> Updated tls_domain: $TLS_DOMAIN"
|
||||
[ "$AD_TAG_PROVIDED" -eq 1 ] && say " -> Updated ad_tag"
|
||||
[ "$PORT_PROVIDED" -eq 1 ] && say " -> $L_INFO_UPD_PORT $SERVER_PORT"
|
||||
[ "$SECRET_PROVIDED" -eq 1 ] && say " -> $L_INFO_UPD_SEC"
|
||||
[ "$DOMAIN_PROVIDED" -eq 1 ] && say " -> $L_INFO_UPD_DOM $TLS_DOMAIN"
|
||||
[ "$AD_TAG_PROVIDED" -eq 1 ] && say " -> $L_INFO_UPD_TAG"
|
||||
|
||||
write_root "$CONFIG_FILE" < "$tmp_conf"
|
||||
rm -f "$tmp_conf"
|
||||
@@ -457,14 +643,14 @@ install_config() {
|
||||
fi
|
||||
|
||||
if [ -z "$USER_SECRET" ]; then
|
||||
USER_SECRET="$(generate_secret)" || die "Failed to generate secret."
|
||||
USER_SECRET="$(generate_secret)" || die "$L_ERR_GEN_SEC"
|
||||
fi
|
||||
|
||||
generate_config_content "$USER_SECRET" "$AD_TAG" | write_root "$CONFIG_FILE" || die "Failed to install config"
|
||||
generate_config_content "$USER_SECRET" "$AD_TAG" | write_root "$CONFIG_FILE" || die "$L_ERR_CONF_INST"
|
||||
$SUDO chown root:telemt "$CONFIG_FILE" && $SUDO chmod 640 "$CONFIG_FILE"
|
||||
|
||||
say " -> Config created successfully."
|
||||
say " -> Configured secret for user 'hello': $USER_SECRET"
|
||||
say " -> $L_INFO_CONF_OK"
|
||||
say " -> $L_INFO_CONF_SEC $USER_SECRET"
|
||||
}
|
||||
|
||||
generate_systemd_content() {
|
||||
@@ -517,7 +703,7 @@ install_service() {
|
||||
$SUDO systemctl enable "$SERVICE_NAME" || true
|
||||
|
||||
if ! $SUDO systemctl start "$SERVICE_NAME"; then
|
||||
say "[WARNING] Failed to start service"
|
||||
say "[WARNING] $L_WARN_SVC_FAIL"
|
||||
SERVICE_START_FAILED=1
|
||||
fi
|
||||
elif [ "$svc" = "openrc" ]; then
|
||||
@@ -527,15 +713,15 @@ install_service() {
|
||||
$SUDO rc-update add "$SERVICE_NAME" default 2>/dev/null || true
|
||||
|
||||
if ! $SUDO rc-service "$SERVICE_NAME" start 2>/dev/null; then
|
||||
say "[WARNING] Failed to start service"
|
||||
say "[WARNING] $L_WARN_SVC_FAIL"
|
||||
SERVICE_START_FAILED=1
|
||||
fi
|
||||
else
|
||||
cmd="\"${INSTALL_DIR}/${BIN_NAME}\" \"${CONFIG_FILE}\""
|
||||
if [ -n "$SUDO" ]; then
|
||||
say " -> Service manager not found. Start manually: sudo -u telemt $cmd"
|
||||
say " -> $L_INFO_MANUAL_START sudo -u telemt $cmd"
|
||||
else
|
||||
say " -> Service manager not found. Start manually: su -s /bin/sh telemt -c '$cmd'"
|
||||
say " -> $L_INFO_MANUAL_START su -s /bin/sh telemt -c '$cmd'"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
@@ -566,12 +752,12 @@ kill_user_procs() {
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
say "Starting uninstallation of $BIN_NAME..."
|
||||
say "$L_INFO_UNINST_START $BIN_NAME..."
|
||||
|
||||
say ">>> Stage 1: Stopping services"
|
||||
say "$L_U_STAGE_1"
|
||||
stop_service
|
||||
|
||||
say ">>> Stage 2: Removing service configuration"
|
||||
say "$L_U_STAGE_2"
|
||||
svc="$(get_svc_mgr)"
|
||||
if [ "$svc" = "systemd" ]; then
|
||||
$SUDO systemctl disable "$SERVICE_NAME" 2>/dev/null || true
|
||||
@@ -582,28 +768,30 @@ uninstall() {
|
||||
$SUDO rm -f "/etc/init.d/${SERVICE_NAME}"
|
||||
fi
|
||||
|
||||
say ">>> Stage 3: Terminating user processes"
|
||||
say "$L_U_STAGE_3"
|
||||
kill_user_procs
|
||||
|
||||
say ">>> Stage 4: Removing binary"
|
||||
say "$L_U_STAGE_4"
|
||||
$SUDO rm -f "${INSTALL_DIR}/${BIN_NAME}"
|
||||
|
||||
if [ "$ACTION" = "purge" ]; then
|
||||
say ">>> Stage 5: Purging configuration, data, and user"
|
||||
say "$L_U_STAGE_5"
|
||||
$SUDO rm -rf "$CONFIG_DIR" "$WORK_DIR"
|
||||
$SUDO rm -f "$CONFIG_FILE"
|
||||
sleep 1
|
||||
$SUDO userdel telemt 2>/dev/null || $SUDO deluser telemt 2>/dev/null || true
|
||||
|
||||
if check_os_entity passwd telemt; then
|
||||
$SUDO userdel telemt 2>/dev/null || $SUDO deluser telemt 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if check_os_entity group telemt; then
|
||||
$SUDO groupdel telemt 2>/dev/null || $SUDO delgroup telemt 2>/dev/null || true
|
||||
fi
|
||||
else
|
||||
say "Note: Configuration and user kept. Run with 'purge' to remove completely."
|
||||
say "$L_INFO_KEEP_CONF"
|
||||
fi
|
||||
|
||||
printf '\n====================================================================\n'
|
||||
printf ' UNINSTALLATION COMPLETE\n'
|
||||
printf ' %s\n' "$L_OUT_UNINST_H"
|
||||
printf '====================================================================\n\n'
|
||||
exit 0
|
||||
}
|
||||
@@ -612,21 +800,45 @@ case "$ACTION" in
|
||||
help) show_help ;;
|
||||
uninstall|purge) verify_common; uninstall ;;
|
||||
install)
|
||||
say "Starting installation of $BIN_NAME (Version: $TARGET_VERSION)"
|
||||
say "$L_INFO_I_START $BIN_NAME (Version: $TARGET_VERSION)"
|
||||
|
||||
say ">>> Stage 1: Verifying environment and dependencies"
|
||||
say "$L_I_STAGE_1"
|
||||
verify_common
|
||||
verify_install_deps
|
||||
|
||||
if is_config_exists && [ "$PORT_PROVIDED" -eq 0 ]; then
|
||||
if is_config_exists; then
|
||||
ext_port="$($SUDO awk -F'=' '/^[ \t]*port[ \t]*=/ {gsub(/[^0-9]/, "", $2); print $2; exit}' "$CONFIG_FILE" 2>/dev/null || true)"
|
||||
if [ -n "$ext_port" ]; then
|
||||
if [ -n "$ext_port" ] && [ "$PORT_PROVIDED" -eq 0 ]; then
|
||||
SERVER_PORT="$ext_port"
|
||||
fi
|
||||
|
||||
ext_secret="$($SUDO awk -F'"' '/^[ \t]*hello[ \t]*=/ {print $2; exit}' "$CONFIG_FILE" 2>/dev/null || true)"
|
||||
if [ -n "$ext_secret" ] && [ "$SECRET_PROVIDED" -eq 0 ]; then
|
||||
USER_SECRET="$ext_secret"
|
||||
fi
|
||||
|
||||
ext_domain="$($SUDO awk -F'"' '/^[ \t]*tls_domain[ \t]*=/ {print $2; exit}' "$CONFIG_FILE" 2>/dev/null || true)"
|
||||
if [ -n "$ext_domain" ] && [ "$DOMAIN_PROVIDED" -eq 0 ]; then
|
||||
TLS_DOMAIN="$ext_domain"
|
||||
fi
|
||||
fi
|
||||
|
||||
check_port_availability
|
||||
|
||||
if [ "$DOMAIN_PROVIDED" -eq 0 ]; then
|
||||
say "$L_I_STAGE_1_5"
|
||||
if [ -t 0 ] || [ -c /dev/tty ]; then
|
||||
printf "$L_I_PROMPT_DOM" "$TLS_DOMAIN"
|
||||
read -r input_domain </dev/tty || input_domain=""
|
||||
if [ -n "$input_domain" ]; then
|
||||
TLS_DOMAIN="$input_domain"
|
||||
fi
|
||||
else
|
||||
say "[WARNING] $L_WARN_NO_TTY $TLS_DOMAIN"
|
||||
fi
|
||||
DOMAIN_PROVIDED=1
|
||||
fi
|
||||
|
||||
if [ "$TARGET_VERSION" != "latest" ]; then
|
||||
TARGET_VERSION="${TARGET_VERSION#v}"
|
||||
fi
|
||||
@@ -640,15 +852,15 @@ case "$ACTION" in
|
||||
DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}"
|
||||
fi
|
||||
|
||||
say ">>> Stage 2: Downloading archive"
|
||||
TEMP_DIR="$(mktemp -d)" || die "Temp directory creation failed"
|
||||
say "$L_I_STAGE_2"
|
||||
TEMP_DIR="$(mktemp -d)" || die "$L_ERR_TMP_DIR"
|
||||
if [ -z "$TEMP_DIR" ] || [ ! -d "$TEMP_DIR" ]; then
|
||||
die "Temp directory is invalid or was not created"
|
||||
die "$L_ERR_TMP_INV"
|
||||
fi
|
||||
|
||||
if ! fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}"; then
|
||||
if [ "$ARCH" = "x86_64-v3" ]; then
|
||||
say " -> x86_64-v3 build not found, falling back to standard x86_64..."
|
||||
say " -> $L_INFO_FALLBACK"
|
||||
ARCH="x86_64"
|
||||
FILE_NAME="${BIN_NAME}-${ARCH}-linux-${LIBC}.tar.gz"
|
||||
if [ "$TARGET_VERSION" = "latest" ]; then
|
||||
@@ -656,64 +868,58 @@ case "$ACTION" in
|
||||
else
|
||||
DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}"
|
||||
fi
|
||||
fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}" || die "Download failed"
|
||||
fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}" || die "$L_ERR_DL_FAIL"
|
||||
else
|
||||
die "Download failed"
|
||||
die "$L_ERR_DL_FAIL"
|
||||
fi
|
||||
fi
|
||||
|
||||
say ">>> Stage 3: Extracting archive"
|
||||
say "$L_I_STAGE_3"
|
||||
if ! gzip -dc "${TEMP_DIR}/${FILE_NAME}" | tar -xf - -C "$TEMP_DIR" 2>/dev/null; then
|
||||
die "Extraction failed (downloaded archive might be invalid or 404)."
|
||||
die "$L_ERR_EXTRACT"
|
||||
fi
|
||||
|
||||
EXTRACTED_BIN="$(find "$TEMP_DIR" -type f -name "$BIN_NAME" -print 2>/dev/null | head -n 1 || true)"
|
||||
[ -n "$EXTRACTED_BIN" ] || die "Binary '$BIN_NAME' not found in archive"
|
||||
[ -n "$EXTRACTED_BIN" ] || die "$L_ERR_BIN_NOT_FOUND"
|
||||
|
||||
say ">>> Stage 4: Setting up environment (User, Group, Directories)"
|
||||
say "$L_I_STAGE_4"
|
||||
ensure_user_group; setup_dirs; stop_service
|
||||
|
||||
say ">>> Stage 5: Installing binary"
|
||||
say "$L_I_STAGE_5"
|
||||
install_binary "$EXTRACTED_BIN" "${INSTALL_DIR}/${BIN_NAME}"
|
||||
|
||||
say ">>> Stage 6: Generating/Updating configuration"
|
||||
say "$L_I_STAGE_6"
|
||||
install_config
|
||||
|
||||
say ">>> Stage 7: Installing and starting service"
|
||||
say "$L_I_STAGE_7"
|
||||
install_service
|
||||
|
||||
if [ "${SERVICE_START_FAILED:-0}" -eq 1 ]; then
|
||||
printf '\n====================================================================\n'
|
||||
printf ' INSTALLATION COMPLETED WITH WARNINGS\n'
|
||||
printf ' %s\n' "$L_OUT_WARN_H"
|
||||
printf '====================================================================\n\n'
|
||||
printf 'The service was installed but failed to start automatically.\n'
|
||||
printf 'Please check the logs to determine the issue.\n\n'
|
||||
printf '%b' "$L_OUT_WARN_D"
|
||||
else
|
||||
printf '\n====================================================================\n'
|
||||
printf ' INSTALLATION SUCCESS\n'
|
||||
printf ' %s\n' "$L_OUT_SUCC_H"
|
||||
printf '====================================================================\n\n'
|
||||
fi
|
||||
|
||||
svc="$(get_svc_mgr)"
|
||||
if [ "$svc" = "systemd" ]; then
|
||||
printf 'To check the status of your proxy service, run:\n'
|
||||
printf ' systemctl status %s\n\n' "$SERVICE_NAME"
|
||||
elif [ "$svc" = "openrc" ]; then
|
||||
printf 'To check the status of your proxy service, run:\n'
|
||||
printf ' rc-service %s status\n\n' "$SERVICE_NAME"
|
||||
fi
|
||||
SERVER_IP=""
|
||||
if command -v curl >/dev/null 2>&1; then SERVER_IP="$(curl -s4 -m 3 ifconfig.me 2>/dev/null || curl -s4 -m 3 api.ipify.org 2>/dev/null || true)"
|
||||
elif command -v wget >/dev/null 2>&1; then SERVER_IP="$(wget -qO- -T 3 ifconfig.me 2>/dev/null || wget -qO- -T 3 api.ipify.org 2>/dev/null || true)"; fi
|
||||
[ -z "$SERVER_IP" ] && SERVER_IP="<YOUR_SERVER_IP>"
|
||||
|
||||
if command -v xxd >/dev/null 2>&1; then HEX_DOMAIN="$(printf '%s' "$TLS_DOMAIN" | xxd -p | tr -d '\n')"
|
||||
elif command -v hexdump >/dev/null 2>&1; then HEX_DOMAIN="$(printf '%s' "$TLS_DOMAIN" | hexdump -v -e '/1 "%02x"')"
|
||||
elif command -v od >/dev/null 2>&1; then HEX_DOMAIN="$(printf '%s' "$TLS_DOMAIN" | od -A n -t x1 | tr -d ' \n')"
|
||||
else HEX_DOMAIN=""; fi
|
||||
|
||||
API_LISTEN="$($SUDO awk -F'"' '/^[ \t]*listen[ \t]*=/ {print $2; exit}' "$CONFIG_FILE" 2>/dev/null || true)"
|
||||
API_LISTEN="${API_LISTEN:-127.0.0.1:9091}"
|
||||
CLIENT_SECRET="ee${USER_SECRET}${HEX_DOMAIN}"
|
||||
|
||||
printf 'To get your user connection links (for Telegram), run:\n'
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
printf ' curl -s http://%s/v1/users | jq -r '\''.data[]? | "User: \\(.username)\\n\\(.links.tls[0] // empty)\\n"'\''\n' "$API_LISTEN"
|
||||
else
|
||||
printf ' curl -s http://%s/v1/users\n' "$API_LISTEN"
|
||||
printf ' (Tip: Install '\''jq'\'' for a much cleaner output)\n'
|
||||
fi
|
||||
printf '%b\n' "$L_OUT_LINK"
|
||||
printf ' tg://proxy?server=%s&port=%s&secret=%s\n\n' "$SERVER_IP" "$SERVER_PORT" "$CLIENT_SECRET"
|
||||
|
||||
printf '\n====================================================================\n'
|
||||
printf '====================================================================\n'
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#![allow(clippy::too_many_arguments)]
|
||||
|
||||
use std::convert::Infallible;
|
||||
use std::io::{Error as IoError, ErrorKind};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
@@ -16,7 +16,7 @@ use tokio::net::TcpListener;
|
||||
use tokio::sync::{Mutex, RwLock, watch};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::config::ProxyConfig;
|
||||
use crate::config::{ApiGrayAction, ProxyConfig};
|
||||
use crate::ip_tracker::UserIpTracker;
|
||||
use crate::proxy::route_mode::RouteRuntimeController;
|
||||
use crate::startup::StartupTracker;
|
||||
@@ -41,8 +41,8 @@ use config_store::{current_revision, load_config_from_disk, parse_if_match};
|
||||
use events::ApiEventStore;
|
||||
use http_utils::{error_response, read_json, read_optional_json, success_response};
|
||||
use model::{
|
||||
ApiFailure, CreateUserRequest, DeleteUserResponse, HealthData, PatchUserRequest,
|
||||
RotateSecretRequest, SummaryData, UserActiveIps,
|
||||
ApiFailure, CreateUserRequest, DeleteUserResponse, HealthData, HealthReadyData,
|
||||
PatchUserRequest, RotateSecretRequest, SummaryData, UserActiveIps,
|
||||
};
|
||||
use runtime_edge::{
|
||||
EdgeConnectionsCacheEntry, build_runtime_connections_summary_data,
|
||||
@@ -184,7 +184,9 @@ pub async fn serve(
|
||||
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
|
||||
.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,
|
||||
shared: Arc<ApiShared>,
|
||||
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||
) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||
) -> Result<Response<Full<Bytes>>, IoError> {
|
||||
let request_id = shared.next_request_id();
|
||||
let cfg = config_rx.borrow().clone();
|
||||
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()))
|
||||
{
|
||||
return Ok(error_response(
|
||||
request_id,
|
||||
ApiFailure::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"forbidden",
|
||||
"Source IP is not allowed",
|
||||
),
|
||||
));
|
||||
return match api_cfg.gray_action {
|
||||
ApiGrayAction::Api => Ok(error_response(
|
||||
request_id,
|
||||
ApiFailure::new(
|
||||
StatusCode::FORBIDDEN,
|
||||
"forbidden",
|
||||
"Source IP is not allowed",
|
||||
),
|
||||
)),
|
||||
ApiGrayAction::Ok200 => Ok(Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header("content-type", "text/html; charset=utf-8")
|
||||
.body(Full::new(Bytes::new()))
|
||||
.unwrap()),
|
||||
ApiGrayAction::Drop => Err(IoError::new(
|
||||
ErrorKind::ConnectionAborted,
|
||||
"api request dropped by gray_action=drop",
|
||||
)),
|
||||
};
|
||||
}
|
||||
|
||||
if !api_cfg.auth_header.is_empty() {
|
||||
@@ -244,11 +257,16 @@ async fn handle(
|
||||
|
||||
let method = req.method().clone();
|
||||
let path = req.uri().path().to_string();
|
||||
let normalized_path = if path.len() > 1 {
|
||||
path.trim_end_matches('/')
|
||||
} else {
|
||||
path.as_str()
|
||||
};
|
||||
let query = req.uri().query().map(str::to_string);
|
||||
let body_limit = api_cfg.request_body_limit_bytes;
|
||||
|
||||
let result: Result<Response<Full<Bytes>>, ApiFailure> = async {
|
||||
match (method.as_str(), path.as_str()) {
|
||||
match (method.as_str(), normalized_path) {
|
||||
("GET", "/v1/health") => {
|
||||
let revision = current_revision(&shared.config_path).await?;
|
||||
let data = HealthData {
|
||||
@@ -257,6 +275,33 @@ async fn handle(
|
||||
};
|
||||
Ok(success_response(StatusCode::OK, data, revision))
|
||||
}
|
||||
("GET", "/v1/health/ready") => {
|
||||
let revision = current_revision(&shared.config_path).await?;
|
||||
let admission_open = shared.runtime_state.admission_open.load(Ordering::Relaxed);
|
||||
let upstream_health = shared.upstream_manager.api_health_summary().await;
|
||||
let ready = admission_open && upstream_health.healthy_total > 0;
|
||||
let reason = if ready {
|
||||
None
|
||||
} else if !admission_open {
|
||||
Some("admission_closed")
|
||||
} else {
|
||||
Some("no_healthy_upstreams")
|
||||
};
|
||||
let data = HealthReadyData {
|
||||
ready,
|
||||
status: if ready { "ready" } else { "not_ready" },
|
||||
reason,
|
||||
admission_open,
|
||||
healthy_upstreams: upstream_health.healthy_total,
|
||||
total_upstreams: upstream_health.configured_total,
|
||||
};
|
||||
let status_code = if ready {
|
||||
StatusCode::OK
|
||||
} else {
|
||||
StatusCode::SERVICE_UNAVAILABLE
|
||||
};
|
||||
Ok(success_response(status_code, data, revision))
|
||||
}
|
||||
("GET", "/v1/system/info") => {
|
||||
let revision = current_revision(&shared.config_path).await?;
|
||||
let data = build_system_info_data(shared.as_ref(), cfg.as_ref(), &revision);
|
||||
@@ -431,7 +476,7 @@ async fn handle(
|
||||
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.contains('/')
|
||||
{
|
||||
@@ -600,6 +645,12 @@ async fn handle(
|
||||
),
|
||||
));
|
||||
}
|
||||
debug!(
|
||||
method = method.as_str(),
|
||||
path = %path,
|
||||
normalized_path = %normalized_path,
|
||||
"API route not found"
|
||||
);
|
||||
Ok(error_response(
|
||||
request_id,
|
||||
ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"),
|
||||
|
||||
@@ -60,6 +60,17 @@ pub(super) struct HealthData {
|
||||
pub(super) read_only: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct HealthReadyData {
|
||||
pub(super) ready: bool,
|
||||
pub(super) status: &'static str,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(super) reason: Option<&'static str>,
|
||||
pub(super) admission_open: bool,
|
||||
pub(super) healthy_upstreams: usize,
|
||||
pub(super) total_upstreams: usize,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(super) struct SummaryData {
|
||||
pub(super) uptime_seconds: f64,
|
||||
|
||||
@@ -452,7 +452,11 @@ fn build_user_links(
|
||||
startup_detected_ip_v6: Option<IpAddr>,
|
||||
) -> UserLinks {
|
||||
let hosts = resolve_link_hosts(cfg, startup_detected_ip_v4, startup_detected_ip_v6);
|
||||
let port = cfg.general.links.public_port.unwrap_or(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 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(
|
||||
cfg: &ProxyConfig,
|
||||
startup_detected_ip_v4: Option<IpAddr>,
|
||||
|
||||
73
src/cli.rs
73
src/cli.rs
@@ -6,12 +6,15 @@
|
||||
//! - `reload [--pid-file PATH]` - Reload configuration (SIGHUP)
|
||||
//! - `status [--pid-file PATH]` - Check daemon status
|
||||
//! - `run [OPTIONS] [config.toml]` - Run in foreground (default behavior)
|
||||
//! - `healthcheck [OPTIONS] [config.toml]` - Run control-plane health probe
|
||||
|
||||
use rand::RngExt;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::healthcheck::{self, HealthcheckMode};
|
||||
|
||||
#[cfg(unix)]
|
||||
use crate::daemon::{self, DEFAULT_PID_FILE, DaemonOptions};
|
||||
|
||||
@@ -28,6 +31,8 @@ pub enum Subcommand {
|
||||
Reload,
|
||||
/// Check daemon status (`status` subcommand).
|
||||
Status,
|
||||
/// Run health probe and exit with status code.
|
||||
Healthcheck,
|
||||
/// Fire-and-forget setup (`--init`).
|
||||
Init,
|
||||
}
|
||||
@@ -38,6 +43,8 @@ pub struct ParsedCommand {
|
||||
pub subcommand: Subcommand,
|
||||
pub pid_file: PathBuf,
|
||||
pub config_path: String,
|
||||
pub healthcheck_mode: HealthcheckMode,
|
||||
pub healthcheck_mode_invalid: Option<String>,
|
||||
#[cfg(unix)]
|
||||
pub daemon_opts: DaemonOptions,
|
||||
pub init_opts: Option<InitOptions>,
|
||||
@@ -52,6 +59,8 @@ impl Default for ParsedCommand {
|
||||
#[cfg(not(unix))]
|
||||
pid_file: PathBuf::from("/var/run/telemt.pid"),
|
||||
config_path: "config.toml".to_string(),
|
||||
healthcheck_mode: HealthcheckMode::Liveness,
|
||||
healthcheck_mode_invalid: None,
|
||||
#[cfg(unix)]
|
||||
daemon_opts: DaemonOptions::default(),
|
||||
init_opts: None,
|
||||
@@ -91,6 +100,9 @@ pub fn parse_command(args: &[String]) -> ParsedCommand {
|
||||
"status" => {
|
||||
cmd.subcommand = Subcommand::Status;
|
||||
}
|
||||
"healthcheck" => {
|
||||
cmd.subcommand = Subcommand::Healthcheck;
|
||||
}
|
||||
"run" => {
|
||||
cmd.subcommand = Subcommand::Run;
|
||||
#[cfg(unix)]
|
||||
@@ -113,7 +125,35 @@ pub fn parse_command(args: &[String]) -> ParsedCommand {
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
// Skip subcommand names
|
||||
"start" | "stop" | "reload" | "status" | "run" => {}
|
||||
"start" | "stop" | "reload" | "status" | "run" | "healthcheck" => {}
|
||||
"--mode" => {
|
||||
i += 1;
|
||||
if i < args.len() {
|
||||
match HealthcheckMode::from_cli_arg(&args[i]) {
|
||||
Some(mode) => {
|
||||
cmd.healthcheck_mode = mode;
|
||||
cmd.healthcheck_mode_invalid = None;
|
||||
}
|
||||
None => {
|
||||
cmd.healthcheck_mode_invalid = Some(args[i].clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
cmd.healthcheck_mode_invalid = Some(String::new());
|
||||
}
|
||||
}
|
||||
s if s.starts_with("--mode=") => {
|
||||
let raw = s.trim_start_matches("--mode=");
|
||||
match HealthcheckMode::from_cli_arg(raw) {
|
||||
Some(mode) => {
|
||||
cmd.healthcheck_mode = mode;
|
||||
cmd.healthcheck_mode_invalid = None;
|
||||
}
|
||||
None => {
|
||||
cmd.healthcheck_mode_invalid = Some(raw.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
// PID file option (for stop/reload/status)
|
||||
"--pid-file" => {
|
||||
i += 1;
|
||||
@@ -152,6 +192,20 @@ pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
||||
Subcommand::Stop => Some(cmd_stop(&cmd.pid_file)),
|
||||
Subcommand::Reload => Some(cmd_reload(&cmd.pid_file)),
|
||||
Subcommand::Status => Some(cmd_status(&cmd.pid_file)),
|
||||
Subcommand::Healthcheck => {
|
||||
if let Some(invalid_mode) = cmd.healthcheck_mode_invalid.as_ref() {
|
||||
if invalid_mode.is_empty() {
|
||||
eprintln!("[telemt] Missing value for --mode (supported: liveness, ready)");
|
||||
} else {
|
||||
eprintln!(
|
||||
"[telemt] Invalid --mode value '{invalid_mode}' (supported: liveness, ready)"
|
||||
);
|
||||
}
|
||||
Some(2)
|
||||
} else {
|
||||
Some(healthcheck::run(&cmd.config_path, cmd.healthcheck_mode))
|
||||
}
|
||||
}
|
||||
Subcommand::Init => {
|
||||
if let Some(opts) = cmd.init_opts.clone() {
|
||||
match run_init(opts) {
|
||||
@@ -177,6 +231,20 @@ pub fn execute_subcommand(cmd: &ParsedCommand) -> Option<i32> {
|
||||
eprintln!("[telemt] Subcommand not supported on this platform");
|
||||
Some(1)
|
||||
}
|
||||
Subcommand::Healthcheck => {
|
||||
if let Some(invalid_mode) = cmd.healthcheck_mode_invalid.as_ref() {
|
||||
if invalid_mode.is_empty() {
|
||||
eprintln!("[telemt] Missing value for --mode (supported: liveness, ready)");
|
||||
} else {
|
||||
eprintln!(
|
||||
"[telemt] Invalid --mode value '{invalid_mode}' (supported: liveness, ready)"
|
||||
);
|
||||
}
|
||||
Some(2)
|
||||
} else {
|
||||
Some(healthcheck::run(&cmd.config_path, cmd.healthcheck_mode))
|
||||
}
|
||||
}
|
||||
Subcommand::Init => {
|
||||
if let Some(opts) = cmd.init_opts.clone() {
|
||||
match run_init(opts) {
|
||||
@@ -598,16 +666,17 @@ secure = false
|
||||
tls = true
|
||||
|
||||
[server]
|
||||
port = {port}
|
||||
listen_addr_ipv4 = "0.0.0.0"
|
||||
listen_addr_ipv6 = "::"
|
||||
|
||||
[[server.listeners]]
|
||||
ip = "0.0.0.0"
|
||||
port = {port}
|
||||
# reuse_allow = false # Set true only when intentionally running multiple telemt instances on same port
|
||||
|
||||
[[server.listeners]]
|
||||
ip = "::"
|
||||
port = {port}
|
||||
|
||||
[timeouts]
|
||||
client_first_byte_idle_secs = 300
|
||||
|
||||
@@ -615,6 +615,26 @@ pub(crate) fn default_mask_relay_max_bytes() -> usize {
|
||||
32 * 1024
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn default_mask_relay_timeout_ms() -> u64 {
|
||||
60_000
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn default_mask_relay_timeout_ms() -> u64 {
|
||||
200
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
pub(crate) fn default_mask_relay_idle_timeout_ms() -> u64 {
|
||||
5_000
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn default_mask_relay_idle_timeout_ms() -> u64 {
|
||||
100
|
||||
}
|
||||
|
||||
pub(crate) fn default_mask_classifier_prefetch_timeout_ms() -> u64 {
|
||||
5
|
||||
}
|
||||
|
||||
@@ -17,8 +17,9 @@
|
||||
//! | `network` | `dns_overrides` | Applied immediately |
|
||||
//! | `access` | All user/quota fields | Effective immediately |
|
||||
//!
|
||||
//! Fields that require re-binding sockets (`server.port`, `censorship.*`,
|
||||
//! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted.
|
||||
//! Fields that require re-binding sockets (`server.listeners`, legacy
|
||||
//! `server.port`, `censorship.*`, `network.*`, `use_middle_proxy`) are **not**
|
||||
//! applied; a warning is emitted.
|
||||
//! Non-hot changes are never mixed into the runtime config snapshot.
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
@@ -120,6 +121,9 @@ pub struct HotFields {
|
||||
pub user_max_tcp_conns_global_each: usize,
|
||||
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
|
||||
pub user_data_quota: std::collections::HashMap<String, u64>,
|
||||
pub user_rate_limits: std::collections::HashMap<String, crate::config::RateLimitBps>,
|
||||
pub cidr_rate_limits:
|
||||
std::collections::HashMap<ipnetwork::IpNetwork, crate::config::RateLimitBps>,
|
||||
pub user_max_unique_ips: std::collections::HashMap<String, usize>,
|
||||
pub user_max_unique_ips_global_each: usize,
|
||||
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,
|
||||
@@ -244,6 +248,8 @@ impl HotFields {
|
||||
user_max_tcp_conns_global_each: cfg.access.user_max_tcp_conns_global_each,
|
||||
user_expirations: cfg.access.user_expirations.clone(),
|
||||
user_data_quota: cfg.access.user_data_quota.clone(),
|
||||
user_rate_limits: cfg.access.user_rate_limits.clone(),
|
||||
cidr_rate_limits: cfg.access.cidr_rate_limits.clone(),
|
||||
user_max_unique_ips: cfg.access.user_max_unique_ips.clone(),
|
||||
user_max_unique_ips_global_each: cfg.access.user_max_unique_ips_global_each,
|
||||
user_max_unique_ips_mode: cfg.access.user_max_unique_ips_mode,
|
||||
@@ -299,6 +305,7 @@ fn listeners_equal(
|
||||
}
|
||||
lhs.iter().zip(rhs.iter()).all(|(a, b)| {
|
||||
a.ip == b.ip
|
||||
&& a.port == b.port
|
||||
&& a.announce == b.announce
|
||||
&& a.announce_ip == b.announce_ip
|
||||
&& a.proxy_protocol == b.proxy_protocol
|
||||
@@ -306,6 +313,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)]
|
||||
struct WatchManifest {
|
||||
files: BTreeSet<PathBuf>,
|
||||
@@ -535,6 +550,8 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
|
||||
cfg.access.user_max_tcp_conns_global_each = new.access.user_max_tcp_conns_global_each;
|
||||
cfg.access.user_expirations = new.access.user_expirations.clone();
|
||||
cfg.access.user_data_quota = new.access.user_data_quota.clone();
|
||||
cfg.access.user_rate_limits = new.access.user_rate_limits.clone();
|
||||
cfg.access.cidr_rate_limits = new.access.cidr_rate_limits.clone();
|
||||
cfg.access.user_max_unique_ips = new.access.user_max_unique_ips.clone();
|
||||
cfg.access.user_max_unique_ips_global_each = new.access.user_max_unique_ips_global_each;
|
||||
cfg.access.user_max_unique_ips_mode = new.access.user_max_unique_ips_mode;
|
||||
@@ -560,6 +577,7 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
||||
if old.server.api.enabled != new.server.api.enabled
|
||||
|| old.server.api.listen != new.server.api.listen
|
||||
|| old.server.api.whitelist != new.server.api.whitelist
|
||||
|| old.server.api.gray_action != new.server.api.gray_action
|
||||
|| old.server.api.auth_header != new.server.api.auth_header
|
||||
|| old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes
|
||||
|| old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
|
||||
@@ -611,6 +629,8 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
||||
|| old.censorship.mask_shape_above_cap_blur_max_bytes
|
||||
!= new.censorship.mask_shape_above_cap_blur_max_bytes
|
||||
|| old.censorship.mask_relay_max_bytes != new.censorship.mask_relay_max_bytes
|
||||
|| old.censorship.mask_relay_timeout_ms != new.censorship.mask_relay_timeout_ms
|
||||
|| old.censorship.mask_relay_idle_timeout_ms != new.censorship.mask_relay_idle_timeout_ms
|
||||
|| old.censorship.mask_classifier_prefetch_timeout_ms
|
||||
!= new.censorship.mask_classifier_prefetch_timeout_ms
|
||||
|| old.censorship.mask_timing_normalization_enabled
|
||||
@@ -1117,7 +1137,7 @@ fn log_changes(
|
||||
.general
|
||||
.links
|
||||
.public_port
|
||||
.unwrap_or(new_cfg.server.port);
|
||||
.unwrap_or(resolve_default_link_port(new_cfg));
|
||||
for user in &added {
|
||||
if let Some(secret) = new_hot.users.get(*user) {
|
||||
print_user_links(user, secret, &host, port, new_cfg);
|
||||
@@ -1170,6 +1190,18 @@ fn log_changes(
|
||||
new_hot.user_data_quota.len()
|
||||
);
|
||||
}
|
||||
if old_hot.user_rate_limits != new_hot.user_rate_limits {
|
||||
info!(
|
||||
"config reload: user_rate_limits updated ({} entries)",
|
||||
new_hot.user_rate_limits.len()
|
||||
);
|
||||
}
|
||||
if old_hot.cidr_rate_limits != new_hot.cidr_rate_limits {
|
||||
info!(
|
||||
"config reload: cidr_rate_limits updated ({} entries)",
|
||||
new_hot.cidr_rate_limits.len()
|
||||
);
|
||||
}
|
||||
if old_hot.user_max_unique_ips != new_hot.user_max_unique_ips {
|
||||
info!(
|
||||
"config reload: user_max_unique_ips updated ({} entries)",
|
||||
|
||||
@@ -253,6 +253,12 @@ fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
|
||||
}
|
||||
|
||||
for upstream in &config.upstreams {
|
||||
if matches!(upstream.ipv4, Some(false)) && matches!(upstream.ipv6, Some(false)) {
|
||||
return Err(ProxyError::Config(
|
||||
"upstream.ipv4 and upstream.ipv6 cannot both be false".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type {
|
||||
let parsed = ShadowsocksServerConfig::from_url(url)
|
||||
.map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?;
|
||||
@@ -337,27 +343,108 @@ impl ProxyConfig {
|
||||
let network_table = parsed_toml
|
||||
.get("network")
|
||||
.and_then(|value| value.as_table());
|
||||
let server_table = parsed_toml.get("server").and_then(|value| value.as_table());
|
||||
let conntrack_control_table = server_table
|
||||
.and_then(|table| table.get("conntrack_control"))
|
||||
.and_then(|value| value.as_table());
|
||||
let update_every_is_explicit = general_table
|
||||
.map(|table| table.contains_key("update_every"))
|
||||
.unwrap_or(false);
|
||||
let beobachten_is_explicit = general_table
|
||||
.map(|table| table.contains_key("beobachten"))
|
||||
.unwrap_or(false);
|
||||
let beobachten_minutes_is_explicit = general_table
|
||||
.map(|table| table.contains_key("beobachten_minutes"))
|
||||
.unwrap_or(false);
|
||||
let beobachten_flush_secs_is_explicit = general_table
|
||||
.map(|table| table.contains_key("beobachten_flush_secs"))
|
||||
.unwrap_or(false);
|
||||
let beobachten_file_is_explicit = general_table
|
||||
.map(|table| table.contains_key("beobachten_file"))
|
||||
.unwrap_or(false);
|
||||
let legacy_secret_is_explicit = general_table
|
||||
.map(|table| table.contains_key("proxy_secret_auto_reload_secs"))
|
||||
.unwrap_or(false);
|
||||
let legacy_config_is_explicit = general_table
|
||||
.map(|table| table.contains_key("proxy_config_auto_reload_secs"))
|
||||
.unwrap_or(false);
|
||||
let legacy_top_level_beobachten = parsed_toml.get("beobachten").cloned();
|
||||
let legacy_top_level_beobachten_minutes = parsed_toml.get("beobachten_minutes").cloned();
|
||||
let legacy_top_level_beobachten_flush_secs =
|
||||
parsed_toml.get("beobachten_flush_secs").cloned();
|
||||
let legacy_top_level_beobachten_file = parsed_toml.get("beobachten_file").cloned();
|
||||
let stun_servers_is_explicit = network_table
|
||||
.map(|table| table.contains_key("stun_servers"))
|
||||
.unwrap_or(false);
|
||||
let inline_conntrack_control_is_explicit = conntrack_control_table
|
||||
.map(|table| table.contains_key("inline_conntrack_control"))
|
||||
.unwrap_or(false);
|
||||
|
||||
let mut config: ProxyConfig = parsed_toml
|
||||
.try_into()
|
||||
.map_err(|e| ProxyError::Config(e.to_string()))?;
|
||||
config
|
||||
.server
|
||||
.conntrack_control
|
||||
.inline_conntrack_control_explicit = inline_conntrack_control_is_explicit;
|
||||
|
||||
if !update_every_is_explicit && (legacy_secret_is_explicit || legacy_config_is_explicit) {
|
||||
config.general.update_every = None;
|
||||
}
|
||||
|
||||
// Backward compatibility: legacy top-level beobachten* keys.
|
||||
// Prefer `[general].*` when both are present.
|
||||
let mut legacy_beobachten_applied = false;
|
||||
if !beobachten_is_explicit && let Some(value) = legacy_top_level_beobachten.as_ref() {
|
||||
let parsed = value.as_bool().ok_or_else(|| {
|
||||
ProxyError::Config("beobachten (top-level) must be a boolean".to_string())
|
||||
})?;
|
||||
config.general.beobachten = parsed;
|
||||
legacy_beobachten_applied = true;
|
||||
}
|
||||
if !beobachten_minutes_is_explicit
|
||||
&& let Some(value) = legacy_top_level_beobachten_minutes.as_ref()
|
||||
{
|
||||
let raw = value.as_integer().ok_or_else(|| {
|
||||
ProxyError::Config("beobachten_minutes (top-level) must be an integer".to_string())
|
||||
})?;
|
||||
let parsed = u64::try_from(raw).map_err(|_| {
|
||||
ProxyError::Config(
|
||||
"beobachten_minutes (top-level) must be within u64 range".to_string(),
|
||||
)
|
||||
})?;
|
||||
config.general.beobachten_minutes = parsed;
|
||||
legacy_beobachten_applied = true;
|
||||
}
|
||||
if !beobachten_flush_secs_is_explicit
|
||||
&& let Some(value) = legacy_top_level_beobachten_flush_secs.as_ref()
|
||||
{
|
||||
let raw = value.as_integer().ok_or_else(|| {
|
||||
ProxyError::Config(
|
||||
"beobachten_flush_secs (top-level) must be an integer".to_string(),
|
||||
)
|
||||
})?;
|
||||
let parsed = u64::try_from(raw).map_err(|_| {
|
||||
ProxyError::Config(
|
||||
"beobachten_flush_secs (top-level) must be within u64 range".to_string(),
|
||||
)
|
||||
})?;
|
||||
config.general.beobachten_flush_secs = parsed;
|
||||
legacy_beobachten_applied = true;
|
||||
}
|
||||
if !beobachten_file_is_explicit
|
||||
&& let Some(value) = legacy_top_level_beobachten_file.as_ref()
|
||||
{
|
||||
let parsed = value.as_str().ok_or_else(|| {
|
||||
ProxyError::Config("beobachten_file (top-level) must be a string".to_string())
|
||||
})?;
|
||||
config.general.beobachten_file = parsed.to_string();
|
||||
legacy_beobachten_applied = true;
|
||||
}
|
||||
if legacy_beobachten_applied {
|
||||
warn!("top-level beobachten* keys are deprecated; use general.beobachten* instead");
|
||||
}
|
||||
|
||||
let legacy_nat_stun = config.general.middle_proxy_nat_stun.take();
|
||||
let legacy_nat_stun_servers =
|
||||
std::mem::take(&mut config.general.middle_proxy_nat_stun_servers);
|
||||
@@ -785,6 +872,22 @@ impl ProxyConfig {
|
||||
));
|
||||
}
|
||||
|
||||
for (user, limit) in &config.access.user_rate_limits {
|
||||
if limit.up_bps == 0 && limit.down_bps == 0 {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"access.user_rate_limits.{user} must set at least one non-zero direction"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
for (cidr, limit) in &config.access.cidr_rate_limits {
|
||||
if limit.up_bps == 0 && limit.down_bps == 0 {
|
||||
return Err(ProxyError::Config(format!(
|
||||
"access.cidr_rate_limits.{cidr} must set at least one non-zero direction"
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if config.general.me_reinit_every_secs == 0 {
|
||||
return Err(ProxyError::Config(
|
||||
"general.me_reinit_every_secs must be > 0".to_string(),
|
||||
@@ -1250,6 +1353,7 @@ impl ProxyConfig {
|
||||
if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() {
|
||||
config.server.listeners.push(ListenerConfig {
|
||||
ip: ipv4,
|
||||
port: Some(config.server.port),
|
||||
announce: None,
|
||||
announce_ip: None,
|
||||
proxy_protocol: None,
|
||||
@@ -1261,6 +1365,7 @@ impl ProxyConfig {
|
||||
{
|
||||
config.server.listeners.push(ListenerConfig {
|
||||
ip: ipv6,
|
||||
port: Some(config.server.port),
|
||||
announce: None,
|
||||
announce_ip: None,
|
||||
proxy_protocol: None,
|
||||
@@ -1269,6 +1374,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.
|
||||
for listener in &mut config.server.listeners {
|
||||
if listener.announce.is_none()
|
||||
@@ -1289,11 +1401,14 @@ impl ProxyConfig {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1385,6 +1500,21 @@ mod tests {
|
||||
const TEST_SHADOWSOCKS_URL: &str =
|
||||
"ss://2022-blake3-aes-256-gcm:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=@127.0.0.1:8388";
|
||||
|
||||
fn load_config_from_temp_toml(toml: &str) -> ProxyConfig {
|
||||
let nonce = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_nanos();
|
||||
let dir = std::env::temp_dir().join(format!("telemt_load_cfg_{nonce}"));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let path = dir.join("config.toml");
|
||||
std::fs::write(&path, toml).unwrap();
|
||||
let cfg = ProxyConfig::load(&path).unwrap();
|
||||
let _ = std::fs::remove_file(path);
|
||||
let _ = std::fs::remove_dir(dir);
|
||||
cfg
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serde_defaults_remain_unchanged_for_present_sections() {
|
||||
let toml = r#"
|
||||
@@ -1481,6 +1611,7 @@ mod tests {
|
||||
cfg.general.rpc_proxy_req_every,
|
||||
default_rpc_proxy_req_every()
|
||||
);
|
||||
assert_eq!(cfg.general.beobachten_file, default_beobachten_file());
|
||||
assert_eq!(cfg.general.update_every, default_update_every());
|
||||
assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4());
|
||||
assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt());
|
||||
@@ -1491,6 +1622,7 @@ mod tests {
|
||||
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
|
||||
assert_eq!(cfg.server.api.listen, default_api_listen());
|
||||
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
||||
assert_eq!(cfg.server.api.gray_action, ApiGrayAction::Drop);
|
||||
assert_eq!(
|
||||
cfg.server.api.request_body_limit_bytes,
|
||||
default_api_request_body_limit_bytes()
|
||||
@@ -1647,6 +1779,7 @@ mod tests {
|
||||
default_upstream_connect_failfast_hard_errors()
|
||||
);
|
||||
assert_eq!(general.rpc_proxy_req_every, default_rpc_proxy_req_every());
|
||||
assert_eq!(general.beobachten_file, default_beobachten_file());
|
||||
assert_eq!(general.update_every, default_update_every());
|
||||
|
||||
let server = ServerConfig::default();
|
||||
@@ -1661,6 +1794,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(server.api.listen, default_api_listen());
|
||||
assert_eq!(server.api.whitelist, default_api_whitelist());
|
||||
assert_eq!(server.api.gray_action, ApiGrayAction::Drop);
|
||||
assert_eq!(
|
||||
server.api.request_body_limit_bytes,
|
||||
default_api_request_body_limit_bytes()
|
||||
@@ -1758,6 +1892,43 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conntrack_inline_explicit_flag_is_false_when_omitted() {
|
||||
let cfg = load_config_from_temp_toml(
|
||||
r#"
|
||||
[general]
|
||||
[network]
|
||||
[server]
|
||||
[server.conntrack_control]
|
||||
[access]
|
||||
"#,
|
||||
);
|
||||
assert!(
|
||||
!cfg.server
|
||||
.conntrack_control
|
||||
.inline_conntrack_control_explicit
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conntrack_inline_explicit_flag_is_true_when_present() {
|
||||
let cfg = load_config_from_temp_toml(
|
||||
r#"
|
||||
[general]
|
||||
[network]
|
||||
[server]
|
||||
[server.conntrack_control]
|
||||
inline_conntrack_control = true
|
||||
[access]
|
||||
"#,
|
||||
);
|
||||
assert!(
|
||||
cfg.server
|
||||
.conntrack_control
|
||||
.inline_conntrack_control_explicit
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_sni_action_parses_and_defaults_to_drop() {
|
||||
let cfg_default: ProxyConfig = toml::from_str(
|
||||
@@ -1808,6 +1979,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]
|
||||
fn dc_overrides_allow_string_and_array() {
|
||||
let toml = r#"
|
||||
|
||||
@@ -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.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -925,6 +940,14 @@ pub struct GeneralConfig {
|
||||
/// Minimum unavailable ME DC groups before degrading.
|
||||
#[serde(default = "default_degradation_min_unavailable_dc_groups")]
|
||||
pub degradation_min_unavailable_dc_groups: u8,
|
||||
|
||||
/// RST-on-close mode for accepted client sockets.
|
||||
/// `off` — normal FIN on all closes (default).
|
||||
/// `errors` — SO_LINGER(0) on accept, cleared after successful auth;
|
||||
/// pre-handshake failures send RST, relayed sessions close gracefully.
|
||||
/// `always` — SO_LINGER(0) on accept, never cleared; all closes send RST.
|
||||
#[serde(default)]
|
||||
pub rst_on_close: RstOnCloseMode,
|
||||
}
|
||||
|
||||
impl Default for GeneralConfig {
|
||||
@@ -1086,6 +1109,7 @@ impl Default for GeneralConfig {
|
||||
ntp_servers: default_ntp_servers(),
|
||||
auto_degradation_enabled: default_true(),
|
||||
degradation_min_unavailable_dc_groups: default_degradation_min_unavailable_dc_groups(),
|
||||
rst_on_close: RstOnCloseMode::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1129,7 +1153,8 @@ pub struct LinksConfig {
|
||||
#[serde(default)]
|
||||
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)]
|
||||
pub public_port: Option<u16>,
|
||||
}
|
||||
@@ -1159,6 +1184,13 @@ pub struct ApiConfig {
|
||||
#[serde(default = "default_api_whitelist")]
|
||||
pub whitelist: Vec<IpNetwork>,
|
||||
|
||||
/// Behavior for requests from source IPs outside `whitelist`.
|
||||
/// - `api`: return structured API forbidden response.
|
||||
/// - `200`: return `200 OK` with an empty body.
|
||||
/// - `drop`: close the connection without HTTP response.
|
||||
#[serde(default)]
|
||||
pub gray_action: ApiGrayAction,
|
||||
|
||||
/// Optional static value for `Authorization` header validation.
|
||||
/// Empty string disables header auth.
|
||||
#[serde(default)]
|
||||
@@ -1203,6 +1235,7 @@ impl Default for ApiConfig {
|
||||
enabled: default_true(),
|
||||
listen: default_api_listen(),
|
||||
whitelist: default_api_whitelist(),
|
||||
gray_action: ApiGrayAction::default(),
|
||||
auth_header: String::new(),
|
||||
request_body_limit_bytes: default_api_request_body_limit_bytes(),
|
||||
minimal_runtime_enabled: default_api_minimal_runtime_enabled(),
|
||||
@@ -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)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ConntrackMode {
|
||||
@@ -1283,6 +1329,10 @@ pub struct ConntrackControlConfig {
|
||||
#[serde(default = "default_conntrack_control_enabled")]
|
||||
pub inline_conntrack_control: bool,
|
||||
|
||||
/// Tracks whether inline_conntrack_control was explicitly set in config.
|
||||
#[serde(skip)]
|
||||
pub inline_conntrack_control_explicit: bool,
|
||||
|
||||
/// Conntrack mode for listener ingress traffic.
|
||||
#[serde(default)]
|
||||
pub mode: ConntrackMode,
|
||||
@@ -1317,6 +1367,7 @@ impl Default for ConntrackControlConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
inline_conntrack_control: default_conntrack_control_enabled(),
|
||||
inline_conntrack_control_explicit: false,
|
||||
mode: ConntrackMode::default(),
|
||||
backend: ConntrackBackend::default(),
|
||||
profile: ConntrackPressureProfile::default(),
|
||||
@@ -1330,6 +1381,8 @@ impl Default for ConntrackControlConfig {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ServerConfig {
|
||||
/// Legacy listener port used for backward compatibility.
|
||||
/// For new configs prefer `[[server.listeners]].port`.
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
|
||||
@@ -1686,6 +1739,19 @@ pub struct AntiCensorshipConfig {
|
||||
#[serde(default = "default_mask_relay_max_bytes")]
|
||||
pub mask_relay_max_bytes: usize,
|
||||
|
||||
/// Wall-clock cap for the full masking relay on non-MTProto fallback paths.
|
||||
/// Raise when the mask target is a long-lived service (e.g. WebSocket).
|
||||
/// Default: 60 000 ms (60 s).
|
||||
#[serde(default = "default_mask_relay_timeout_ms")]
|
||||
pub mask_relay_timeout_ms: u64,
|
||||
|
||||
/// Per-read idle timeout on masking relay and drain paths.
|
||||
/// Limits resource consumption by slow-loris attacks and port scanners.
|
||||
/// A read call stalling beyond this is treated as an abandoned connection.
|
||||
/// Default: 5 000 ms (5 s).
|
||||
#[serde(default = "default_mask_relay_idle_timeout_ms")]
|
||||
pub mask_relay_idle_timeout_ms: u64,
|
||||
|
||||
/// Prefetch timeout (ms) for extending fragmented masking classifier window.
|
||||
#[serde(default = "default_mask_classifier_prefetch_timeout_ms")]
|
||||
pub mask_classifier_prefetch_timeout_ms: u64,
|
||||
@@ -1731,6 +1797,8 @@ impl Default for AntiCensorshipConfig {
|
||||
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(),
|
||||
mask_shape_above_cap_blur_max_bytes: default_mask_shape_above_cap_blur_max_bytes(),
|
||||
mask_relay_max_bytes: default_mask_relay_max_bytes(),
|
||||
mask_relay_timeout_ms: default_mask_relay_timeout_ms(),
|
||||
mask_relay_idle_timeout_ms: default_mask_relay_idle_timeout_ms(),
|
||||
mask_classifier_prefetch_timeout_ms: default_mask_classifier_prefetch_timeout_ms(),
|
||||
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
|
||||
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
|
||||
@@ -1763,6 +1831,21 @@ pub struct AccessConfig {
|
||||
#[serde(default)]
|
||||
pub user_data_quota: HashMap<String, u64>,
|
||||
|
||||
/// Per-user transport rate limits in bits-per-second.
|
||||
///
|
||||
/// Each entry supports independent upload (`up_bps`) and download
|
||||
/// (`down_bps`) ceilings. A value of `0` in one direction means
|
||||
/// "unlimited" for that direction.
|
||||
#[serde(default)]
|
||||
pub user_rate_limits: HashMap<String, RateLimitBps>,
|
||||
|
||||
/// Per-CIDR aggregate transport rate limits in bits-per-second.
|
||||
///
|
||||
/// Matching uses longest-prefix-wins semantics. A value of `0` in one
|
||||
/// direction means "unlimited" for that direction.
|
||||
#[serde(default)]
|
||||
pub cidr_rate_limits: HashMap<IpNetwork, RateLimitBps>,
|
||||
|
||||
#[serde(default)]
|
||||
pub user_max_unique_ips: HashMap<String, usize>,
|
||||
|
||||
@@ -1796,6 +1879,8 @@ impl Default for AccessConfig {
|
||||
user_max_tcp_conns_global_each: default_user_max_tcp_conns_global_each(),
|
||||
user_expirations: HashMap::new(),
|
||||
user_data_quota: HashMap::new(),
|
||||
user_rate_limits: HashMap::new(),
|
||||
cidr_rate_limits: HashMap::new(),
|
||||
user_max_unique_ips: HashMap::new(),
|
||||
user_max_unique_ips_global_each: default_user_max_unique_ips_global_each(),
|
||||
user_max_unique_ips_mode: UserMaxUniqueIpsMode::default(),
|
||||
@@ -1807,6 +1892,14 @@ impl Default for AccessConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RateLimitBps {
|
||||
#[serde(default)]
|
||||
pub up_bps: u64,
|
||||
#[serde(default)]
|
||||
pub down_bps: u64,
|
||||
}
|
||||
|
||||
// ============= Aux Structures =============
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
@@ -1817,6 +1910,10 @@ pub enum UpstreamType {
|
||||
interface: Option<String>,
|
||||
#[serde(default)]
|
||||
bind_addresses: Option<Vec<String>>,
|
||||
/// Linux-only hard interface pinning via `SO_BINDTODEVICE`.
|
||||
/// Optional alias: `force_bind`.
|
||||
#[serde(default, alias = "force_bind")]
|
||||
bindtodevice: Option<String>,
|
||||
},
|
||||
Socks4 {
|
||||
address: String,
|
||||
@@ -1853,11 +1950,22 @@ pub struct UpstreamConfig {
|
||||
pub scopes: String,
|
||||
#[serde(skip)]
|
||||
pub selected_scope: String,
|
||||
/// Allow IPv4 DC targets for this upstream.
|
||||
/// `None` means auto-detect from runtime connectivity state.
|
||||
#[serde(default)]
|
||||
pub ipv4: Option<bool>,
|
||||
/// Allow IPv6 DC targets for this upstream.
|
||||
/// `None` means auto-detect from runtime connectivity state.
|
||||
#[serde(default)]
|
||||
pub ipv6: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ListenerConfig {
|
||||
pub ip: IpAddr,
|
||||
/// Per-listener TCP port. If omitted, falls back to legacy `server.port`.
|
||||
#[serde(default)]
|
||||
pub port: Option<u16>,
|
||||
/// IP address or hostname to announce in proxy links.
|
||||
/// Takes precedence over `announce_ip` if both are set.
|
||||
#[serde(default)]
|
||||
|
||||
@@ -24,6 +24,13 @@ enum NetfilterBackend {
|
||||
Iptables,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct ConntrackRuntimeSupport {
|
||||
netfilter_backend: Option<NetfilterBackend>,
|
||||
has_cap_net_admin: bool,
|
||||
has_conntrack_binary: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PressureSample {
|
||||
conn_pct: Option<u8>,
|
||||
@@ -56,11 +63,8 @@ pub(crate) fn spawn_conntrack_controller(
|
||||
shared: Arc<ProxySharedState>,
|
||||
) {
|
||||
if !cfg!(target_os = "linux") {
|
||||
let enabled = config_rx
|
||||
.borrow()
|
||||
.server
|
||||
.conntrack_control
|
||||
.inline_conntrack_control;
|
||||
let cfg = config_rx.borrow();
|
||||
let enabled = cfg.server.conntrack_control.inline_conntrack_control;
|
||||
stats.set_conntrack_control_enabled(enabled);
|
||||
stats.set_conntrack_control_available(false);
|
||||
stats.set_conntrack_pressure_active(false);
|
||||
@@ -68,9 +72,14 @@ pub(crate) fn spawn_conntrack_controller(
|
||||
stats.set_conntrack_rule_apply_ok(false);
|
||||
shared.disable_conntrack_close_sender();
|
||||
shared.set_conntrack_pressure_active(false);
|
||||
if enabled {
|
||||
if enabled
|
||||
&& cfg
|
||||
.server
|
||||
.conntrack_control
|
||||
.inline_conntrack_control_explicit
|
||||
{
|
||||
warn!(
|
||||
"conntrack control is configured but unsupported on this OS; disabling runtime worker"
|
||||
"conntrack control explicitly enabled but unsupported on this OS; disabling runtime worker"
|
||||
);
|
||||
}
|
||||
return;
|
||||
@@ -92,16 +101,17 @@ async fn run_conntrack_controller(
|
||||
let mut cfg = config_rx.borrow().clone();
|
||||
let mut pressure_state = PressureState::new(stats.as_ref());
|
||||
let mut delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||
let mut backend = pick_backend(cfg.server.conntrack_control.backend);
|
||||
let mut runtime_support = probe_runtime_support(cfg.server.conntrack_control.backend);
|
||||
let mut effective_enabled = effective_conntrack_enabled(&cfg, runtime_support);
|
||||
|
||||
apply_runtime_state(
|
||||
stats.as_ref(),
|
||||
shared.as_ref(),
|
||||
&cfg,
|
||||
backend.is_some(),
|
||||
runtime_support,
|
||||
false,
|
||||
);
|
||||
reconcile_rules(&cfg, backend, stats.as_ref()).await;
|
||||
reconcile_rules(&cfg, runtime_support, stats.as_ref()).await;
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
@@ -110,17 +120,18 @@ async fn run_conntrack_controller(
|
||||
break;
|
||||
}
|
||||
cfg = config_rx.borrow_and_update().clone();
|
||||
backend = pick_backend(cfg.server.conntrack_control.backend);
|
||||
runtime_support = probe_runtime_support(cfg.server.conntrack_control.backend);
|
||||
effective_enabled = effective_conntrack_enabled(&cfg, runtime_support);
|
||||
delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||
apply_runtime_state(stats.as_ref(), shared.as_ref(), &cfg, backend.is_some(), pressure_state.active);
|
||||
reconcile_rules(&cfg, backend, stats.as_ref()).await;
|
||||
apply_runtime_state(stats.as_ref(), shared.as_ref(), &cfg, runtime_support, pressure_state.active);
|
||||
reconcile_rules(&cfg, runtime_support, stats.as_ref()).await;
|
||||
}
|
||||
event = close_rx.recv() => {
|
||||
let Some(event) = event else {
|
||||
break;
|
||||
};
|
||||
stats.set_conntrack_event_queue_depth(close_rx.len() as u64);
|
||||
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||
if !effective_enabled {
|
||||
continue;
|
||||
}
|
||||
if !pressure_state.active {
|
||||
@@ -156,6 +167,7 @@ async fn run_conntrack_controller(
|
||||
stats.as_ref(),
|
||||
shared.as_ref(),
|
||||
&cfg,
|
||||
effective_enabled,
|
||||
&sample,
|
||||
&mut pressure_state,
|
||||
);
|
||||
@@ -175,20 +187,30 @@ fn apply_runtime_state(
|
||||
stats: &Stats,
|
||||
shared: &ProxySharedState,
|
||||
cfg: &ProxyConfig,
|
||||
backend_available: bool,
|
||||
runtime_support: ConntrackRuntimeSupport,
|
||||
pressure_active: bool,
|
||||
) {
|
||||
let enabled = cfg.server.conntrack_control.inline_conntrack_control;
|
||||
let available = enabled && backend_available && has_cap_net_admin();
|
||||
if enabled && !available {
|
||||
let available = effective_conntrack_enabled(cfg, runtime_support);
|
||||
if enabled
|
||||
&& !available
|
||||
&& cfg
|
||||
.server
|
||||
.conntrack_control
|
||||
.inline_conntrack_control_explicit
|
||||
{
|
||||
warn!(
|
||||
"conntrack control enabled but unavailable (missing CAP_NET_ADMIN or backend binaries)"
|
||||
has_cap_net_admin = runtime_support.has_cap_net_admin,
|
||||
backend_available = runtime_support.netfilter_backend.is_some(),
|
||||
conntrack_binary_available = runtime_support.has_conntrack_binary,
|
||||
configured_backend = ?cfg.server.conntrack_control.backend,
|
||||
"conntrack control explicitly enabled but unavailable; disabling runtime features"
|
||||
);
|
||||
}
|
||||
stats.set_conntrack_control_enabled(enabled);
|
||||
stats.set_conntrack_control_available(available);
|
||||
shared.set_conntrack_pressure_active(enabled && pressure_active);
|
||||
stats.set_conntrack_pressure_active(enabled && pressure_active);
|
||||
shared.set_conntrack_pressure_active(available && pressure_active);
|
||||
stats.set_conntrack_pressure_active(available && pressure_active);
|
||||
}
|
||||
|
||||
fn collect_pressure_sample(
|
||||
@@ -228,10 +250,11 @@ fn update_pressure_state(
|
||||
stats: &Stats,
|
||||
shared: &ProxySharedState,
|
||||
cfg: &ProxyConfig,
|
||||
effective_enabled: bool,
|
||||
sample: &PressureSample,
|
||||
state: &mut PressureState,
|
||||
) {
|
||||
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||
if !effective_enabled {
|
||||
if state.active {
|
||||
state.active = false;
|
||||
state.low_streak = 0;
|
||||
@@ -285,22 +308,26 @@ fn update_pressure_state(
|
||||
state.low_streak = 0;
|
||||
}
|
||||
|
||||
async fn reconcile_rules(cfg: &ProxyConfig, backend: Option<NetfilterBackend>, stats: &Stats) {
|
||||
async fn reconcile_rules(
|
||||
cfg: &ProxyConfig,
|
||||
runtime_support: ConntrackRuntimeSupport,
|
||||
stats: &Stats,
|
||||
) {
|
||||
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||
clear_notrack_rules_all_backends().await;
|
||||
stats.set_conntrack_rule_apply_ok(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if !has_cap_net_admin() {
|
||||
if !effective_conntrack_enabled(cfg, runtime_support) {
|
||||
clear_notrack_rules_all_backends().await;
|
||||
stats.set_conntrack_rule_apply_ok(false);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(backend) = backend else {
|
||||
stats.set_conntrack_rule_apply_ok(false);
|
||||
return;
|
||||
};
|
||||
let backend = runtime_support
|
||||
.netfilter_backend
|
||||
.expect("netfilter backend must be available for effective conntrack control");
|
||||
|
||||
let apply_result = match backend {
|
||||
NetfilterBackend::Nftables => apply_nft_rules(cfg).await,
|
||||
@@ -315,6 +342,24 @@ async fn reconcile_rules(cfg: &ProxyConfig, backend: Option<NetfilterBackend>, s
|
||||
}
|
||||
}
|
||||
|
||||
fn probe_runtime_support(configured_backend: ConntrackBackend) -> ConntrackRuntimeSupport {
|
||||
ConntrackRuntimeSupport {
|
||||
netfilter_backend: pick_backend(configured_backend),
|
||||
has_cap_net_admin: has_cap_net_admin(),
|
||||
has_conntrack_binary: command_exists("conntrack"),
|
||||
}
|
||||
}
|
||||
|
||||
fn effective_conntrack_enabled(
|
||||
cfg: &ProxyConfig,
|
||||
runtime_support: ConntrackRuntimeSupport,
|
||||
) -> bool {
|
||||
cfg.server.conntrack_control.inline_conntrack_control
|
||||
&& runtime_support.has_cap_net_admin
|
||||
&& runtime_support.netfilter_backend.is_some()
|
||||
&& runtime_support.has_conntrack_binary
|
||||
}
|
||||
|
||||
fn pick_backend(configured: ConntrackBackend) -> Option<NetfilterBackend> {
|
||||
match configured {
|
||||
ConntrackBackend::Auto => {
|
||||
@@ -343,15 +388,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 mut v4_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
|
||||
let mut v6_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
|
||||
let mut v4_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
|
||||
let mut v6_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
|
||||
|
||||
match mode {
|
||||
ConntrackMode::Tracked => {}
|
||||
ConntrackMode::Notrack => {
|
||||
if cfg.server.listeners.is_empty() {
|
||||
let port = cfg.server.port;
|
||||
if let Some(ipv4) = cfg
|
||||
.server
|
||||
.listen_addr_ipv4
|
||||
@@ -359,9 +417,9 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr
|
||||
.and_then(|s| s.parse::<IpAddr>().ok())
|
||||
{
|
||||
if ipv4.is_unspecified() {
|
||||
v4_targets.insert(None);
|
||||
v4_targets.insert((None, port));
|
||||
} else {
|
||||
v4_targets.insert(Some(ipv4));
|
||||
v4_targets.insert((Some(ipv4), port));
|
||||
}
|
||||
}
|
||||
if let Some(ipv6) = cfg
|
||||
@@ -371,33 +429,39 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr
|
||||
.and_then(|s| s.parse::<IpAddr>().ok())
|
||||
{
|
||||
if ipv6.is_unspecified() {
|
||||
v6_targets.insert(None);
|
||||
v6_targets.insert((None, port));
|
||||
} else {
|
||||
v6_targets.insert(Some(ipv6));
|
||||
v6_targets.insert((Some(ipv6), port));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for listener in &cfg.server.listeners {
|
||||
let port = listener.port.unwrap_or(cfg.server.port);
|
||||
if listener.ip.is_ipv4() {
|
||||
if listener.ip.is_unspecified() {
|
||||
v4_targets.insert(None);
|
||||
v4_targets.insert((None, port));
|
||||
} else {
|
||||
v4_targets.insert(Some(listener.ip));
|
||||
v4_targets.insert((Some(listener.ip), port));
|
||||
}
|
||||
} else if listener.ip.is_unspecified() {
|
||||
v6_targets.insert(None);
|
||||
v6_targets.insert((None, port));
|
||||
} else {
|
||||
v6_targets.insert(Some(listener.ip));
|
||||
v6_targets.insert((Some(listener.ip), port));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ConntrackMode::Hybrid => {
|
||||
let ports = listener_port_set(cfg);
|
||||
for ip in &cfg.server.conntrack_control.hybrid_listener_ips {
|
||||
if ip.is_ipv4() {
|
||||
v4_targets.insert(Some(*ip));
|
||||
for port in &ports {
|
||||
v4_targets.insert((Some(*ip), *port));
|
||||
}
|
||||
} else {
|
||||
v6_targets.insert(Some(*ip));
|
||||
for port in &ports {
|
||||
v6_targets.insert((Some(*ip), *port));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -422,19 +486,19 @@ async fn apply_nft_rules(cfg: &ProxyConfig) -> Result<(), String> {
|
||||
|
||||
let (v4_targets, v6_targets) = notrack_targets(cfg);
|
||||
let mut rules = Vec::new();
|
||||
for ip in v4_targets {
|
||||
for (ip, port) in v4_targets {
|
||||
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 {
|
||||
format!("tcp dport {} notrack", cfg.server.port)
|
||||
format!("tcp dport {} notrack", port)
|
||||
};
|
||||
rules.push(rule);
|
||||
}
|
||||
for ip in v6_targets {
|
||||
for (ip, port) in v6_targets {
|
||||
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 {
|
||||
format!("tcp dport {} notrack", cfg.server.port)
|
||||
format!("tcp dport {} notrack", port)
|
||||
};
|
||||
rules.push(rule);
|
||||
}
|
||||
@@ -498,7 +562,7 @@ async fn apply_iptables_rules_for_binary(
|
||||
|
||||
let (v4_targets, v6_targets) = notrack_targets(cfg);
|
||||
let selected = if ipv4 { v4_targets } else { v6_targets };
|
||||
for ip in selected {
|
||||
for (ip, port) in selected {
|
||||
let mut args = vec![
|
||||
"-t".to_string(),
|
||||
"raw".to_string(),
|
||||
@@ -507,7 +571,7 @@ async fn apply_iptables_rules_for_binary(
|
||||
"-p".to_string(),
|
||||
"tcp".to_string(),
|
||||
"--dport".to_string(),
|
||||
cfg.server.port.to_string(),
|
||||
port.to_string(),
|
||||
];
|
||||
if let Some(ip) = ip {
|
||||
args.push("-d".to_string());
|
||||
@@ -691,7 +755,7 @@ mod tests {
|
||||
me_queue_pressure_delta: 0,
|
||||
};
|
||||
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &sample, &mut state);
|
||||
|
||||
assert!(state.active);
|
||||
assert!(shared.conntrack_pressure_active());
|
||||
@@ -712,7 +776,14 @@ mod tests {
|
||||
accept_timeout_delta: 0,
|
||||
me_queue_pressure_delta: 0,
|
||||
};
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, &high_sample, &mut state);
|
||||
update_pressure_state(
|
||||
&stats,
|
||||
shared.as_ref(),
|
||||
&cfg,
|
||||
true,
|
||||
&high_sample,
|
||||
&mut state,
|
||||
);
|
||||
assert!(state.active);
|
||||
|
||||
let low_sample = PressureSample {
|
||||
@@ -721,11 +792,11 @@ mod tests {
|
||||
accept_timeout_delta: 0,
|
||||
me_queue_pressure_delta: 0,
|
||||
};
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &low_sample, &mut state);
|
||||
assert!(state.active);
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &low_sample, &mut state);
|
||||
assert!(state.active);
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, true, &low_sample, &mut state);
|
||||
|
||||
assert!(!state.active);
|
||||
assert!(!shared.conntrack_pressure_active());
|
||||
@@ -746,7 +817,7 @@ mod tests {
|
||||
me_queue_pressure_delta: 10,
|
||||
};
|
||||
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
|
||||
update_pressure_state(&stats, shared.as_ref(), &cfg, false, &sample, &mut state);
|
||||
|
||||
assert!(!state.active);
|
||||
assert!(!shared.conntrack_pressure_active());
|
||||
|
||||
@@ -8,6 +8,7 @@ use std::io::{self, Read, Write};
|
||||
use std::os::unix::fs::OpenOptionsExt;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use nix::errno::Errno;
|
||||
use nix::fcntl::{Flock, FlockArg};
|
||||
use nix::unistd::{self, ForkResult, Gid, Pid, Uid, chdir, close, fork, getpid, setsid};
|
||||
use tracing::{debug, info, warn};
|
||||
@@ -157,15 +158,15 @@ fn redirect_stdio_to_devnull() -> Result<(), DaemonError> {
|
||||
unsafe {
|
||||
// Redirect stdin (fd 0)
|
||||
if libc::dup2(devnull_fd, 0) < 0 {
|
||||
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
||||
return Err(DaemonError::RedirectFailed(Errno::last()));
|
||||
}
|
||||
// Redirect stdout (fd 1)
|
||||
if libc::dup2(devnull_fd, 1) < 0 {
|
||||
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
||||
return Err(DaemonError::RedirectFailed(Errno::last()));
|
||||
}
|
||||
// Redirect stderr (fd 2)
|
||||
if libc::dup2(devnull_fd, 2) < 0 {
|
||||
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last()));
|
||||
return Err(DaemonError::RedirectFailed(Errno::last()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,6 +338,27 @@ fn is_process_running(pid: i32) -> bool {
|
||||
nix::sys::signal::kill(Pid::from_raw(pid), None).is_ok()
|
||||
}
|
||||
|
||||
// macOS gates nix::unistd::setgroups differently in the current dependency set,
|
||||
// so call libc directly there while preserving the original nix path elsewhere.
|
||||
fn set_supplementary_groups(gid: Gid) -> Result<(), nix::Error> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let groups = [gid.as_raw()];
|
||||
let rc = unsafe {
|
||||
libc::setgroups(
|
||||
i32::try_from(groups.len()).expect("single supplementary group must fit in c_int"),
|
||||
groups.as_ptr(),
|
||||
)
|
||||
};
|
||||
if rc == 0 { Ok(()) } else { Err(Errno::last()) }
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
unistd::setgroups(&[gid])
|
||||
}
|
||||
}
|
||||
|
||||
/// Drops privileges to the specified user and group.
|
||||
///
|
||||
/// This should be called after binding privileged ports but before entering
|
||||
@@ -368,7 +390,7 @@ pub fn drop_privileges(
|
||||
|
||||
if let Some(gid) = target_gid {
|
||||
unistd::setgid(gid).map_err(DaemonError::PrivilegeDrop)?;
|
||||
unistd::setgroups(&[gid]).map_err(DaemonError::PrivilegeDrop)?;
|
||||
set_supplementary_groups(gid).map_err(DaemonError::PrivilegeDrop)?;
|
||||
info!(gid = gid.as_raw(), "Dropped group privileges");
|
||||
}
|
||||
|
||||
|
||||
211
src/healthcheck.rs
Normal file
211
src/healthcheck.rs
Normal file
@@ -0,0 +1,211 @@
|
||||
use std::io::{Read, Write};
|
||||
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr, TcpStream};
|
||||
use std::time::Duration;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::config::ProxyConfig;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum HealthcheckMode {
|
||||
Liveness,
|
||||
Ready,
|
||||
}
|
||||
|
||||
impl HealthcheckMode {
|
||||
pub(crate) fn from_cli_arg(value: &str) -> Option<Self> {
|
||||
match value {
|
||||
"liveness" => Some(Self::Liveness),
|
||||
"ready" => Some(Self::Ready),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn request_path(self) -> &'static str {
|
||||
match self {
|
||||
Self::Liveness => "/v1/health",
|
||||
Self::Ready => "/v1/health/ready",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn run(config_path: &str, mode: HealthcheckMode) -> i32 {
|
||||
match run_inner(config_path, mode) {
|
||||
Ok(()) => 0,
|
||||
Err(error) => {
|
||||
eprintln!("[telemt] healthcheck failed: {error}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn run_inner(config_path: &str, mode: HealthcheckMode) -> Result<(), String> {
|
||||
let config =
|
||||
ProxyConfig::load(config_path).map_err(|error| format!("config load failed: {error}"))?;
|
||||
let api_cfg = &config.server.api;
|
||||
if !api_cfg.enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let listen: SocketAddr = api_cfg
|
||||
.listen
|
||||
.parse()
|
||||
.map_err(|_| format!("invalid API listen address: {}", api_cfg.listen))?;
|
||||
if listen.port() == 0 {
|
||||
return Err("API listen port is 0".to_string());
|
||||
}
|
||||
let target = probe_target(listen);
|
||||
|
||||
let mut stream = TcpStream::connect_timeout(&target, Duration::from_secs(2))
|
||||
.map_err(|error| format!("connect {target} failed: {error}"))?;
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| format!("set read timeout failed: {error}"))?;
|
||||
stream
|
||||
.set_write_timeout(Some(Duration::from_secs(2)))
|
||||
.map_err(|error| format!("set write timeout failed: {error}"))?;
|
||||
|
||||
let request = build_request(target, mode.request_path(), &api_cfg.auth_header);
|
||||
stream
|
||||
.write_all(request.as_bytes())
|
||||
.map_err(|error| format!("request write failed: {error}"))?;
|
||||
stream
|
||||
.flush()
|
||||
.map_err(|error| format!("request flush failed: {error}"))?;
|
||||
|
||||
let mut raw_response = Vec::new();
|
||||
stream
|
||||
.read_to_end(&mut raw_response)
|
||||
.map_err(|error| format!("response read failed: {error}"))?;
|
||||
let response =
|
||||
String::from_utf8(raw_response).map_err(|_| "response is not valid UTF-8".to_string())?;
|
||||
|
||||
let (status_code, body) = split_response(&response)?;
|
||||
if status_code != 200 {
|
||||
return Err(format!("HTTP status {status_code}"));
|
||||
}
|
||||
|
||||
validate_payload(mode, body)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn probe_target(listen: SocketAddr) -> SocketAddr {
|
||||
match listen {
|
||||
SocketAddr::V4(addr) => {
|
||||
let ip = if addr.ip().is_unspecified() {
|
||||
Ipv4Addr::LOCALHOST
|
||||
} else {
|
||||
*addr.ip()
|
||||
};
|
||||
SocketAddr::from((ip, addr.port()))
|
||||
}
|
||||
SocketAddr::V6(addr) => {
|
||||
let ip = if addr.ip().is_unspecified() {
|
||||
Ipv6Addr::LOCALHOST
|
||||
} else {
|
||||
*addr.ip()
|
||||
};
|
||||
SocketAddr::from((ip, addr.port()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_request(target: SocketAddr, path: &str, auth_header: &str) -> String {
|
||||
let mut request = format!(
|
||||
"GET {path} HTTP/1.1\r\nHost: {}\r\nConnection: close\r\n",
|
||||
target
|
||||
);
|
||||
if !auth_header.is_empty() {
|
||||
request.push_str("Authorization: ");
|
||||
request.push_str(auth_header);
|
||||
request.push_str("\r\n");
|
||||
}
|
||||
request.push_str("\r\n");
|
||||
request
|
||||
}
|
||||
|
||||
fn split_response(response: &str) -> Result<(u16, &str), String> {
|
||||
let header_end = response
|
||||
.find("\r\n\r\n")
|
||||
.ok_or_else(|| "invalid HTTP response headers".to_string())?;
|
||||
let header = &response[..header_end];
|
||||
let body = &response[header_end + 4..];
|
||||
let status_line = header
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or_else(|| "missing HTTP status line".to_string())?;
|
||||
let status_code = parse_status_code(status_line)?;
|
||||
Ok((status_code, body))
|
||||
}
|
||||
|
||||
fn parse_status_code(status_line: &str) -> Result<u16, String> {
|
||||
let mut parts = status_line.split_whitespace();
|
||||
let version = parts
|
||||
.next()
|
||||
.ok_or_else(|| "missing HTTP version".to_string())?;
|
||||
if !version.starts_with("HTTP/") {
|
||||
return Err(format!("invalid HTTP status line: {status_line}"));
|
||||
}
|
||||
let code = parts
|
||||
.next()
|
||||
.ok_or_else(|| "missing HTTP status code".to_string())?;
|
||||
code.parse::<u16>()
|
||||
.map_err(|_| format!("invalid HTTP status code: {code}"))
|
||||
}
|
||||
|
||||
fn validate_payload(mode: HealthcheckMode, body: &str) -> Result<(), String> {
|
||||
let payload: Value =
|
||||
serde_json::from_str(body).map_err(|_| "response body is not valid JSON".to_string())?;
|
||||
if payload.get("ok").and_then(Value::as_bool) != Some(true) {
|
||||
return Err("response JSON has ok=false".to_string());
|
||||
}
|
||||
|
||||
let data = payload
|
||||
.get("data")
|
||||
.ok_or_else(|| "response JSON has no data field".to_string())?;
|
||||
match mode {
|
||||
HealthcheckMode::Liveness => {
|
||||
if data.get("status").and_then(Value::as_str) != Some("ok") {
|
||||
return Err("liveness status is not ok".to_string());
|
||||
}
|
||||
}
|
||||
HealthcheckMode::Ready => {
|
||||
if data.get("ready").and_then(Value::as_bool) != Some(true) {
|
||||
return Err("readiness flag is false".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{HealthcheckMode, parse_status_code, split_response, validate_payload};
|
||||
|
||||
#[test]
|
||||
fn parse_status_code_reads_http_200() {
|
||||
let status = parse_status_code("HTTP/1.1 200 OK").expect("must parse status");
|
||||
assert_eq!(status, 200);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_response_extracts_status_and_body() {
|
||||
let response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"ok\":true}";
|
||||
let (status, body) = split_response(response).expect("must split response");
|
||||
assert_eq!(status, 200);
|
||||
assert_eq!(body, "{\"ok\":true}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_payload_accepts_liveness_contract() {
|
||||
let body = "{\"ok\":true,\"data\":{\"status\":\"ok\"}}";
|
||||
validate_payload(HealthcheckMode::Liveness, body).expect("liveness payload must pass");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_payload_rejects_not_ready() {
|
||||
let body = "{\"ok\":true,\"data\":{\"ready\":false}}";
|
||||
let result = validate_payload(HealthcheckMode::Ready, body);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@ use tokio::net::UnixListener;
|
||||
use tokio::sync::{Semaphore, watch};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
use crate::config::ProxyConfig;
|
||||
use crate::config::{ProxyConfig, RstOnCloseMode};
|
||||
use crate::crypto::SecureRandom;
|
||||
use crate::ip_tracker::UserIpTracker;
|
||||
use crate::proxy::ClientHandler;
|
||||
@@ -21,6 +21,7 @@ use crate::stats::{ReplayChecker, Stats};
|
||||
use crate::stream::BufferPool;
|
||||
use crate::tls_front::TlsFrontCache;
|
||||
use crate::transport::middle_proxy::MePool;
|
||||
use crate::transport::socket::set_linger_zero;
|
||||
use crate::transport::{ListenOptions, UpstreamManager, create_listener, find_listener_processes};
|
||||
|
||||
use super::helpers::{is_expected_handshake_eof, print_proxy_links};
|
||||
@@ -30,6 +31,19 @@ pub(crate) struct BoundListeners {
|
||||
pub(crate) has_unix_listener: bool,
|
||||
}
|
||||
|
||||
fn listener_port_or_legacy(listener: &crate::config::ListenerConfig, config: &ProxyConfig) -> u16 {
|
||||
listener.port.unwrap_or(config.server.port)
|
||||
}
|
||||
|
||||
fn default_link_port(config: &ProxyConfig) -> u16 {
|
||||
config
|
||||
.server
|
||||
.listeners
|
||||
.first()
|
||||
.and_then(|listener| listener.port)
|
||||
.unwrap_or(config.server.port)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn bind_listeners(
|
||||
config: &Arc<ProxyConfig>,
|
||||
@@ -62,7 +76,8 @@ pub(crate) async fn bind_listeners(
|
||||
let mut listeners = Vec::new();
|
||||
|
||||
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 {
|
||||
warn!(%addr, "Skipping IPv4 listener: IPv4 disabled by [network]");
|
||||
continue;
|
||||
@@ -105,11 +120,7 @@ pub(crate) async fn bind_listeners(
|
||||
if config.general.links.public_host.is_none()
|
||||
&& !config.general.links.show.is_empty()
|
||||
{
|
||||
let link_port = config
|
||||
.general
|
||||
.links
|
||||
.public_port
|
||||
.unwrap_or(config.server.port);
|
||||
let link_port = config.general.links.public_port.unwrap_or(listener_port);
|
||||
print_proxy_links(&public_host, link_port, config);
|
||||
}
|
||||
|
||||
@@ -157,7 +168,7 @@ pub(crate) async fn bind_listeners(
|
||||
.general
|
||||
.links
|
||||
.public_port
|
||||
.unwrap_or(config.server.port),
|
||||
.unwrap_or(default_link_port(config)),
|
||||
)
|
||||
} else {
|
||||
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
|
||||
.links
|
||||
.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 {
|
||||
match listener.accept().await {
|
||||
Ok((stream, peer_addr)) => {
|
||||
let rst_mode = config_rx.borrow().general.rst_on_close;
|
||||
#[cfg(unix)]
|
||||
let raw_fd = {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
stream.as_raw_fd()
|
||||
};
|
||||
if matches!(rst_mode, RstOnCloseMode::Errors | RstOnCloseMode::Always) {
|
||||
let _ = set_linger_zero(&stream);
|
||||
}
|
||||
if !*admission_rx_tcp.borrow() {
|
||||
debug!(peer = %peer_addr, "Admission gate closed, dropping connection");
|
||||
drop(stream);
|
||||
@@ -454,6 +474,9 @@ pub(crate) fn spawn_tcp_accept_loops(
|
||||
shared,
|
||||
proxy_protocol_enabled,
|
||||
real_peer_report_for_handler,
|
||||
#[cfg(unix)]
|
||||
raw_fd,
|
||||
rst_mode,
|
||||
)
|
||||
.run()
|
||||
.await
|
||||
|
||||
@@ -81,23 +81,11 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
async fn run_inner(
|
||||
daemon_opts: DaemonOptions,
|
||||
// Shared maestro startup and main loop. `drop_after_bind` runs on Unix after listeners are bound
|
||||
// (for privilege drop); it is a no-op on other platforms.
|
||||
async fn run_telemt_core(
|
||||
drop_after_bind: impl FnOnce(),
|
||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||
// Acquire PID file if daemonizing or if explicitly requested
|
||||
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
|
||||
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
|
||||
let mut pf = PidFile::new(daemon_opts.pid_file_path());
|
||||
if let Err(e) = pf.acquire() {
|
||||
eprintln!("[telemt] {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
Some(pf)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let process_started_at = Instant::now();
|
||||
let process_started_at_epoch_secs = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -676,6 +664,11 @@ async fn run_inner(
|
||||
));
|
||||
|
||||
let buffer_pool = Arc::new(BufferPool::with_config(64 * 1024, 4096));
|
||||
let shared_state = ProxySharedState::new();
|
||||
shared_state.traffic_limiter.apply_policy(
|
||||
config.access.user_rate_limits.clone(),
|
||||
config.access.cidr_rate_limits.clone(),
|
||||
);
|
||||
|
||||
connectivity::run_startup_connectivity(
|
||||
&config,
|
||||
@@ -707,6 +700,7 @@ async fn run_inner(
|
||||
beobachten.clone(),
|
||||
api_config_tx.clone(),
|
||||
me_pool.clone(),
|
||||
shared_state.clone(),
|
||||
)
|
||||
.await;
|
||||
let config_rx = runtime_watches.config_rx;
|
||||
@@ -723,7 +717,6 @@ async fn run_inner(
|
||||
)
|
||||
.await;
|
||||
let _admission_tx_hold = admission_tx;
|
||||
let shared_state = ProxySharedState::new();
|
||||
conntrack_control::spawn_conntrack_controller(
|
||||
config_rx.clone(),
|
||||
stats.clone(),
|
||||
@@ -761,17 +754,8 @@ async fn run_inner(
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
// Drop privileges after binding sockets (which may require root for port < 1024)
|
||||
if daemon_opts.user.is_some() || daemon_opts.group.is_some() {
|
||||
if let Err(e) = drop_privileges(
|
||||
daemon_opts.user.as_deref(),
|
||||
daemon_opts.group.as_deref(),
|
||||
_pid_file.as_ref(),
|
||||
) {
|
||||
error!(error = %e, "Failed to drop privileges");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
// On Unix, caller supplies privilege drop after bind (may require root for port < 1024).
|
||||
drop_after_bind();
|
||||
|
||||
runtime_tasks::apply_runtime_log_filter(
|
||||
has_rust_log,
|
||||
@@ -819,3 +803,39 @@ async fn run_inner(
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ pub(crate) async fn spawn_runtime_tasks(
|
||||
beobachten: Arc<BeobachtenStore>,
|
||||
api_config_tx: watch::Sender<Arc<ProxyConfig>>,
|
||||
me_pool_for_policy: Option<Arc<MePool>>,
|
||||
shared_state: Arc<ProxySharedState>,
|
||||
) -> RuntimeWatches {
|
||||
let um_clone = upstream_manager.clone();
|
||||
let dc_overrides_for_health = config.dc_overrides.clone();
|
||||
@@ -182,6 +183,41 @@ pub(crate) async fn spawn_runtime_tasks(
|
||||
}
|
||||
});
|
||||
|
||||
let limiter = shared_state.traffic_limiter.clone();
|
||||
limiter.apply_policy(
|
||||
config.access.user_rate_limits.clone(),
|
||||
config.access.cidr_rate_limits.clone(),
|
||||
);
|
||||
let mut config_rx_rate_limits = config_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut prev_user_limits = config_rx_rate_limits
|
||||
.borrow()
|
||||
.access
|
||||
.user_rate_limits
|
||||
.clone();
|
||||
let mut prev_cidr_limits = config_rx_rate_limits
|
||||
.borrow()
|
||||
.access
|
||||
.cidr_rate_limits
|
||||
.clone();
|
||||
loop {
|
||||
if config_rx_rate_limits.changed().await.is_err() {
|
||||
break;
|
||||
}
|
||||
let cfg = config_rx_rate_limits.borrow_and_update().clone();
|
||||
if prev_user_limits != cfg.access.user_rate_limits
|
||||
|| prev_cidr_limits != cfg.access.cidr_rate_limits
|
||||
{
|
||||
limiter.apply_policy(
|
||||
cfg.access.user_rate_limits.clone(),
|
||||
cfg.access.cidr_rate_limits.clone(),
|
||||
);
|
||||
prev_user_limits = cfg.access.user_rate_limits.clone();
|
||||
prev_cidr_limits = cfg.access.cidr_rate_limits.clone();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let beobachten_writer = beobachten.clone();
|
||||
let config_rx_beobachten = config_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
|
||||
@@ -8,6 +8,7 @@ mod crypto;
|
||||
#[cfg(unix)]
|
||||
mod daemon;
|
||||
mod error;
|
||||
mod healthcheck;
|
||||
mod ip_tracker;
|
||||
#[cfg(test)]
|
||||
#[path = "tests/ip_tracker_encapsulation_adversarial_tests.rs"]
|
||||
|
||||
270
src/metrics.rs
270
src/metrics.rs
@@ -575,6 +575,139 @@ async fn render_metrics(
|
||||
}
|
||||
);
|
||||
|
||||
let limiter_metrics = shared_state.traffic_limiter.metrics_snapshot();
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_rate_limiter_throttle_total Traffic limiter throttle events by scope and direction"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_rate_limiter_throttle_total counter");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_throttle_total{{scope=\"user\",direction=\"up\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.user_throttle_up_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_throttle_total{{scope=\"user\",direction=\"down\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.user_throttle_down_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_throttle_total{{scope=\"cidr\",direction=\"up\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.cidr_throttle_up_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_throttle_total{{scope=\"cidr\",direction=\"down\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.cidr_throttle_down_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_rate_limiter_wait_ms_total Traffic limiter accumulated wait time in milliseconds by scope and direction"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_rate_limiter_wait_ms_total counter");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_wait_ms_total{{scope=\"user\",direction=\"up\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.user_wait_up_ms_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_wait_ms_total{{scope=\"user\",direction=\"down\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.user_wait_down_ms_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_wait_ms_total{{scope=\"cidr\",direction=\"up\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.cidr_wait_up_ms_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_wait_ms_total{{scope=\"cidr\",direction=\"down\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.cidr_wait_down_ms_total
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_rate_limiter_active_leases Active relay leases under rate limiting by scope"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_rate_limiter_active_leases gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_active_leases{{scope=\"user\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.user_active_leases
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_active_leases{{scope=\"cidr\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.cidr_active_leases
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_rate_limiter_policy_entries Active rate-limit policy entries by scope"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_rate_limiter_policy_entries gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_policy_entries{{scope=\"user\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.user_policy_entries
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_rate_limiter_policy_entries{{scope=\"cidr\"}} {}",
|
||||
if core_enabled {
|
||||
limiter_metrics.cidr_policy_entries
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_upstream_connect_attempt_total Upstream connect attempts across all requests"
|
||||
@@ -1177,6 +1310,143 @@ async fn render_metrics(
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_me_fair_pressure_state Worker-local fairness pressure state"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_me_fair_pressure_state gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_pressure_state {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_pressure_state_gauge()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_me_fair_active_flows Fair-scheduler active flow count"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_me_fair_active_flows gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_active_flows {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_active_flows_gauge()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_me_fair_queued_bytes Fair-scheduler queued bytes"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_me_fair_queued_bytes gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_queued_bytes {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_queued_bytes_gauge()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_me_fair_flow_state_gauge Fair-scheduler flow health classes"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_me_fair_flow_state_gauge gauge");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_flow_state_gauge{{class=\"standing\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_standing_flows_gauge()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_flow_state_gauge{{class=\"backpressured\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_backpressured_flows_gauge()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"# HELP telemt_me_fair_events_total Fair-scheduler event counters"
|
||||
);
|
||||
let _ = writeln!(out, "# TYPE telemt_me_fair_events_total counter");
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_events_total{{event=\"scheduler_round\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_scheduler_rounds_total()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_events_total{{event=\"deficit_grant\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_deficit_grants_total()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_events_total{{event=\"deficit_skip\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_deficit_skips_total()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_events_total{{event=\"enqueue_reject\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_enqueue_rejects_total()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_events_total{{event=\"shed_drop\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_shed_drops_total()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_events_total{{event=\"penalty\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_penalties_total()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
let _ = writeln!(
|
||||
out,
|
||||
"telemt_me_fair_events_total{{event=\"downstream_stall\"}} {}",
|
||||
if me_allows_normal {
|
||||
stats.get_me_fair_downstream_stalls_total()
|
||||
} else {
|
||||
0
|
||||
}
|
||||
);
|
||||
|
||||
let _ = writeln!(
|
||||
out,
|
||||
|
||||
@@ -97,6 +97,7 @@ pub async fn run_probe(
|
||||
let UpstreamType::Direct {
|
||||
interface,
|
||||
bind_addresses,
|
||||
..
|
||||
} = &upstream.upstream_type
|
||||
else {
|
||||
continue;
|
||||
|
||||
@@ -804,6 +804,9 @@ pub struct RunningClientHandler {
|
||||
beobachten: Arc<BeobachtenStore>,
|
||||
shared: Arc<ProxySharedState>,
|
||||
proxy_protocol_enabled: bool,
|
||||
#[cfg(unix)]
|
||||
raw_fd: std::os::unix::io::RawFd,
|
||||
rst_on_close: crate::config::RstOnCloseMode,
|
||||
}
|
||||
|
||||
impl ClientHandler {
|
||||
@@ -825,6 +828,11 @@ impl ClientHandler {
|
||||
proxy_protocol_enabled: bool,
|
||||
real_peer_report: Arc<std::sync::Mutex<Option<SocketAddr>>>,
|
||||
) -> RunningClientHandler {
|
||||
#[cfg(unix)]
|
||||
let raw_fd = {
|
||||
use std::os::unix::io::AsRawFd;
|
||||
stream.as_raw_fd()
|
||||
};
|
||||
Self::new_with_shared(
|
||||
stream,
|
||||
peer,
|
||||
@@ -842,6 +850,9 @@ impl ClientHandler {
|
||||
ProxySharedState::new(),
|
||||
proxy_protocol_enabled,
|
||||
real_peer_report,
|
||||
#[cfg(unix)]
|
||||
raw_fd,
|
||||
crate::config::RstOnCloseMode::Off,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -863,6 +874,8 @@ impl ClientHandler {
|
||||
shared: Arc<ProxySharedState>,
|
||||
proxy_protocol_enabled: bool,
|
||||
real_peer_report: Arc<std::sync::Mutex<Option<SocketAddr>>>,
|
||||
#[cfg(unix)] raw_fd: std::os::unix::io::RawFd,
|
||||
rst_on_close: crate::config::RstOnCloseMode,
|
||||
) -> RunningClientHandler {
|
||||
let normalized_peer = normalize_ip(peer);
|
||||
RunningClientHandler {
|
||||
@@ -883,6 +896,9 @@ impl ClientHandler {
|
||||
beobachten,
|
||||
shared,
|
||||
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");
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
let raw_fd = self.raw_fd;
|
||||
let rst_on_close = self.rst_on_close;
|
||||
|
||||
let outcome = match self.do_handshake().await? {
|
||||
Some(outcome) => outcome,
|
||||
None => return Ok(()),
|
||||
@@ -908,7 +928,14 @@ impl RunningClientHandler {
|
||||
|
||||
// Phase 2: relay (WITHOUT handshake timeout — relay has its own activity timeouts)
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -316,6 +316,9 @@ where
|
||||
|
||||
stats.increment_user_connects(user);
|
||||
let _direct_connection_lease = stats.acquire_direct_connection_lease();
|
||||
let traffic_lease = shared
|
||||
.traffic_limiter
|
||||
.acquire_lease(user, success.peer.ip());
|
||||
|
||||
let buffer_pool_trim = Arc::clone(&buffer_pool);
|
||||
let relay_activity_timeout = if shared.conntrack_pressure_active() {
|
||||
@@ -329,7 +332,7 @@ where
|
||||
} else {
|
||||
Duration::from_secs(1800)
|
||||
};
|
||||
let relay_result = crate::proxy::relay::relay_bidirectional_with_activity_timeout(
|
||||
let relay_result = crate::proxy::relay::relay_bidirectional_with_activity_timeout_and_lease(
|
||||
client_reader,
|
||||
client_writer,
|
||||
tg_reader,
|
||||
@@ -340,6 +343,7 @@ where
|
||||
Arc::clone(&stats),
|
||||
config.access.user_data_quota.get(user).copied(),
|
||||
buffer_pool,
|
||||
traffic_lease,
|
||||
relay_activity_timeout,
|
||||
);
|
||||
tokio::pin!(relay_result);
|
||||
|
||||
@@ -28,14 +28,10 @@ use tracing::debug;
|
||||
const MASK_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
#[cfg(test)]
|
||||
const MASK_TIMEOUT: Duration = Duration::from_millis(50);
|
||||
/// Maximum duration for the entire masking relay.
|
||||
/// Limits resource consumption from slow-loris attacks and port scanners.
|
||||
#[cfg(not(test))]
|
||||
const MASK_RELAY_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
/// Maximum duration for the entire masking relay under test (replaced by config at runtime).
|
||||
#[cfg(test)]
|
||||
const MASK_RELAY_TIMEOUT: Duration = Duration::from_millis(200);
|
||||
#[cfg(not(test))]
|
||||
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
/// Per-read idle timeout for masking relay and drain paths under test (replaced by config at runtime).
|
||||
#[cfg(test)]
|
||||
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||
const MASK_BUFFER_SIZE: usize = 8192;
|
||||
@@ -55,6 +51,7 @@ async fn copy_with_idle_timeout<R, W>(
|
||||
writer: &mut W,
|
||||
byte_cap: usize,
|
||||
shutdown_on_eof: bool,
|
||||
idle_timeout: Duration,
|
||||
) -> CopyOutcome
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
@@ -78,7 +75,7 @@ where
|
||||
}
|
||||
|
||||
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 {
|
||||
Ok(Ok(n)) => n,
|
||||
Ok(Err(_)) | Err(_) => break,
|
||||
@@ -86,13 +83,13 @@ where
|
||||
if n == 0 {
|
||||
ended_by_eof = true;
|
||||
if shutdown_on_eof {
|
||||
let _ = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.shutdown()).await;
|
||||
let _ = timeout(idle_timeout, writer.shutdown()).await;
|
||||
}
|
||||
break;
|
||||
}
|
||||
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 {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(_)) | Err(_) => break,
|
||||
@@ -230,13 +227,20 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
async fn consume_client_data_with_timeout_and_cap<R>(reader: R, byte_cap: usize)
|
||||
where
|
||||
async fn consume_client_data_with_timeout_and_cap<R>(
|
||||
reader: R,
|
||||
byte_cap: usize,
|
||||
relay_timeout: Duration,
|
||||
idle_timeout: Duration,
|
||||
) where
|
||||
R: AsyncRead + Unpin,
|
||||
{
|
||||
if timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, byte_cap))
|
||||
.await
|
||||
.is_err()
|
||||
if timeout(
|
||||
relay_timeout,
|
||||
consume_client_data(reader, byte_cap, idle_timeout),
|
||||
)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
let relay_timeout = Duration::from_millis(config.censorship.mask_relay_timeout_ms);
|
||||
let idle_timeout = Duration::from_millis(config.censorship.mask_relay_idle_timeout_ms);
|
||||
|
||||
if !config.censorship.mask {
|
||||
// Masking disabled, just consume data
|
||||
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
|
||||
.await;
|
||||
consume_client_data_with_timeout_and_cap(
|
||||
reader,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
relay_timeout,
|
||||
idle_timeout,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -674,7 +686,7 @@ pub async fn handle_bad_client<R, W>(
|
||||
return;
|
||||
}
|
||||
if timeout(
|
||||
MASK_RELAY_TIMEOUT,
|
||||
relay_timeout,
|
||||
relay_to_mask(
|
||||
reader,
|
||||
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_hardening_aggressive_mode,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
idle_timeout,
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -703,6 +716,8 @@ pub async fn handle_bad_client<R, W>(
|
||||
consume_client_data_with_timeout_and_cap(
|
||||
reader,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
relay_timeout,
|
||||
idle_timeout,
|
||||
)
|
||||
.await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
@@ -712,6 +727,8 @@ pub async fn handle_bad_client<R, W>(
|
||||
consume_client_data_with_timeout_and_cap(
|
||||
reader,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
relay_timeout,
|
||||
idle_timeout,
|
||||
)
|
||||
.await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
@@ -742,8 +759,13 @@ pub async fn handle_bad_client<R, W>(
|
||||
local = %local_addr,
|
||||
"Mask target resolves to local listener; refusing self-referential masking fallback"
|
||||
);
|
||||
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
|
||||
.await;
|
||||
consume_client_data_with_timeout_and_cap(
|
||||
reader,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
relay_timeout,
|
||||
idle_timeout,
|
||||
)
|
||||
.await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
return;
|
||||
}
|
||||
@@ -777,7 +799,7 @@ pub async fn handle_bad_client<R, W>(
|
||||
return;
|
||||
}
|
||||
if timeout(
|
||||
MASK_RELAY_TIMEOUT,
|
||||
relay_timeout,
|
||||
relay_to_mask(
|
||||
reader,
|
||||
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_hardening_aggressive_mode,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
idle_timeout,
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -806,6 +829,8 @@ pub async fn handle_bad_client<R, W>(
|
||||
consume_client_data_with_timeout_and_cap(
|
||||
reader,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
relay_timeout,
|
||||
idle_timeout,
|
||||
)
|
||||
.await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
@@ -815,6 +840,8 @@ pub async fn handle_bad_client<R, W>(
|
||||
consume_client_data_with_timeout_and_cap(
|
||||
reader,
|
||||
config.censorship.mask_relay_max_bytes,
|
||||
relay_timeout,
|
||||
idle_timeout,
|
||||
)
|
||||
.await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
@@ -836,6 +863,7 @@ async fn relay_to_mask<R, W, MR, MW>(
|
||||
shape_above_cap_blur_max_bytes: usize,
|
||||
shape_hardening_aggressive_mode: bool,
|
||||
mask_relay_max_bytes: usize,
|
||||
idle_timeout: Duration,
|
||||
) where
|
||||
R: AsyncRead + Unpin + Send + 'static,
|
||||
W: AsyncWrite + Unpin + Send + 'static,
|
||||
@@ -857,11 +885,19 @@ async fn relay_to_mask<R, W, MR, MW>(
|
||||
&mut mask_write,
|
||||
mask_relay_max_bytes,
|
||||
!shape_hardening_enabled,
|
||||
idle_timeout,
|
||||
)
|
||||
.await
|
||||
},
|
||||
async {
|
||||
copy_with_idle_timeout(&mut mask_read, &mut writer, mask_relay_max_bytes, true).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.
|
||||
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 {
|
||||
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 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(Err(_)) | Err(_) => break,
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ use crate::proxy::route_mode::{
|
||||
use crate::proxy::shared_state::{
|
||||
ConntrackCloseEvent, ConntrackClosePublishResult, ConntrackCloseReason, ProxySharedState,
|
||||
};
|
||||
use crate::proxy::traffic_limiter::{RateDirection, TrafficLease, next_refill_delay};
|
||||
use crate::stats::{
|
||||
MeD2cFlushReason, MeD2cQuotaRejectStage, MeD2cWriteMode, QuotaReserveError, Stats, UserStats,
|
||||
};
|
||||
@@ -286,6 +287,10 @@ impl RelayClientIdleState {
|
||||
self.last_client_frame_at = now;
|
||||
self.soft_idle_marked = false;
|
||||
}
|
||||
|
||||
fn on_client_tiny_frame(&mut self, now: Instant) {
|
||||
self.last_client_frame_at = now;
|
||||
}
|
||||
}
|
||||
|
||||
impl MeD2cFlushPolicy {
|
||||
@@ -595,6 +600,41 @@ async fn reserve_user_quota_with_yield(
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_traffic_budget(
|
||||
lease: Option<&Arc<TrafficLease>>,
|
||||
direction: RateDirection,
|
||||
bytes: u64,
|
||||
) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
let Some(lease) = lease else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut remaining = bytes;
|
||||
while remaining > 0 {
|
||||
let consume = lease.try_consume(direction, remaining);
|
||||
if consume.granted > 0 {
|
||||
remaining = remaining.saturating_sub(consume.granted);
|
||||
continue;
|
||||
}
|
||||
|
||||
let wait_started_at = Instant::now();
|
||||
tokio::time::sleep(next_refill_delay()).await;
|
||||
let wait_ms = wait_started_at
|
||||
.elapsed()
|
||||
.as_millis()
|
||||
.min(u128::from(u64::MAX)) as u64;
|
||||
lease.observe_wait_ms(
|
||||
direction,
|
||||
consume.blocked_user,
|
||||
consume.blocked_cidr,
|
||||
wait_ms,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn classify_me_d2c_flush_reason(
|
||||
flush_immediately: bool,
|
||||
batch_frames: usize,
|
||||
@@ -985,6 +1025,7 @@ where
|
||||
let quota_limit = config.access.user_data_quota.get(&user).copied();
|
||||
let quota_user_stats = quota_limit.map(|_| stats.get_or_create_user_stats_handle(&user));
|
||||
let peer = success.peer;
|
||||
let traffic_lease = shared.traffic_limiter.acquire_lease(&user, peer.ip());
|
||||
let proto_tag = success.proto_tag;
|
||||
let pool_generation = me_pool.current_generation();
|
||||
|
||||
@@ -1120,6 +1161,7 @@ where
|
||||
let rng_clone = rng.clone();
|
||||
let user_clone = user.clone();
|
||||
let quota_user_stats_me_writer = quota_user_stats.clone();
|
||||
let traffic_lease_me_writer = traffic_lease.clone();
|
||||
let last_downstream_activity_ms_clone = last_downstream_activity_ms.clone();
|
||||
let bytes_me2c_clone = bytes_me2c.clone();
|
||||
let d2c_flush_policy = MeD2cFlushPolicy::from_config(&config);
|
||||
@@ -1153,7 +1195,7 @@ where
|
||||
|
||||
let first_is_downstream_activity =
|
||||
matches!(&first, MeResponse::Data { .. } | MeResponse::Ack(_));
|
||||
match process_me_writer_response(
|
||||
match process_me_writer_response_with_traffic_lease(
|
||||
first,
|
||||
&mut writer,
|
||||
proto_tag,
|
||||
@@ -1164,6 +1206,7 @@ where
|
||||
quota_user_stats_me_writer.as_deref(),
|
||||
quota_limit,
|
||||
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||
traffic_lease_me_writer.as_ref(),
|
||||
bytes_me2c_clone.as_ref(),
|
||||
conn_id,
|
||||
d2c_flush_policy.ack_flush_immediate,
|
||||
@@ -1213,7 +1256,7 @@ where
|
||||
|
||||
let next_is_downstream_activity =
|
||||
matches!(&next, MeResponse::Data { .. } | MeResponse::Ack(_));
|
||||
match process_me_writer_response(
|
||||
match process_me_writer_response_with_traffic_lease(
|
||||
next,
|
||||
&mut writer,
|
||||
proto_tag,
|
||||
@@ -1224,6 +1267,7 @@ where
|
||||
quota_user_stats_me_writer.as_deref(),
|
||||
quota_limit,
|
||||
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||
traffic_lease_me_writer.as_ref(),
|
||||
bytes_me2c_clone.as_ref(),
|
||||
conn_id,
|
||||
d2c_flush_policy.ack_flush_immediate,
|
||||
@@ -1276,7 +1320,7 @@ where
|
||||
Ok(Some(next)) => {
|
||||
let next_is_downstream_activity =
|
||||
matches!(&next, MeResponse::Data { .. } | MeResponse::Ack(_));
|
||||
match process_me_writer_response(
|
||||
match process_me_writer_response_with_traffic_lease(
|
||||
next,
|
||||
&mut writer,
|
||||
proto_tag,
|
||||
@@ -1287,6 +1331,7 @@ where
|
||||
quota_user_stats_me_writer.as_deref(),
|
||||
quota_limit,
|
||||
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||
traffic_lease_me_writer.as_ref(),
|
||||
bytes_me2c_clone.as_ref(),
|
||||
conn_id,
|
||||
d2c_flush_policy.ack_flush_immediate,
|
||||
@@ -1341,7 +1386,7 @@ where
|
||||
|
||||
let extra_is_downstream_activity =
|
||||
matches!(&extra, MeResponse::Data { .. } | MeResponse::Ack(_));
|
||||
match process_me_writer_response(
|
||||
match process_me_writer_response_with_traffic_lease(
|
||||
extra,
|
||||
&mut writer,
|
||||
proto_tag,
|
||||
@@ -1352,6 +1397,7 @@ where
|
||||
quota_user_stats_me_writer.as_deref(),
|
||||
quota_limit,
|
||||
d2c_flush_policy.quota_soft_overshoot_bytes,
|
||||
traffic_lease_me_writer.as_ref(),
|
||||
bytes_me2c_clone.as_ref(),
|
||||
conn_id,
|
||||
d2c_flush_policy.ack_flush_immediate,
|
||||
@@ -1542,6 +1588,12 @@ where
|
||||
match payload_result {
|
||||
Ok(Some((payload, quickack))) => {
|
||||
trace!(conn_id, bytes = payload.len(), "C->ME frame");
|
||||
wait_for_traffic_budget(
|
||||
traffic_lease.as_ref(),
|
||||
RateDirection::Up,
|
||||
payload.len() as u64,
|
||||
)
|
||||
.await;
|
||||
forensics.bytes_c2me = forensics
|
||||
.bytes_c2me
|
||||
.saturating_add(payload.len() as u64);
|
||||
@@ -1762,40 +1814,6 @@ where
|
||||
let downstream_ms = last_downstream_activity_ms.load(Ordering::Relaxed);
|
||||
let hard_deadline =
|
||||
hard_deadline(idle_policy, idle_state, session_started_at, downstream_ms);
|
||||
if now >= hard_deadline {
|
||||
clear_relay_idle_candidate_in(shared, forensics.conn_id);
|
||||
stats.increment_relay_idle_hard_close_total();
|
||||
let client_idle_secs = now
|
||||
.saturating_duration_since(idle_state.last_client_frame_at)
|
||||
.as_secs();
|
||||
let downstream_idle_secs = now
|
||||
.saturating_duration_since(
|
||||
session_started_at + Duration::from_millis(downstream_ms),
|
||||
)
|
||||
.as_secs();
|
||||
warn!(
|
||||
trace_id = format_args!("0x{:016x}", forensics.trace_id),
|
||||
conn_id = forensics.conn_id,
|
||||
user = %forensics.user,
|
||||
read_label,
|
||||
client_idle_secs,
|
||||
downstream_idle_secs,
|
||||
soft_idle_secs = idle_policy.soft_idle.as_secs(),
|
||||
hard_idle_secs = idle_policy.hard_idle.as_secs(),
|
||||
grace_secs = idle_policy.grace_after_downstream_activity.as_secs(),
|
||||
"Middle-relay hard idle close"
|
||||
);
|
||||
return Err(ProxyError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
format!(
|
||||
"middle-relay hard idle timeout while reading {read_label}: client_idle_secs={client_idle_secs}, downstream_idle_secs={downstream_idle_secs}, soft_idle_secs={}, hard_idle_secs={}, grace_secs={}",
|
||||
idle_policy.soft_idle.as_secs(),
|
||||
idle_policy.hard_idle.as_secs(),
|
||||
idle_policy.grace_after_downstream_activity.as_secs(),
|
||||
),
|
||||
)));
|
||||
}
|
||||
|
||||
if !idle_state.soft_idle_marked
|
||||
&& now.saturating_duration_since(idle_state.last_client_frame_at)
|
||||
>= idle_policy.soft_idle
|
||||
@@ -1850,7 +1868,45 @@ where
|
||||
),
|
||||
)));
|
||||
}
|
||||
Err(_) => {}
|
||||
Err(_) => {
|
||||
let now = Instant::now();
|
||||
let downstream_ms = last_downstream_activity_ms.load(Ordering::Relaxed);
|
||||
let hard_deadline =
|
||||
hard_deadline(idle_policy, idle_state, session_started_at, downstream_ms);
|
||||
if now >= hard_deadline {
|
||||
clear_relay_idle_candidate_in(shared, forensics.conn_id);
|
||||
stats.increment_relay_idle_hard_close_total();
|
||||
let client_idle_secs = now
|
||||
.saturating_duration_since(idle_state.last_client_frame_at)
|
||||
.as_secs();
|
||||
let downstream_idle_secs = now
|
||||
.saturating_duration_since(
|
||||
session_started_at + Duration::from_millis(downstream_ms),
|
||||
)
|
||||
.as_secs();
|
||||
warn!(
|
||||
trace_id = format_args!("0x{:016x}", forensics.trace_id),
|
||||
conn_id = forensics.conn_id,
|
||||
user = %forensics.user,
|
||||
read_label,
|
||||
client_idle_secs,
|
||||
downstream_idle_secs,
|
||||
soft_idle_secs = idle_policy.soft_idle.as_secs(),
|
||||
hard_idle_secs = idle_policy.hard_idle.as_secs(),
|
||||
grace_secs = idle_policy.grace_after_downstream_activity.as_secs(),
|
||||
"Middle-relay hard idle close"
|
||||
);
|
||||
return Err(ProxyError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::TimedOut,
|
||||
format!(
|
||||
"middle-relay hard idle timeout while reading {read_label}: client_idle_secs={client_idle_secs}, downstream_idle_secs={downstream_idle_secs}, soft_idle_secs={}, hard_idle_secs={}, grace_secs={}",
|
||||
idle_policy.soft_idle.as_secs(),
|
||||
idle_policy.hard_idle.as_secs(),
|
||||
idle_policy.grace_after_downstream_activity.as_secs(),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1941,6 +1997,7 @@ where
|
||||
};
|
||||
|
||||
if len == 0 {
|
||||
idle_state.on_client_tiny_frame(Instant::now());
|
||||
idle_state.tiny_frame_debt = idle_state
|
||||
.tiny_frame_debt
|
||||
.saturating_add(TINY_FRAME_DEBT_PER_TINY);
|
||||
@@ -2160,6 +2217,46 @@ async fn process_me_writer_response<W>(
|
||||
ack_flush_immediate: bool,
|
||||
batched: bool,
|
||||
) -> Result<MeWriterResponseOutcome>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
process_me_writer_response_with_traffic_lease(
|
||||
response,
|
||||
client_writer,
|
||||
proto_tag,
|
||||
rng,
|
||||
frame_buf,
|
||||
stats,
|
||||
user,
|
||||
quota_user_stats,
|
||||
quota_limit,
|
||||
quota_soft_overshoot_bytes,
|
||||
None,
|
||||
bytes_me2c,
|
||||
conn_id,
|
||||
ack_flush_immediate,
|
||||
batched,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn process_me_writer_response_with_traffic_lease<W>(
|
||||
response: MeResponse,
|
||||
client_writer: &mut CryptoWriter<W>,
|
||||
proto_tag: ProtoTag,
|
||||
rng: &SecureRandom,
|
||||
frame_buf: &mut Vec<u8>,
|
||||
stats: &Stats,
|
||||
user: &str,
|
||||
quota_user_stats: Option<&UserStats>,
|
||||
quota_limit: Option<u64>,
|
||||
quota_soft_overshoot_bytes: u64,
|
||||
traffic_lease: Option<&Arc<TrafficLease>>,
|
||||
bytes_me2c: &AtomicU64,
|
||||
conn_id: u64,
|
||||
ack_flush_immediate: bool,
|
||||
batched: bool,
|
||||
) -> Result<MeWriterResponseOutcome>
|
||||
where
|
||||
W: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
@@ -2183,6 +2280,7 @@ where
|
||||
});
|
||||
}
|
||||
}
|
||||
wait_for_traffic_budget(traffic_lease, RateDirection::Down, data_len).await;
|
||||
|
||||
let write_mode =
|
||||
match write_client_payload(client_writer, proto_tag, flags, &data, rng, frame_buf)
|
||||
@@ -2220,6 +2318,7 @@ where
|
||||
} else {
|
||||
trace!(conn_id, confirm, "ME->C quickack");
|
||||
}
|
||||
wait_for_traffic_budget(traffic_lease, RateDirection::Down, 4).await;
|
||||
write_client_ack(client_writer, proto_tag, confirm).await?;
|
||||
stats.increment_me_d2c_ack_frames_total();
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@ pub mod relay;
|
||||
pub mod route_mode;
|
||||
pub mod session_eviction;
|
||||
pub mod shared_state;
|
||||
pub mod traffic_limiter;
|
||||
|
||||
pub use client::ClientHandler;
|
||||
#[allow(unused_imports)]
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
//! - `SharedCounters` (atomics) let the watchdog read stats without locking
|
||||
|
||||
use crate::error::{ProxyError, Result};
|
||||
use crate::proxy::traffic_limiter::{RateDirection, TrafficLease, next_refill_delay};
|
||||
use crate::stats::{Stats, UserStats};
|
||||
use crate::stream::BufferPool;
|
||||
use std::io;
|
||||
@@ -61,7 +62,7 @@ use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||
use std::task::{Context, Poll};
|
||||
use std::time::Duration;
|
||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadBuf, copy_bidirectional_with_sizes};
|
||||
use tokio::time::Instant;
|
||||
use tokio::time::{Instant, Sleep};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
// ============= Constants =============
|
||||
@@ -210,12 +211,24 @@ struct StatsIo<S> {
|
||||
stats: Arc<Stats>,
|
||||
user: String,
|
||||
user_stats: Arc<UserStats>,
|
||||
traffic_lease: Option<Arc<TrafficLease>>,
|
||||
c2s_rate_debt_bytes: u64,
|
||||
c2s_wait: RateWaitState,
|
||||
s2c_wait: RateWaitState,
|
||||
quota_limit: Option<u64>,
|
||||
quota_exceeded: Arc<AtomicBool>,
|
||||
quota_bytes_since_check: u64,
|
||||
epoch: Instant,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct RateWaitState {
|
||||
sleep: Option<Pin<Box<Sleep>>>,
|
||||
started_at: Option<Instant>,
|
||||
blocked_user: bool,
|
||||
blocked_cidr: bool,
|
||||
}
|
||||
|
||||
impl<S> StatsIo<S> {
|
||||
fn new(
|
||||
inner: S,
|
||||
@@ -225,6 +238,28 @@ impl<S> StatsIo<S> {
|
||||
quota_limit: Option<u64>,
|
||||
quota_exceeded: Arc<AtomicBool>,
|
||||
epoch: Instant,
|
||||
) -> Self {
|
||||
Self::new_with_traffic_lease(
|
||||
inner,
|
||||
counters,
|
||||
stats,
|
||||
user,
|
||||
None,
|
||||
quota_limit,
|
||||
quota_exceeded,
|
||||
epoch,
|
||||
)
|
||||
}
|
||||
|
||||
fn new_with_traffic_lease(
|
||||
inner: S,
|
||||
counters: Arc<SharedCounters>,
|
||||
stats: Arc<Stats>,
|
||||
user: String,
|
||||
traffic_lease: Option<Arc<TrafficLease>>,
|
||||
quota_limit: Option<u64>,
|
||||
quota_exceeded: Arc<AtomicBool>,
|
||||
epoch: Instant,
|
||||
) -> Self {
|
||||
// Mark initial activity so the watchdog doesn't fire before data flows
|
||||
counters.touch(Instant::now(), epoch);
|
||||
@@ -235,12 +270,88 @@ impl<S> StatsIo<S> {
|
||||
stats,
|
||||
user,
|
||||
user_stats,
|
||||
traffic_lease,
|
||||
c2s_rate_debt_bytes: 0,
|
||||
c2s_wait: RateWaitState::default(),
|
||||
s2c_wait: RateWaitState::default(),
|
||||
quota_limit,
|
||||
quota_exceeded,
|
||||
quota_bytes_since_check: 0,
|
||||
epoch,
|
||||
}
|
||||
}
|
||||
|
||||
fn record_wait(
|
||||
wait: &mut RateWaitState,
|
||||
lease: Option<&Arc<TrafficLease>>,
|
||||
direction: RateDirection,
|
||||
) {
|
||||
let Some(started_at) = wait.started_at.take() else {
|
||||
return;
|
||||
};
|
||||
let wait_ms = started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
|
||||
if let Some(lease) = lease {
|
||||
lease.observe_wait_ms(direction, wait.blocked_user, wait.blocked_cidr, wait_ms);
|
||||
}
|
||||
wait.blocked_user = false;
|
||||
wait.blocked_cidr = false;
|
||||
}
|
||||
|
||||
fn arm_wait(wait: &mut RateWaitState, blocked_user: bool, blocked_cidr: bool) {
|
||||
if wait.sleep.is_none() {
|
||||
wait.sleep = Some(Box::pin(tokio::time::sleep(next_refill_delay())));
|
||||
wait.started_at = Some(Instant::now());
|
||||
}
|
||||
wait.blocked_user |= blocked_user;
|
||||
wait.blocked_cidr |= blocked_cidr;
|
||||
}
|
||||
|
||||
fn poll_wait(
|
||||
wait: &mut RateWaitState,
|
||||
cx: &mut Context<'_>,
|
||||
lease: Option<&Arc<TrafficLease>>,
|
||||
direction: RateDirection,
|
||||
) -> Poll<()> {
|
||||
let Some(sleep) = wait.sleep.as_mut() else {
|
||||
return Poll::Ready(());
|
||||
};
|
||||
if sleep.as_mut().poll(cx).is_pending() {
|
||||
return Poll::Pending;
|
||||
}
|
||||
wait.sleep = None;
|
||||
Self::record_wait(wait, lease, direction);
|
||||
Poll::Ready(())
|
||||
}
|
||||
|
||||
fn settle_c2s_rate_debt(&mut self, cx: &mut Context<'_>) -> Poll<()> {
|
||||
let Some(lease) = self.traffic_lease.as_ref() else {
|
||||
self.c2s_rate_debt_bytes = 0;
|
||||
return Poll::Ready(());
|
||||
};
|
||||
|
||||
while self.c2s_rate_debt_bytes > 0 {
|
||||
let consume = lease.try_consume(RateDirection::Up, self.c2s_rate_debt_bytes);
|
||||
if consume.granted > 0 {
|
||||
self.c2s_rate_debt_bytes = self.c2s_rate_debt_bytes.saturating_sub(consume.granted);
|
||||
continue;
|
||||
}
|
||||
Self::arm_wait(
|
||||
&mut self.c2s_wait,
|
||||
consume.blocked_user,
|
||||
consume.blocked_cidr,
|
||||
);
|
||||
if Self::poll_wait(&mut self.c2s_wait, cx, Some(lease), RateDirection::Up).is_pending()
|
||||
{
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
|
||||
if Self::poll_wait(&mut self.c2s_wait, cx, Some(lease), RateDirection::Up).is_pending() {
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
Poll::Ready(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -286,6 +397,25 @@ fn should_immediate_quota_check(remaining_before: u64, charge_bytes: u64) -> boo
|
||||
remaining_before <= QUOTA_NEAR_LIMIT_BYTES || charge_bytes >= QUOTA_LARGE_CHARGE_BYTES
|
||||
}
|
||||
|
||||
fn refund_reserved_quota_bytes(user_stats: &UserStats, reserved_bytes: u64) {
|
||||
if reserved_bytes == 0 {
|
||||
return;
|
||||
}
|
||||
let mut current = user_stats.quota_used.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let next = current.saturating_sub(reserved_bytes);
|
||||
match user_stats.quota_used.compare_exchange_weak(
|
||||
current,
|
||||
next,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => return,
|
||||
Err(observed) => current = observed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
||||
fn poll_read(
|
||||
self: Pin<&mut Self>,
|
||||
@@ -296,6 +426,9 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
||||
if this.quota_exceeded.load(Ordering::Acquire) {
|
||||
return Poll::Ready(Err(quota_io_error()));
|
||||
}
|
||||
if this.settle_c2s_rate_debt(cx).is_pending() {
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
let mut remaining_before = None;
|
||||
if let Some(limit) = this.quota_limit {
|
||||
@@ -377,6 +510,11 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
|
||||
.add_user_octets_from_handle(this.user_stats.as_ref(), n_to_charge);
|
||||
this.stats
|
||||
.increment_user_msgs_from_handle(this.user_stats.as_ref());
|
||||
if this.traffic_lease.is_some() {
|
||||
this.c2s_rate_debt_bytes =
|
||||
this.c2s_rate_debt_bytes.saturating_add(n_to_charge);
|
||||
let _ = this.settle_c2s_rate_debt(cx);
|
||||
}
|
||||
|
||||
trace!(user = %this.user, bytes = n, "C->S");
|
||||
}
|
||||
@@ -398,28 +536,66 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
||||
return Poll::Ready(Err(quota_io_error()));
|
||||
}
|
||||
|
||||
let mut shaper_reserved_bytes = 0u64;
|
||||
let mut write_buf = buf;
|
||||
if let Some(lease) = this.traffic_lease.as_ref() {
|
||||
if !buf.is_empty() {
|
||||
loop {
|
||||
let consume = lease.try_consume(RateDirection::Down, buf.len() as u64);
|
||||
if consume.granted > 0 {
|
||||
shaper_reserved_bytes = consume.granted;
|
||||
if consume.granted < buf.len() as u64 {
|
||||
write_buf = &buf[..consume.granted as usize];
|
||||
}
|
||||
let _ = Self::poll_wait(
|
||||
&mut this.s2c_wait,
|
||||
cx,
|
||||
Some(lease),
|
||||
RateDirection::Down,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
Self::arm_wait(
|
||||
&mut this.s2c_wait,
|
||||
consume.blocked_user,
|
||||
consume.blocked_cidr,
|
||||
);
|
||||
if Self::poll_wait(&mut this.s2c_wait, cx, Some(lease), RateDirection::Down)
|
||||
.is_pending()
|
||||
{
|
||||
return Poll::Pending;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let _ = Self::poll_wait(&mut this.s2c_wait, cx, Some(lease), RateDirection::Down);
|
||||
}
|
||||
}
|
||||
|
||||
let mut remaining_before = None;
|
||||
let mut reserved_bytes = 0u64;
|
||||
let mut write_buf = buf;
|
||||
if let Some(limit) = this.quota_limit {
|
||||
if !buf.is_empty() {
|
||||
if !write_buf.is_empty() {
|
||||
let mut reserve_rounds = 0usize;
|
||||
while reserved_bytes == 0 {
|
||||
let used_before = this.user_stats.quota_used();
|
||||
let remaining = limit.saturating_sub(used_before);
|
||||
if remaining == 0 {
|
||||
if let Some(lease) = this.traffic_lease.as_ref() {
|
||||
lease.refund(RateDirection::Down, shaper_reserved_bytes);
|
||||
}
|
||||
this.quota_exceeded.store(true, Ordering::Release);
|
||||
return Poll::Ready(Err(quota_io_error()));
|
||||
}
|
||||
remaining_before = Some(remaining);
|
||||
|
||||
let desired = remaining.min(buf.len() as u64);
|
||||
let desired = remaining.min(write_buf.len() as u64);
|
||||
let mut saw_contention = false;
|
||||
for _ in 0..QUOTA_RESERVE_SPIN_RETRIES {
|
||||
match this.user_stats.quota_try_reserve(desired, limit) {
|
||||
Ok(_) => {
|
||||
reserved_bytes = desired;
|
||||
write_buf = &buf[..desired as usize];
|
||||
write_buf = &write_buf[..desired as usize];
|
||||
break;
|
||||
}
|
||||
Err(crate::stats::QuotaReserveError::LimitExceeded) => {
|
||||
@@ -434,6 +610,9 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
||||
if reserved_bytes == 0 {
|
||||
reserve_rounds = reserve_rounds.saturating_add(1);
|
||||
if reserve_rounds >= QUOTA_RESERVE_MAX_ROUNDS {
|
||||
if let Some(lease) = this.traffic_lease.as_ref() {
|
||||
lease.refund(RateDirection::Down, shaper_reserved_bytes);
|
||||
}
|
||||
this.quota_exceeded.store(true, Ordering::Release);
|
||||
return Poll::Ready(Err(quota_io_error()));
|
||||
}
|
||||
@@ -446,6 +625,9 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
||||
let used_before = this.user_stats.quota_used();
|
||||
let remaining = limit.saturating_sub(used_before);
|
||||
if remaining == 0 {
|
||||
if let Some(lease) = this.traffic_lease.as_ref() {
|
||||
lease.refund(RateDirection::Down, shaper_reserved_bytes);
|
||||
}
|
||||
this.quota_exceeded.store(true, Ordering::Release);
|
||||
return Poll::Ready(Err(quota_io_error()));
|
||||
}
|
||||
@@ -456,23 +638,20 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
||||
match Pin::new(&mut this.inner).poll_write(cx, write_buf) {
|
||||
Poll::Ready(Ok(n)) => {
|
||||
if reserved_bytes > n as u64 {
|
||||
let refund = reserved_bytes - n as u64;
|
||||
let mut current = this.user_stats.quota_used.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let next = current.saturating_sub(refund);
|
||||
match this.user_stats.quota_used.compare_exchange_weak(
|
||||
current,
|
||||
next,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(observed) => current = observed,
|
||||
}
|
||||
}
|
||||
refund_reserved_quota_bytes(
|
||||
this.user_stats.as_ref(),
|
||||
reserved_bytes - n as u64,
|
||||
);
|
||||
}
|
||||
if shaper_reserved_bytes > n as u64
|
||||
&& let Some(lease) = this.traffic_lease.as_ref()
|
||||
{
|
||||
lease.refund(RateDirection::Down, shaper_reserved_bytes - n as u64);
|
||||
}
|
||||
|
||||
if n > 0 {
|
||||
if let Some(lease) = this.traffic_lease.as_ref() {
|
||||
Self::record_wait(&mut this.s2c_wait, Some(lease), RateDirection::Down);
|
||||
}
|
||||
let n_to_charge = n as u64;
|
||||
|
||||
// S→C: data written to client
|
||||
@@ -512,37 +691,23 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
||||
}
|
||||
Poll::Ready(Err(err)) => {
|
||||
if reserved_bytes > 0 {
|
||||
let mut current = this.user_stats.quota_used.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let next = current.saturating_sub(reserved_bytes);
|
||||
match this.user_stats.quota_used.compare_exchange_weak(
|
||||
current,
|
||||
next,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(observed) => current = observed,
|
||||
}
|
||||
}
|
||||
refund_reserved_quota_bytes(this.user_stats.as_ref(), reserved_bytes);
|
||||
}
|
||||
if shaper_reserved_bytes > 0
|
||||
&& let Some(lease) = this.traffic_lease.as_ref()
|
||||
{
|
||||
lease.refund(RateDirection::Down, shaper_reserved_bytes);
|
||||
}
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
Poll::Pending => {
|
||||
if reserved_bytes > 0 {
|
||||
let mut current = this.user_stats.quota_used.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let next = current.saturating_sub(reserved_bytes);
|
||||
match this.user_stats.quota_used.compare_exchange_weak(
|
||||
current,
|
||||
next,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(observed) => current = observed,
|
||||
}
|
||||
}
|
||||
refund_reserved_quota_bytes(this.user_stats.as_ref(), reserved_bytes);
|
||||
}
|
||||
if shaper_reserved_bytes > 0
|
||||
&& let Some(lease) = this.traffic_lease.as_ref()
|
||||
{
|
||||
lease.refund(RateDirection::Down, shaper_reserved_bytes);
|
||||
}
|
||||
Poll::Pending
|
||||
}
|
||||
@@ -627,6 +792,43 @@ pub async fn relay_bidirectional_with_activity_timeout<CR, CW, SR, SW>(
|
||||
_buffer_pool: Arc<BufferPool>,
|
||||
activity_timeout: Duration,
|
||||
) -> Result<()>
|
||||
where
|
||||
CR: AsyncRead + Unpin + Send + 'static,
|
||||
CW: AsyncWrite + Unpin + Send + 'static,
|
||||
SR: AsyncRead + Unpin + Send + 'static,
|
||||
SW: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
relay_bidirectional_with_activity_timeout_and_lease(
|
||||
client_reader,
|
||||
client_writer,
|
||||
server_reader,
|
||||
server_writer,
|
||||
c2s_buf_size,
|
||||
s2c_buf_size,
|
||||
user,
|
||||
stats,
|
||||
quota_limit,
|
||||
_buffer_pool,
|
||||
None,
|
||||
activity_timeout,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn relay_bidirectional_with_activity_timeout_and_lease<CR, CW, SR, SW>(
|
||||
client_reader: CR,
|
||||
client_writer: CW,
|
||||
server_reader: SR,
|
||||
server_writer: SW,
|
||||
c2s_buf_size: usize,
|
||||
s2c_buf_size: usize,
|
||||
user: &str,
|
||||
stats: Arc<Stats>,
|
||||
quota_limit: Option<u64>,
|
||||
_buffer_pool: Arc<BufferPool>,
|
||||
traffic_lease: Option<Arc<TrafficLease>>,
|
||||
activity_timeout: Duration,
|
||||
) -> Result<()>
|
||||
where
|
||||
CR: AsyncRead + Unpin + Send + 'static,
|
||||
CW: AsyncWrite + Unpin + Send + 'static,
|
||||
@@ -644,11 +846,12 @@ where
|
||||
let mut server = CombinedStream::new(server_reader, server_writer);
|
||||
|
||||
// Wrap client with stats/activity tracking
|
||||
let mut client = StatsIo::new(
|
||||
let mut client = StatsIo::new_with_traffic_lease(
|
||||
client_combined,
|
||||
Arc::clone(&counters),
|
||||
Arc::clone(&stats),
|
||||
user_owned.clone(),
|
||||
traffic_lease,
|
||||
quota_limit,
|
||||
Arc::clone("a_exceeded),
|
||||
epoch,
|
||||
|
||||
@@ -10,6 +10,7 @@ use tokio::sync::mpsc;
|
||||
|
||||
use crate::proxy::handshake::{AuthProbeSaturationState, AuthProbeState};
|
||||
use crate::proxy::middle_relay::{DesyncDedupRotationState, RelayIdleCandidateRegistry};
|
||||
use crate::proxy::traffic_limiter::TrafficLimiter;
|
||||
|
||||
const HANDSHAKE_RECENT_USER_RING_LEN: usize = 64;
|
||||
|
||||
@@ -65,6 +66,7 @@ pub(crate) struct MiddleRelaySharedState {
|
||||
pub(crate) struct ProxySharedState {
|
||||
pub(crate) handshake: HandshakeSharedState,
|
||||
pub(crate) middle_relay: MiddleRelaySharedState,
|
||||
pub(crate) traffic_limiter: Arc<TrafficLimiter>,
|
||||
pub(crate) conntrack_pressure_active: AtomicBool,
|
||||
pub(crate) conntrack_close_tx: Mutex<Option<mpsc::Sender<ConntrackCloseEvent>>>,
|
||||
}
|
||||
@@ -98,6 +100,7 @@ impl ProxySharedState {
|
||||
relay_idle_registry: Mutex::new(RelayIdleCandidateRegistry::default()),
|
||||
relay_idle_mark_seq: AtomicU64::new(0),
|
||||
},
|
||||
traffic_limiter: TrafficLimiter::new(),
|
||||
conntrack_pressure_active: AtomicBool::new(false),
|
||||
conntrack_close_tx: Mutex::new(None),
|
||||
})
|
||||
|
||||
@@ -31,11 +31,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -27,11 +27,14 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -25,11 +25,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -38,11 +38,14 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -16,11 +16,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -39,11 +39,14 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -229,11 +232,14 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -470,11 +476,14 @@ async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sen
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -544,11 +553,14 @@ async fn capture_forwarded_probe_len(tls_len: u16, body_sent: usize) -> usize {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -13,11 +13,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -11,11 +11,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -25,11 +25,14 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -332,11 +332,14 @@ async fn relay_task_abort_releases_user_gate_and_ip_reservation() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -446,11 +449,14 @@ async fn relay_cutover_releases_user_gate_and_ip_reservation() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -570,11 +576,14 @@ async fn integration_route_cutover_and_quota_overlap_fails_closed_and_releases_s
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -740,11 +749,14 @@ async fn proxy_protocol_header_is_rejected_when_trust_list_is_empty() {
|
||||
upstream_type: crate::config::UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -817,11 +829,14 @@ async fn proxy_protocol_header_from_untrusted_peer_range_is_rejected_under_load(
|
||||
upstream_type: crate::config::UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -977,11 +992,14 @@ async fn short_tls_probe_is_masked_through_client_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1065,11 +1083,14 @@ async fn tls12_record_probe_is_masked_through_client_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1151,11 +1172,14 @@ async fn handle_client_stream_increments_connects_all_exactly_once() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1244,11 +1268,14 @@ async fn running_client_handler_increments_connects_all_exactly_once() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1334,11 +1361,14 @@ async fn idle_pooled_connection_closes_cleanly_in_generic_stream_path() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1405,11 +1435,14 @@ async fn idle_pooled_connection_closes_cleanly_in_client_handler_path() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1491,11 +1524,14 @@ async fn partial_tls_header_stall_triggers_handshake_timeout() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1816,11 +1852,14 @@ async fn valid_tls_path_does_not_fall_back_to_mask_backend() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1925,11 +1964,14 @@ async fn valid_tls_with_invalid_mtproto_falls_back_to_mask_backend() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -2032,11 +2074,14 @@ async fn client_handler_tls_bad_mtproto_is_forwarded_to_mask_backend() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -2154,11 +2199,14 @@ async fn alpn_mismatch_tls_probe_is_masked_through_client_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -2247,11 +2295,14 @@ async fn invalid_hmac_tls_probe_is_masked_through_client_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -2346,11 +2397,14 @@ async fn burst_invalid_tls_probes_are_masked_verbatim() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -3251,11 +3305,14 @@ async fn relay_connect_error_releases_user_and_ip_before_return() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -3812,11 +3869,14 @@ async fn untrusted_proxy_header_source_is_rejected() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -3882,11 +3942,14 @@ async fn empty_proxy_trusted_cidrs_rejects_proxy_header_by_default() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -3979,11 +4042,14 @@ async fn oversized_tls_record_is_masked_in_generic_stream_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -4082,11 +4148,14 @@ async fn oversized_tls_record_is_masked_in_client_handler_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -4199,11 +4268,14 @@ async fn tls_record_len_min_minus_1_is_rejected_in_generic_stream_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -4302,11 +4374,14 @@ async fn tls_record_len_min_minus_1_is_rejected_in_client_handler_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -4408,11 +4483,14 @@ async fn tls_record_len_16384_is_accepted_in_generic_stream_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -4509,11 +4587,14 @@ async fn tls_record_len_16384_is_accepted_in_client_handler_pipeline() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -24,11 +24,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -26,11 +26,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -27,11 +27,14 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -41,11 +41,14 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
@@ -1293,11 +1293,14 @@ async fn direct_relay_abort_midflight_releases_route_gauge() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1400,11 +1403,14 @@ async fn direct_relay_cutover_midflight_releases_route_gauge() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1522,11 +1528,14 @@ async fn direct_relay_cutover_storm_multi_session_keeps_generic_errors_and_relea
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
@@ -1758,8 +1767,11 @@ async fn negative_direct_relay_dc_connection_refused_fails_fast() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
100,
|
||||
@@ -1849,8 +1861,11 @@ async fn adversarial_direct_relay_cutover_integrity() {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
100,
|
||||
|
||||
@@ -47,7 +47,7 @@ async fn consume_client_data_stops_after_byte_cap_without_eof() {
|
||||
};
|
||||
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);
|
||||
assert!(
|
||||
|
||||
@@ -31,7 +31,7 @@ async fn stalling_client_terminates_at_idle_not_relay_timeout() {
|
||||
|
||||
let result = tokio::time::timeout(
|
||||
MASK_RELAY_TIMEOUT,
|
||||
consume_client_data(reader, MASK_BUFFER_SIZE * 4),
|
||||
consume_client_data(reader, MASK_BUFFER_SIZE * 4, MASK_RELAY_IDLE_TIMEOUT),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -57,9 +57,12 @@ async fn fast_reader_drains_to_eof() {
|
||||
let data = vec![0xAAu8; 32 * 1024];
|
||||
let reader = std::io::Cursor::new(data);
|
||||
|
||||
tokio::time::timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, usize::MAX))
|
||||
.await
|
||||
.expect("consume_client_data did not complete for fast EOF reader");
|
||||
tokio::time::timeout(
|
||||
MASK_RELAY_TIMEOUT,
|
||||
consume_client_data(reader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
.expect("consume_client_data did not complete for fast EOF reader");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -81,7 +84,7 @@ async fn io_error_terminates_cleanly() {
|
||||
|
||||
tokio::time::timeout(
|
||||
MASK_RELAY_TIMEOUT,
|
||||
consume_client_data(ErrReader, usize::MAX),
|
||||
consume_client_data(ErrReader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
|
||||
)
|
||||
.await
|
||||
.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 {
|
||||
tokio::time::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
|
||||
.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]
|
||||
async fn consume_zero_cap_returns_immediately() {
|
||||
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!(
|
||||
started.elapsed() < MASK_RELAY_IDLE_TIMEOUT,
|
||||
"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 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, 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 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!(
|
||||
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 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!(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() {
|
||||
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!(
|
||||
started.elapsed() < Duration::from_millis(350),
|
||||
@@ -190,7 +216,12 @@ async fn integration_consume_path_honors_production_cap_for_large_payload() {
|
||||
|
||||
let bounded = timeout(
|
||||
Duration::from_millis(350),
|
||||
consume_client_data_with_timeout_and_cap(reader, PROD_CAP_BYTES),
|
||||
consume_client_data_with_timeout_and_cap(
|
||||
reader,
|
||||
PROD_CAP_BYTES,
|
||||
MASK_RELAY_TIMEOUT,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
),
|
||||
)
|
||||
.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 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!(
|
||||
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 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);
|
||||
|
||||
assert_eq!(
|
||||
@@ -261,7 +300,14 @@ async fn stress_parallel_copy_tasks_with_production_cap_complete_without_leaks()
|
||||
read_calls,
|
||||
);
|
||||
let mut writer = CountingWriter::default();
|
||||
copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).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,
|
||||
false,
|
||||
32 * 1024,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
@@ -81,6 +82,7 @@ async fn relay_to_mask_propagates_client_half_close_without_waiting_for_other_di
|
||||
0,
|
||||
false,
|
||||
32 * 1024,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
@@ -1377,6 +1377,7 @@ async fn relay_to_mask_keeps_backend_to_client_flow_when_client_to_backend_stall
|
||||
0,
|
||||
false,
|
||||
5 * 1024 * 1024,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
@@ -1508,6 +1509,7 @@ async fn relay_to_mask_timeout_cancels_and_drops_all_io_endpoints() {
|
||||
0,
|
||||
false,
|
||||
5 * 1024 * 1024,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -228,6 +228,7 @@ async fn relay_path_idle_timeout_eviction_remains_effective() {
|
||||
0,
|
||||
false,
|
||||
5 * 1024 * 1024,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ async fn run_relay_case(
|
||||
above_cap_blur_max_bytes,
|
||||
false,
|
||||
5 * 1024 * 1024,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
@@ -89,6 +89,7 @@ async fn relay_to_mask_applies_cap_clamped_padding_for_non_power_of_two_cap() {
|
||||
0,
|
||||
false,
|
||||
5 * 1024 * 1024,
|
||||
MASK_RELAY_IDLE_TIMEOUT,
|
||||
)
|
||||
.await;
|
||||
});
|
||||
|
||||
@@ -53,11 +53,14 @@ fn new_client_harness() -> ClientHarness {
|
||||
upstream_type: UpstreamType::Direct {
|
||||
interface: None,
|
||||
bind_addresses: None,
|
||||
bindtodevice: None,
|
||||
},
|
||||
weight: 1,
|
||||
enabled: true,
|
||||
scopes: String::new(),
|
||||
selected_scope: String::new(),
|
||||
ipv4: None,
|
||||
ipv6: None,
|
||||
}],
|
||||
1,
|
||||
1,
|
||||
|
||||
853
src/proxy/traffic_limiter.rs
Normal file
853
src/proxy/traffic_limiter.rs
Normal file
@@ -0,0 +1,853 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use std::sync::OnceLock;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use dashmap::DashMap;
|
||||
use ipnetwork::IpNetwork;
|
||||
|
||||
use crate::config::RateLimitBps;
|
||||
|
||||
const REGISTRY_SHARDS: usize = 64;
|
||||
const FAIR_EPOCH_MS: u64 = 20;
|
||||
const MAX_BORROW_CHUNK_BYTES: u64 = 32 * 1024;
|
||||
const CLEANUP_INTERVAL_SECS: u64 = 60;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RateDirection {
|
||||
Up,
|
||||
Down,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TrafficConsumeResult {
|
||||
pub granted: u64,
|
||||
pub blocked_user: bool,
|
||||
pub blocked_cidr: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TrafficLimiterMetricsSnapshot {
|
||||
pub user_throttle_up_total: u64,
|
||||
pub user_throttle_down_total: u64,
|
||||
pub cidr_throttle_up_total: u64,
|
||||
pub cidr_throttle_down_total: u64,
|
||||
pub user_wait_up_ms_total: u64,
|
||||
pub user_wait_down_ms_total: u64,
|
||||
pub cidr_wait_up_ms_total: u64,
|
||||
pub cidr_wait_down_ms_total: u64,
|
||||
pub user_active_leases: u64,
|
||||
pub cidr_active_leases: u64,
|
||||
pub user_policy_entries: u64,
|
||||
pub cidr_policy_entries: u64,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ScopeMetrics {
|
||||
throttle_up_total: AtomicU64,
|
||||
throttle_down_total: AtomicU64,
|
||||
wait_up_ms_total: AtomicU64,
|
||||
wait_down_ms_total: AtomicU64,
|
||||
active_leases: AtomicU64,
|
||||
policy_entries: AtomicU64,
|
||||
}
|
||||
|
||||
impl ScopeMetrics {
|
||||
fn throttle(&self, direction: RateDirection) {
|
||||
match direction {
|
||||
RateDirection::Up => {
|
||||
self.throttle_up_total.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
RateDirection::Down => {
|
||||
self.throttle_down_total.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn wait_ms(&self, direction: RateDirection, wait_ms: u64) {
|
||||
match direction {
|
||||
RateDirection::Up => {
|
||||
self.wait_up_ms_total.fetch_add(wait_ms, Ordering::Relaxed);
|
||||
}
|
||||
RateDirection::Down => {
|
||||
self.wait_down_ms_total
|
||||
.fetch_add(wait_ms, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct AtomicRatePair {
|
||||
up_bps: AtomicU64,
|
||||
down_bps: AtomicU64,
|
||||
}
|
||||
|
||||
impl AtomicRatePair {
|
||||
fn set(&self, limits: RateLimitBps) {
|
||||
self.up_bps.store(limits.up_bps, Ordering::Relaxed);
|
||||
self.down_bps.store(limits.down_bps, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
fn get(&self, direction: RateDirection) -> u64 {
|
||||
match direction {
|
||||
RateDirection::Up => self.up_bps.load(Ordering::Relaxed),
|
||||
RateDirection::Down => self.down_bps.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct DirectionBucket {
|
||||
epoch: AtomicU64,
|
||||
used: AtomicU64,
|
||||
}
|
||||
|
||||
impl DirectionBucket {
|
||||
fn sync_epoch(&self, epoch: u64) {
|
||||
let current = self.epoch.load(Ordering::Relaxed);
|
||||
if current == epoch {
|
||||
return;
|
||||
}
|
||||
if current < epoch
|
||||
&& self
|
||||
.epoch
|
||||
.compare_exchange(current, epoch, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.used.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_consume(&self, cap_bps: u64, requested: u64) -> u64 {
|
||||
if requested == 0 {
|
||||
return 0;
|
||||
}
|
||||
if cap_bps == 0 {
|
||||
return requested;
|
||||
}
|
||||
|
||||
let epoch = current_epoch();
|
||||
self.sync_epoch(epoch);
|
||||
let cap_epoch = bytes_per_epoch(cap_bps);
|
||||
|
||||
loop {
|
||||
let used = self.used.load(Ordering::Relaxed);
|
||||
if used >= cap_epoch {
|
||||
return 0;
|
||||
}
|
||||
let remaining = cap_epoch.saturating_sub(used);
|
||||
let grant = requested.min(remaining);
|
||||
if grant == 0 {
|
||||
return 0;
|
||||
}
|
||||
let next = used.saturating_add(grant);
|
||||
if self
|
||||
.used
|
||||
.compare_exchange_weak(used, next, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
return grant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refund(&self, bytes: u64) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
decrement_atomic_saturating(&self.used, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
struct UserBucket {
|
||||
rates: AtomicRatePair,
|
||||
up: DirectionBucket,
|
||||
down: DirectionBucket,
|
||||
active_leases: AtomicU64,
|
||||
}
|
||||
|
||||
impl UserBucket {
|
||||
fn new(limits: RateLimitBps) -> Self {
|
||||
let rates = AtomicRatePair::default();
|
||||
rates.set(limits);
|
||||
Self {
|
||||
rates,
|
||||
up: DirectionBucket::default(),
|
||||
down: DirectionBucket::default(),
|
||||
active_leases: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_rates(&self, limits: RateLimitBps) {
|
||||
self.rates.set(limits);
|
||||
}
|
||||
|
||||
fn try_consume(&self, direction: RateDirection, requested: u64) -> u64 {
|
||||
let cap_bps = self.rates.get(direction);
|
||||
match direction {
|
||||
RateDirection::Up => self.up.try_consume(cap_bps, requested),
|
||||
RateDirection::Down => self.down.try_consume(cap_bps, requested),
|
||||
}
|
||||
}
|
||||
|
||||
fn refund(&self, direction: RateDirection, bytes: u64) {
|
||||
match direction {
|
||||
RateDirection::Up => self.up.refund(bytes),
|
||||
RateDirection::Down => self.down.refund(bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CidrDirectionBucket {
|
||||
epoch: AtomicU64,
|
||||
used: AtomicU64,
|
||||
active_users: AtomicU64,
|
||||
}
|
||||
|
||||
impl CidrDirectionBucket {
|
||||
fn sync_epoch(&self, epoch: u64) {
|
||||
let current = self.epoch.load(Ordering::Relaxed);
|
||||
if current == epoch {
|
||||
return;
|
||||
}
|
||||
if current < epoch
|
||||
&& self
|
||||
.epoch
|
||||
.compare_exchange(current, epoch, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.used.store(0, Ordering::Relaxed);
|
||||
self.active_users.store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn try_consume(
|
||||
&self,
|
||||
user_state: &CidrUserDirectionState,
|
||||
cap_epoch: u64,
|
||||
requested: u64,
|
||||
) -> u64 {
|
||||
if requested == 0 || cap_epoch == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let epoch = current_epoch();
|
||||
self.sync_epoch(epoch);
|
||||
user_state.sync_epoch_and_mark_active(epoch, &self.active_users);
|
||||
let active_users = self.active_users.load(Ordering::Relaxed).max(1);
|
||||
let fair_share = cap_epoch.saturating_div(active_users).max(1);
|
||||
|
||||
loop {
|
||||
let total_used = self.used.load(Ordering::Relaxed);
|
||||
if total_used >= cap_epoch {
|
||||
return 0;
|
||||
}
|
||||
let total_remaining = cap_epoch.saturating_sub(total_used);
|
||||
let user_used = user_state.used.load(Ordering::Relaxed);
|
||||
let guaranteed_remaining = fair_share.saturating_sub(user_used);
|
||||
|
||||
let grant = if guaranteed_remaining > 0 {
|
||||
requested.min(guaranteed_remaining).min(total_remaining)
|
||||
} else {
|
||||
requested.min(total_remaining).min(MAX_BORROW_CHUNK_BYTES)
|
||||
};
|
||||
|
||||
if grant == 0 {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let next_total = total_used.saturating_add(grant);
|
||||
if self
|
||||
.used
|
||||
.compare_exchange_weak(total_used, next_total, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
user_state.used.fetch_add(grant, Ordering::Relaxed);
|
||||
return grant;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn refund(&self, bytes: u64) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
decrement_atomic_saturating(&self.used, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct CidrUserDirectionState {
|
||||
epoch: AtomicU64,
|
||||
used: AtomicU64,
|
||||
}
|
||||
|
||||
impl CidrUserDirectionState {
|
||||
fn sync_epoch_and_mark_active(&self, epoch: u64, active_users: &AtomicU64) {
|
||||
let current = self.epoch.load(Ordering::Relaxed);
|
||||
if current == epoch {
|
||||
return;
|
||||
}
|
||||
if current < epoch
|
||||
&& self
|
||||
.epoch
|
||||
.compare_exchange(current, epoch, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_ok()
|
||||
{
|
||||
self.used.store(0, Ordering::Relaxed);
|
||||
active_users.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn refund(&self, bytes: u64) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
decrement_atomic_saturating(&self.used, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
struct CidrUserShare {
|
||||
active_conns: AtomicU64,
|
||||
up: CidrUserDirectionState,
|
||||
down: CidrUserDirectionState,
|
||||
}
|
||||
|
||||
impl CidrUserShare {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
active_conns: AtomicU64::new(0),
|
||||
up: CidrUserDirectionState::default(),
|
||||
down: CidrUserDirectionState::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CidrBucket {
|
||||
rates: AtomicRatePair,
|
||||
up: CidrDirectionBucket,
|
||||
down: CidrDirectionBucket,
|
||||
users: ShardedRegistry<CidrUserShare>,
|
||||
active_leases: AtomicU64,
|
||||
}
|
||||
|
||||
impl CidrBucket {
|
||||
fn new(limits: RateLimitBps) -> Self {
|
||||
let rates = AtomicRatePair::default();
|
||||
rates.set(limits);
|
||||
Self {
|
||||
rates,
|
||||
up: CidrDirectionBucket::default(),
|
||||
down: CidrDirectionBucket::default(),
|
||||
users: ShardedRegistry::new(REGISTRY_SHARDS),
|
||||
active_leases: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_rates(&self, limits: RateLimitBps) {
|
||||
self.rates.set(limits);
|
||||
}
|
||||
|
||||
fn acquire_user_share(&self, user: &str) -> Arc<CidrUserShare> {
|
||||
let share = self.users.get_or_insert_with(user, CidrUserShare::new);
|
||||
share.active_conns.fetch_add(1, Ordering::Relaxed);
|
||||
share
|
||||
}
|
||||
|
||||
fn release_user_share(&self, user: &str, share: &Arc<CidrUserShare>) {
|
||||
decrement_atomic_saturating(&share.active_conns, 1);
|
||||
let share_for_remove = Arc::clone(share);
|
||||
let _ = self.users.remove_if(user, |candidate| {
|
||||
Arc::ptr_eq(candidate, &share_for_remove)
|
||||
&& candidate.active_conns.load(Ordering::Relaxed) == 0
|
||||
});
|
||||
}
|
||||
|
||||
fn try_consume_for_user(
|
||||
&self,
|
||||
direction: RateDirection,
|
||||
share: &CidrUserShare,
|
||||
requested: u64,
|
||||
) -> u64 {
|
||||
let cap_bps = self.rates.get(direction);
|
||||
if cap_bps == 0 {
|
||||
return requested;
|
||||
}
|
||||
let cap_epoch = bytes_per_epoch(cap_bps);
|
||||
match direction {
|
||||
RateDirection::Up => self.up.try_consume(&share.up, cap_epoch, requested),
|
||||
RateDirection::Down => self.down.try_consume(&share.down, cap_epoch, requested),
|
||||
}
|
||||
}
|
||||
|
||||
fn refund_for_user(&self, direction: RateDirection, share: &CidrUserShare, bytes: u64) {
|
||||
match direction {
|
||||
RateDirection::Up => {
|
||||
self.up.refund(bytes);
|
||||
share.up.refund(bytes);
|
||||
}
|
||||
RateDirection::Down => {
|
||||
self.down.refund(bytes);
|
||||
share.down.refund(bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_idle_users(&self) {
|
||||
self.users
|
||||
.retain(|_, share| share.active_conns.load(Ordering::Relaxed) > 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct CidrRule {
|
||||
key: String,
|
||||
cidr: IpNetwork,
|
||||
limits: RateLimitBps,
|
||||
prefix_len: u8,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PolicySnapshot {
|
||||
user_limits: HashMap<String, RateLimitBps>,
|
||||
cidr_rules_v4: Vec<CidrRule>,
|
||||
cidr_rules_v6: Vec<CidrRule>,
|
||||
cidr_rule_keys: HashSet<String>,
|
||||
}
|
||||
|
||||
impl PolicySnapshot {
|
||||
fn match_cidr(&self, ip: IpAddr) -> Option<&CidrRule> {
|
||||
match ip {
|
||||
IpAddr::V4(_) => self
|
||||
.cidr_rules_v4
|
||||
.iter()
|
||||
.find(|rule| rule.cidr.contains(ip)),
|
||||
IpAddr::V6(_) => self
|
||||
.cidr_rules_v6
|
||||
.iter()
|
||||
.find(|rule| rule.cidr.contains(ip)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ShardedRegistry<T> {
|
||||
shards: Box<[DashMap<String, Arc<T>>]>,
|
||||
mask: usize,
|
||||
}
|
||||
|
||||
impl<T> ShardedRegistry<T> {
|
||||
fn new(shards: usize) -> Self {
|
||||
let shard_count = shards.max(1).next_power_of_two();
|
||||
let mut items = Vec::with_capacity(shard_count);
|
||||
for _ in 0..shard_count {
|
||||
items.push(DashMap::<String, Arc<T>>::new());
|
||||
}
|
||||
Self {
|
||||
shards: items.into_boxed_slice(),
|
||||
mask: shard_count.saturating_sub(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn shard_index(&self, key: &str) -> usize {
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
key.hash(&mut hasher);
|
||||
(hasher.finish() as usize) & self.mask
|
||||
}
|
||||
|
||||
fn get_or_insert_with<F>(&self, key: &str, make: F) -> Arc<T>
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
let shard = &self.shards[self.shard_index(key)];
|
||||
match shard.entry(key.to_string()) {
|
||||
dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()),
|
||||
dashmap::mapref::entry::Entry::Vacant(slot) => {
|
||||
let value = Arc::new(make());
|
||||
slot.insert(Arc::clone(&value));
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn retain<F>(&self, predicate: F)
|
||||
where
|
||||
F: Fn(&String, &Arc<T>) -> bool + Copy,
|
||||
{
|
||||
for shard in &*self.shards {
|
||||
shard.retain(|key, value| predicate(key, value));
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_if<F>(&self, key: &str, predicate: F) -> bool
|
||||
where
|
||||
F: Fn(&Arc<T>) -> bool,
|
||||
{
|
||||
let shard = &self.shards[self.shard_index(key)];
|
||||
let should_remove = match shard.get(key) {
|
||||
Some(entry) => predicate(entry.value()),
|
||||
None => false,
|
||||
};
|
||||
if !should_remove {
|
||||
return false;
|
||||
}
|
||||
shard.remove(key).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TrafficLease {
|
||||
limiter: Arc<TrafficLimiter>,
|
||||
user_bucket: Option<Arc<UserBucket>>,
|
||||
cidr_bucket: Option<Arc<CidrBucket>>,
|
||||
cidr_user_key: Option<String>,
|
||||
cidr_user_share: Option<Arc<CidrUserShare>>,
|
||||
}
|
||||
|
||||
impl TrafficLease {
|
||||
pub fn try_consume(&self, direction: RateDirection, requested: u64) -> TrafficConsumeResult {
|
||||
if requested == 0 {
|
||||
return TrafficConsumeResult {
|
||||
granted: 0,
|
||||
blocked_user: false,
|
||||
blocked_cidr: false,
|
||||
};
|
||||
}
|
||||
|
||||
let mut granted = requested;
|
||||
if let Some(user_bucket) = self.user_bucket.as_ref() {
|
||||
let user_granted = user_bucket.try_consume(direction, granted);
|
||||
if user_granted == 0 {
|
||||
self.limiter.observe_throttle(direction, true, false);
|
||||
return TrafficConsumeResult {
|
||||
granted: 0,
|
||||
blocked_user: true,
|
||||
blocked_cidr: false,
|
||||
};
|
||||
}
|
||||
granted = user_granted;
|
||||
}
|
||||
|
||||
if let (Some(cidr_bucket), Some(cidr_user_share)) =
|
||||
(self.cidr_bucket.as_ref(), self.cidr_user_share.as_ref())
|
||||
{
|
||||
let cidr_granted =
|
||||
cidr_bucket.try_consume_for_user(direction, cidr_user_share, granted);
|
||||
if cidr_granted < granted
|
||||
&& let Some(user_bucket) = self.user_bucket.as_ref()
|
||||
{
|
||||
user_bucket.refund(direction, granted.saturating_sub(cidr_granted));
|
||||
}
|
||||
if cidr_granted == 0 {
|
||||
self.limiter.observe_throttle(direction, false, true);
|
||||
return TrafficConsumeResult {
|
||||
granted: 0,
|
||||
blocked_user: false,
|
||||
blocked_cidr: true,
|
||||
};
|
||||
}
|
||||
granted = cidr_granted;
|
||||
}
|
||||
|
||||
TrafficConsumeResult {
|
||||
granted,
|
||||
blocked_user: false,
|
||||
blocked_cidr: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn refund(&self, direction: RateDirection, bytes: u64) {
|
||||
if bytes == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(user_bucket) = self.user_bucket.as_ref() {
|
||||
user_bucket.refund(direction, bytes);
|
||||
}
|
||||
if let (Some(cidr_bucket), Some(cidr_user_share)) =
|
||||
(self.cidr_bucket.as_ref(), self.cidr_user_share.as_ref())
|
||||
{
|
||||
cidr_bucket.refund_for_user(direction, cidr_user_share, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe_wait_ms(
|
||||
&self,
|
||||
direction: RateDirection,
|
||||
blocked_user: bool,
|
||||
blocked_cidr: bool,
|
||||
wait_ms: u64,
|
||||
) {
|
||||
if wait_ms == 0 {
|
||||
return;
|
||||
}
|
||||
self.limiter
|
||||
.observe_wait(direction, blocked_user, blocked_cidr, wait_ms);
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TrafficLease {
|
||||
fn drop(&mut self) {
|
||||
if let Some(bucket) = self.user_bucket.as_ref() {
|
||||
decrement_atomic_saturating(&bucket.active_leases, 1);
|
||||
decrement_atomic_saturating(&self.limiter.user_scope.active_leases, 1);
|
||||
}
|
||||
|
||||
if let Some(bucket) = self.cidr_bucket.as_ref() {
|
||||
if let (Some(user_key), Some(share)) =
|
||||
(self.cidr_user_key.as_ref(), self.cidr_user_share.as_ref())
|
||||
{
|
||||
bucket.release_user_share(user_key, share);
|
||||
}
|
||||
decrement_atomic_saturating(&bucket.active_leases, 1);
|
||||
decrement_atomic_saturating(&self.limiter.cidr_scope.active_leases, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TrafficLimiter {
|
||||
policy: ArcSwap<PolicySnapshot>,
|
||||
user_buckets: ShardedRegistry<UserBucket>,
|
||||
cidr_buckets: ShardedRegistry<CidrBucket>,
|
||||
user_scope: ScopeMetrics,
|
||||
cidr_scope: ScopeMetrics,
|
||||
last_cleanup_epoch_secs: AtomicU64,
|
||||
}
|
||||
|
||||
impl TrafficLimiter {
|
||||
pub fn new() -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
policy: ArcSwap::from_pointee(PolicySnapshot::default()),
|
||||
user_buckets: ShardedRegistry::new(REGISTRY_SHARDS),
|
||||
cidr_buckets: ShardedRegistry::new(REGISTRY_SHARDS),
|
||||
user_scope: ScopeMetrics::default(),
|
||||
cidr_scope: ScopeMetrics::default(),
|
||||
last_cleanup_epoch_secs: AtomicU64::new(0),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn apply_policy(
|
||||
&self,
|
||||
user_limits: HashMap<String, RateLimitBps>,
|
||||
cidr_limits: HashMap<IpNetwork, RateLimitBps>,
|
||||
) {
|
||||
let filtered_users = user_limits
|
||||
.into_iter()
|
||||
.filter(|(_, limit)| limit.up_bps > 0 || limit.down_bps > 0)
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let mut cidr_rules_v4 = Vec::new();
|
||||
let mut cidr_rules_v6 = Vec::new();
|
||||
let mut cidr_rule_keys = HashSet::new();
|
||||
for (cidr, limits) in cidr_limits {
|
||||
if limits.up_bps == 0 && limits.down_bps == 0 {
|
||||
continue;
|
||||
}
|
||||
let key = cidr.to_string();
|
||||
let rule = CidrRule {
|
||||
key: key.clone(),
|
||||
cidr,
|
||||
limits,
|
||||
prefix_len: cidr.prefix(),
|
||||
};
|
||||
cidr_rule_keys.insert(key);
|
||||
match rule.cidr {
|
||||
IpNetwork::V4(_) => cidr_rules_v4.push(rule),
|
||||
IpNetwork::V6(_) => cidr_rules_v6.push(rule),
|
||||
}
|
||||
}
|
||||
|
||||
cidr_rules_v4.sort_by(|a, b| b.prefix_len.cmp(&a.prefix_len));
|
||||
cidr_rules_v6.sort_by(|a, b| b.prefix_len.cmp(&a.prefix_len));
|
||||
|
||||
self.user_scope
|
||||
.policy_entries
|
||||
.store(filtered_users.len() as u64, Ordering::Relaxed);
|
||||
self.cidr_scope
|
||||
.policy_entries
|
||||
.store(cidr_rule_keys.len() as u64, Ordering::Relaxed);
|
||||
|
||||
self.policy.store(Arc::new(PolicySnapshot {
|
||||
user_limits: filtered_users,
|
||||
cidr_rules_v4,
|
||||
cidr_rules_v6,
|
||||
cidr_rule_keys,
|
||||
}));
|
||||
|
||||
self.maybe_cleanup();
|
||||
}
|
||||
|
||||
pub fn acquire_lease(
|
||||
self: &Arc<Self>,
|
||||
user: &str,
|
||||
client_ip: IpAddr,
|
||||
) -> Option<Arc<TrafficLease>> {
|
||||
let policy = self.policy.load_full();
|
||||
let mut user_bucket = None;
|
||||
if let Some(limit) = policy.user_limits.get(user).copied() {
|
||||
let bucket = self
|
||||
.user_buckets
|
||||
.get_or_insert_with(user, || UserBucket::new(limit));
|
||||
bucket.set_rates(limit);
|
||||
bucket.active_leases.fetch_add(1, Ordering::Relaxed);
|
||||
self.user_scope
|
||||
.active_leases
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
user_bucket = Some(bucket);
|
||||
}
|
||||
|
||||
let mut cidr_bucket = None;
|
||||
let mut cidr_user_key = None;
|
||||
let mut cidr_user_share = None;
|
||||
if let Some(rule) = policy.match_cidr(client_ip) {
|
||||
let bucket = self
|
||||
.cidr_buckets
|
||||
.get_or_insert_with(rule.key.as_str(), || CidrBucket::new(rule.limits));
|
||||
bucket.set_rates(rule.limits);
|
||||
bucket.active_leases.fetch_add(1, Ordering::Relaxed);
|
||||
self.cidr_scope
|
||||
.active_leases
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
let share = bucket.acquire_user_share(user);
|
||||
cidr_user_key = Some(user.to_string());
|
||||
cidr_user_share = Some(share);
|
||||
cidr_bucket = Some(bucket);
|
||||
}
|
||||
|
||||
if user_bucket.is_none() && cidr_bucket.is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
self.maybe_cleanup();
|
||||
Some(Arc::new(TrafficLease {
|
||||
limiter: Arc::clone(self),
|
||||
user_bucket,
|
||||
cidr_bucket,
|
||||
cidr_user_key,
|
||||
cidr_user_share,
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn metrics_snapshot(&self) -> TrafficLimiterMetricsSnapshot {
|
||||
TrafficLimiterMetricsSnapshot {
|
||||
user_throttle_up_total: self.user_scope.throttle_up_total.load(Ordering::Relaxed),
|
||||
user_throttle_down_total: self.user_scope.throttle_down_total.load(Ordering::Relaxed),
|
||||
cidr_throttle_up_total: self.cidr_scope.throttle_up_total.load(Ordering::Relaxed),
|
||||
cidr_throttle_down_total: self.cidr_scope.throttle_down_total.load(Ordering::Relaxed),
|
||||
user_wait_up_ms_total: self.user_scope.wait_up_ms_total.load(Ordering::Relaxed),
|
||||
user_wait_down_ms_total: self.user_scope.wait_down_ms_total.load(Ordering::Relaxed),
|
||||
cidr_wait_up_ms_total: self.cidr_scope.wait_up_ms_total.load(Ordering::Relaxed),
|
||||
cidr_wait_down_ms_total: self.cidr_scope.wait_down_ms_total.load(Ordering::Relaxed),
|
||||
user_active_leases: self.user_scope.active_leases.load(Ordering::Relaxed),
|
||||
cidr_active_leases: self.cidr_scope.active_leases.load(Ordering::Relaxed),
|
||||
user_policy_entries: self.user_scope.policy_entries.load(Ordering::Relaxed),
|
||||
cidr_policy_entries: self.cidr_scope.policy_entries.load(Ordering::Relaxed),
|
||||
}
|
||||
}
|
||||
|
||||
fn observe_throttle(&self, direction: RateDirection, blocked_user: bool, blocked_cidr: bool) {
|
||||
if blocked_user {
|
||||
self.user_scope.throttle(direction);
|
||||
}
|
||||
if blocked_cidr {
|
||||
self.cidr_scope.throttle(direction);
|
||||
}
|
||||
}
|
||||
|
||||
fn observe_wait(
|
||||
&self,
|
||||
direction: RateDirection,
|
||||
blocked_user: bool,
|
||||
blocked_cidr: bool,
|
||||
wait_ms: u64,
|
||||
) {
|
||||
if blocked_user {
|
||||
self.user_scope.wait_ms(direction, wait_ms);
|
||||
}
|
||||
if blocked_cidr {
|
||||
self.cidr_scope.wait_ms(direction, wait_ms);
|
||||
}
|
||||
}
|
||||
|
||||
fn maybe_cleanup(&self) {
|
||||
let now_epoch_secs = now_epoch_secs();
|
||||
let last = self.last_cleanup_epoch_secs.load(Ordering::Relaxed);
|
||||
if now_epoch_secs.saturating_sub(last) < CLEANUP_INTERVAL_SECS {
|
||||
return;
|
||||
}
|
||||
if self
|
||||
.last_cleanup_epoch_secs
|
||||
.compare_exchange(last, now_epoch_secs, Ordering::Relaxed, Ordering::Relaxed)
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let policy = self.policy.load_full();
|
||||
self.user_buckets.retain(|user, bucket| {
|
||||
bucket.active_leases.load(Ordering::Relaxed) > 0
|
||||
|| policy.user_limits.contains_key(user)
|
||||
});
|
||||
self.cidr_buckets.retain(|cidr_key, bucket| {
|
||||
bucket.cleanup_idle_users();
|
||||
bucket.active_leases.load(Ordering::Relaxed) > 0
|
||||
|| policy.cidr_rule_keys.contains(cidr_key)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_refill_delay() -> Duration {
|
||||
let start = limiter_epoch_start();
|
||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||
let epoch_pos = elapsed_ms % FAIR_EPOCH_MS;
|
||||
let wait_ms = FAIR_EPOCH_MS.saturating_sub(epoch_pos).max(1);
|
||||
Duration::from_millis(wait_ms)
|
||||
}
|
||||
|
||||
fn decrement_atomic_saturating(counter: &AtomicU64, by: u64) {
|
||||
if by == 0 {
|
||||
return;
|
||||
}
|
||||
let mut current = counter.load(Ordering::Relaxed);
|
||||
loop {
|
||||
if current == 0 {
|
||||
return;
|
||||
}
|
||||
let next = current.saturating_sub(by);
|
||||
match counter.compare_exchange_weak(current, next, Ordering::Relaxed, Ordering::Relaxed) {
|
||||
Ok(_) => return,
|
||||
Err(actual) => current = actual,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn now_epoch_secs() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs()
|
||||
}
|
||||
|
||||
fn bytes_per_epoch(bps: u64) -> u64 {
|
||||
if bps == 0 {
|
||||
return 0;
|
||||
}
|
||||
let numerator = bps.saturating_mul(FAIR_EPOCH_MS);
|
||||
let bytes = numerator.saturating_div(8_000);
|
||||
bytes.max(1)
|
||||
}
|
||||
|
||||
fn current_epoch() -> u64 {
|
||||
let start = limiter_epoch_start();
|
||||
let elapsed_ms = start.elapsed().as_millis() as u64;
|
||||
elapsed_ms / FAIR_EPOCH_MS
|
||||
}
|
||||
|
||||
fn limiter_epoch_start() -> &'static Instant {
|
||||
static START: OnceLock<Instant> = OnceLock::new();
|
||||
START.get_or_init(Instant::now)
|
||||
}
|
||||
121
src/stats/mod.rs
121
src/stats/mod.rs
@@ -175,6 +175,18 @@ pub struct Stats {
|
||||
me_route_drop_queue_full: AtomicU64,
|
||||
me_route_drop_queue_full_base: AtomicU64,
|
||||
me_route_drop_queue_full_high: AtomicU64,
|
||||
me_fair_pressure_state_gauge: AtomicU64,
|
||||
me_fair_active_flows_gauge: AtomicU64,
|
||||
me_fair_queued_bytes_gauge: AtomicU64,
|
||||
me_fair_standing_flows_gauge: AtomicU64,
|
||||
me_fair_backpressured_flows_gauge: AtomicU64,
|
||||
me_fair_scheduler_rounds_total: AtomicU64,
|
||||
me_fair_deficit_grants_total: AtomicU64,
|
||||
me_fair_deficit_skips_total: AtomicU64,
|
||||
me_fair_enqueue_rejects_total: AtomicU64,
|
||||
me_fair_shed_drops_total: AtomicU64,
|
||||
me_fair_penalties_total: AtomicU64,
|
||||
me_fair_downstream_stalls_total: AtomicU64,
|
||||
me_d2c_batches_total: AtomicU64,
|
||||
me_d2c_batch_frames_total: AtomicU64,
|
||||
me_d2c_batch_bytes_total: AtomicU64,
|
||||
@@ -856,6 +868,78 @@ impl Stats {
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn set_me_fair_pressure_state_gauge(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() {
|
||||
self.me_fair_pressure_state_gauge
|
||||
.store(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn set_me_fair_active_flows_gauge(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() {
|
||||
self.me_fair_active_flows_gauge
|
||||
.store(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn set_me_fair_queued_bytes_gauge(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() {
|
||||
self.me_fair_queued_bytes_gauge
|
||||
.store(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn set_me_fair_standing_flows_gauge(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() {
|
||||
self.me_fair_standing_flows_gauge
|
||||
.store(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn set_me_fair_backpressured_flows_gauge(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() {
|
||||
self.me_fair_backpressured_flows_gauge
|
||||
.store(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn add_me_fair_scheduler_rounds_total(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() && value > 0 {
|
||||
self.me_fair_scheduler_rounds_total
|
||||
.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn add_me_fair_deficit_grants_total(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() && value > 0 {
|
||||
self.me_fair_deficit_grants_total
|
||||
.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn add_me_fair_deficit_skips_total(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() && value > 0 {
|
||||
self.me_fair_deficit_skips_total
|
||||
.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn add_me_fair_enqueue_rejects_total(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() && value > 0 {
|
||||
self.me_fair_enqueue_rejects_total
|
||||
.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn add_me_fair_shed_drops_total(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() && value > 0 {
|
||||
self.me_fair_shed_drops_total
|
||||
.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn add_me_fair_penalties_total(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() && value > 0 {
|
||||
self.me_fair_penalties_total
|
||||
.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn add_me_fair_downstream_stalls_total(&self, value: u64) {
|
||||
if self.telemetry_me_allows_normal() && value > 0 {
|
||||
self.me_fair_downstream_stalls_total
|
||||
.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub fn increment_me_d2c_batches_total(&self) {
|
||||
if self.telemetry_me_allows_normal() {
|
||||
self.me_d2c_batches_total.fetch_add(1, Ordering::Relaxed);
|
||||
@@ -1806,6 +1890,43 @@ impl Stats {
|
||||
pub fn get_me_route_drop_queue_full_high(&self) -> u64 {
|
||||
self.me_route_drop_queue_full_high.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_pressure_state_gauge(&self) -> u64 {
|
||||
self.me_fair_pressure_state_gauge.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_active_flows_gauge(&self) -> u64 {
|
||||
self.me_fair_active_flows_gauge.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_queued_bytes_gauge(&self) -> u64 {
|
||||
self.me_fair_queued_bytes_gauge.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_standing_flows_gauge(&self) -> u64 {
|
||||
self.me_fair_standing_flows_gauge.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_backpressured_flows_gauge(&self) -> u64 {
|
||||
self.me_fair_backpressured_flows_gauge
|
||||
.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_scheduler_rounds_total(&self) -> u64 {
|
||||
self.me_fair_scheduler_rounds_total.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_deficit_grants_total(&self) -> u64 {
|
||||
self.me_fair_deficit_grants_total.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_deficit_skips_total(&self) -> u64 {
|
||||
self.me_fair_deficit_skips_total.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_enqueue_rejects_total(&self) -> u64 {
|
||||
self.me_fair_enqueue_rejects_total.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_shed_drops_total(&self) -> u64 {
|
||||
self.me_fair_shed_drops_total.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_penalties_total(&self) -> u64 {
|
||||
self.me_fair_penalties_total.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_fair_downstream_stalls_total(&self) -> u64 {
|
||||
self.me_fair_downstream_stalls_total.load(Ordering::Relaxed)
|
||||
}
|
||||
pub fn get_me_d2c_batches_total(&self) -> u64 {
|
||||
self.me_d2c_batches_total.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user