Compare commits

...

65 Commits

Author SHA1 Message Date
miniusercoder
b246f0ed99 do not use haproxy in xray double hop configuration 2026-04-09 19:51:35 +03:00
miniusercoder
1265234491 xray with xhttp configuration 2026-04-09 18:48:37 +03:00
miniusercoder
a526fee728 fix documentation for Xray double hop setup 2026-04-08 22:24:51 +03:00
miniusercoder
970313edcb add documentation for Xray double hop setup 2026-04-08 19:17:09 +03:00
Alexey
852dc11722 Update README.md 2026-04-08 11:52:17 +03:00
Alexey
cda9600169 Update README.md 2026-04-08 11:52:00 +03:00
Alexey
dc03c73dd6 Update README.md 2026-04-08 11:50:50 +03:00
Alexey
c99f55f216 Update README.md 2026-04-08 11:35:37 +03:00
Alexey
f5786d284b Merge pull request #657 from Dimasssss/patch-3
Update install.sh - Add interactive domain prompt and EN/RU support
2026-04-08 11:33:06 +03:00
Alexey
0281cad564 Merge pull request #658 from Dimasssss/patch-4
Add install.sh installation method to QUICK_START_GUIDE
2026-04-08 11:30:20 +03:00
Dimasssss
91d9cb8de0 Update README.md 2026-04-07 23:06:11 +03:00
Dimasssss
9e74a78209 Update QUICK_START_GUIDE.en.md 2026-04-07 22:40:54 +03:00
Dimasssss
9933cdf245 Update QUICK_START_GUIDE.ru.md 2026-04-07 22:39:39 +03:00
Dimasssss
b4a3ad9aad Update install.sh - Add interactive domain prompt, EN/RU support, and script optimizations 2026-04-07 21:43:22 +03:00
Alexey
23156a840d Merge pull request #654 from TWRoman/main
Changes to the documentation and README
2026-04-07 20:12:55 +03:00
Roman
cf9d4b2c61 Changes in README and Docs
Changed the folder structure of the documentation.
Edited the README.
Added a Russian-language README.
Moved some information from the README to the FAQ.
2026-04-07 20:00:23 +03:00
TWRoman
63cfc067f6 Changes in README and Docs 2026-04-07 20:00:23 +03:00
TWRoman
5863b33b81 Changes in README and Docs 2026-04-07 20:00:22 +03:00
TWRoman
7ce87749c0 Changes in README and Docs 2026-04-07 20:00:22 +03:00
Alexey
bc691539a1 Bump 2026-04-07 19:28:05 +03:00
Alexey
2162a63e3e Memory Hard-bounds + Handshake Budget in Metrics + No mutable in hotpath ConnRegistry + Build-info in Metrics + TLS Fronting fixes + Round-bounded Retries + Bounded Retry-Round Constant + QueueFall Bounded Retry on Data-route: merge pull request #655 from telemt/flow
Memory Hard-bounds + Handshake Budget in Metrics + No mutable in hotpath ConnRegistry + Build-info in Metrics + TLS Fronting fixes + Round-bounded Retries + Bounded Retry-Round Constant + QueueFall Bounded Retry on Data-route
2026-04-07 19:26:07 +03:00
Alexey
4a77335ba9 Round-bounded Retries + Bounded Retry-Round Constant
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-07 19:19:40 +03:00
Alexey
ba29b66c4c Merge branch 'flow' of https://github.com/telemt/telemt into flow 2026-04-07 18:42:10 +03:00
Alexey
e8cf97095f QueueFall Bounded Retry on Data-route
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-07 18:41:59 +03:00
Alexey
ee4264af50 Merge pull request #624 from mammuthus/feature/metrics-build-info
metrics: export CARGO_PKG_VERSION as telemt_build_info version metric
2026-04-07 18:35:06 +03:00
Alexey
59c2476650 Merge branch 'flow' into feature/metrics-build-info 2026-04-07 18:34:51 +03:00
Alexey
89d6be267d Merge pull request #652 from groozchique/flow
[docs] Hotfix for link's obtaining command
2026-04-07 18:23:34 +03:00
Alexey
3b717c75da Memory Hard-bounds + Handshake Budget in Metrics + No mutable in hotpath ConnRegistry
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-07 18:18:47 +03:00
Nick Parfyonov
3af7673342 [docs] add classic/secure links to the output
After further testing I discovered that the current command only returns TLS links, ignoring classic/secure links if they are present
2026-04-07 13:53:12 +03:00
Alexey
ad2057ad44 Merge pull request #649 from JetJava/flow
tls_front/emulator: hash compact cert info payload before TLS emulation
2026-04-07 13:26:22 +03:00
Alexey
f8cfd4f0bc Merge pull request #651 from groozchique/flow
[FAQ] More user-friendly output when obtaining proxy links
2026-04-07 13:22:17 +03:00
Alexey
5cbcfb2a91 Merge pull request #643 from Dimasssss/patch-2
Update install.sh - fix "Permission denied (os error 13)"
2026-04-07 13:20:46 +03:00
Alexey
aec2c23a0c Merge pull request #650 from pavlozt/fix/zabbix-storage
Zabbix template: disable intermediate data storage
2026-04-07 13:19:35 +03:00
Nick Parfyonov
f5e63ab145 [FAQ] change output of user's links more to more user-friendly look
Currently output of existing method for obtaining proxy links of users is cluttered and messy, let's change it to a more clean and precise one
2026-04-07 13:12:22 +03:00
PavelZ
12f99eebab Zabbix template: disable intermediate data storage 2026-04-07 11:55:51 +03:00
Ivan
bc3ad02a20 tls_front/emulator: hash compact cert info payload before TLS emulation 2026-04-07 11:31:12 +04:00
Alexey
14674bd4e6 Update relay.rs 2026-04-06 19:01:12 +03:00
Alexey
a36c7b3f66 Update handshake_security_tests.rs 2026-04-06 17:45:45 +03:00
Alexey
d848e4a729 Fixes for test + Rustfmt 2026-04-06 16:12:46 +03:00
Alexey
8d865a980c MRU Search + Runtime user snapshot + Ordered candidate auth + Sticky hints + Overload Budgets 2026-04-06 15:04:15 +03:00
Dimasssss
f829439e8f Update install.sh - fix "Permission denied (os error 13)" 2026-04-06 14:33:02 +03:00
Alexey
a14f8b14d2 Licenses Updating 2026-04-06 13:40:32 +03:00
Alexey
31af2da4d5 Licenses -> License 2026-04-06 13:33:08 +03:00
Alexey
ac2b88d6ea License -> Licenses 2026-04-06 13:32:18 +03:00
Alexey
4a3ef62494 License 3.3 Translations 2026-04-06 13:31:22 +03:00
Alexey
6996d6e597 Update LICENSE 2026-04-06 13:21:16 +03:00
Alexey
b3f11624c9 Update LICENSE 2026-04-06 13:12:06 +03:00
Alexey
13dc1f70bf Accept as unknown_sni_action 2026-04-06 12:03:06 +03:00
Alexey
b88457b9bc Rename test.yml to check.yml 2026-04-06 11:19:35 +03:00
Alexey
d176766db2 Uploading Binary as artifact in Github Actions 2026-04-06 11:17:15 +03:00
Alexey
fa4e2000a8 Privileges fix
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-06 11:10:41 +03:00
Alexey
4d87a790cc Merge pull request #626 from vladon/fix/strip-release-binaries
[codex] Strip release binaries before packaging
2026-04-05 21:12:07 +03:00
Alexey
07fed8f871 Merge pull request #632 from SysAdminKo/main
Актуализация документации CONFIG_PARAMS
2026-04-05 21:10:58 +03:00
Alexey
407d686d49 Merge pull request #638 from Dimasssss/patch-1
Update install.sh - add port availability check and new CLI arguments + update QUICK_START_GUIDE - add CAP_NET_ADMIN Service
2026-04-05 21:06:29 +03:00
Dimasssss
eac5cc81fb Update QUICK_START_GUIDE.ru.md 2026-04-05 18:53:16 +03:00
Dimasssss
c51d16f403 Update QUICK_START_GUIDE.en.md 2026-04-05 18:53:06 +03:00
Dimasssss
b5146bba94 Update install.sh 2026-04-05 18:43:08 +03:00
SysAdminKo
5ed525fa48 Add server.conntrack_control configuration section with detailed parameters and descriptions
This update introduces a new section in the configuration documentation for `server.conntrack_control`, outlining various parameters such as `inline_conntrack_control`, `mode`, `backend`, `profile`, `hybrid_listener_ips`, `pressure_high_watermark_pct`, `pressure_low_watermark_pct`, and `delete_budget_per_sec`. Each parameter includes constraints, descriptions, and examples to assist users in configuring conntrack control effectively.
2026-04-05 18:05:13 +03:00
Олегсей Бреднев
9f7c1693ce Merge branch 'telemt:main' into main 2026-04-05 17:42:08 +03:00
Dimasssss
1524396e10 Update install.sh
Новые аргументы командной строки:
-d, --domain : TLS-домен (дефолт: petrovich.ru)
-p, --port : Порт сервера (дефолт: 443)
-s, --secret : Секрет пользователя (32 hex-символа)
-a, --ad-tag : Установка ad_tag

⚠️ Если эти флаги переданы при запуске, они заменят собой старые сохраненные значения.
2026-04-05 17:32:21 +03:00
SysAdminKo
444a20672d Refine CONFIG_PARAMS documentation by updating default values to use a dash (—) for optional parameters instead of null. Adjust constraints for clarity, ensuring all types are accurately represented as required. Enhance descriptions for better understanding of configuration options. 2026-04-04 21:56:24 +03:00
mammuthus
9b64d2ee17 style(metrics): apply rustfmt for build_info additions 2026-04-03 07:49:37 +00:00
Vlad Yaroslavlev
d673935b6d Merge branch 'main' into fix/strip-release-binaries 2026-04-03 00:35:20 +03:00
Vladislav Yaroslavlev
363b5014f7 Strip release binaries before packaging 2026-04-03 00:17:43 +03:00
mammuthus
873618ce53 metrics: export telemt_build_info version metric 2026-04-02 18:14:50 +00:00
57 changed files with 7275 additions and 987 deletions

View File

@@ -36,4 +36,10 @@ jobs:
${{ runner.os }}-cargo-
- name: Build Release
run: cargo build --release --verbose
run: cargo build --release --verbose
- name: Upload binary artifact
uses: actions/upload-artifact@v4
with:
name: telemt
path: target/release/telemt

View File

@@ -151,6 +151,14 @@ jobs:
mkdir -p dist
cp "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}" dist/telemt
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-gnu" ]; then
STRIP_BIN=aarch64-linux-gnu-strip
else
STRIP_BIN=strip
fi
"${STRIP_BIN}" dist/telemt
cd dist
tar -czf "${{ matrix.asset }}.tar.gz" \
--owner=0 --group=0 --numeric-owner \
@@ -279,6 +287,14 @@ jobs:
mkdir -p dist
cp "target/${{ matrix.target }}/release/${{ env.BINARY_NAME }}" dist/telemt
if [ "${{ matrix.target }}" = "aarch64-unknown-linux-musl" ]; then
STRIP_BIN=aarch64-linux-musl-strip
else
STRIP_BIN=strip
fi
"${STRIP_BIN}" dist/telemt
cd dist
tar -czf "${{ matrix.asset }}.tar.gz" \
--owner=0 --group=0 --numeric-owner \

2
Cargo.lock generated
View File

@@ -2780,7 +2780,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "telemt"
version = "3.3.38"
version = "3.3.39"
dependencies = [
"aes",
"anyhow",

View File

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

16
LICENSE
View File

@@ -1,4 +1,4 @@
###### TELEMT Public License 3 ######
######## TELEMT LICENSE 3.3 #########
##### Copyright (c) 2026 Telemt #####
Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -14,11 +14,15 @@ are preserved and complied with.
The canonical version of this License is the English version.
Official translations are provided for informational purposes only
and for convenience, and do not have legal force. In case of any
discrepancy, the English version of this License shall prevail.
Available versions:
- English in Markdown: docs/LICENSE/LICENSE.md
- German: docs/LICENSE/LICENSE.de.md
- Russian: docs/LICENSE/LICENSE.ru.md
discrepancy, the English version of this License shall prevail
/----------------------------------------------------------\
| Language | Location |
|-------------|--------------------------------------------|
| English | docs/LICENSE/TELEMT-LICENSE.en.md |
| German | docs/LICENSE/TELEMT-LICENSE.de.md |
| Russian | docs/LICENSE/TELEMT-LICENSE.ru.md |
\----------------------------------------------------------/
### License Versioning Policy

217
README.md
View File

@@ -2,189 +2,61 @@
***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 **Telegram Desktop** starting from version **6.7.2**
> - in **Telegram Android Client** starting from version **12.6.4**
> - **release for iOS is "work in progress"**
>
> To work with EE-MTProxy, please update your client!
<p align="center">
<a href="https://t.me/telemtrs">
<img src="docs/assets/telegram_button.png" alt="Join us in Telegram" width="200" />
</a>
</p>
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements 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
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/Architecture/Model/MODEL.en.md);
- [Full-covered API w/ management](https://github.com/telemt/telemt/blob/main/docs/Architecture/API/API.md);
- Anti-Replay on Sliding Window;
- Prometheus-format Metrics;
- TLS-Fronting and TCP-Splicing for masking from "prying" eyes.
![telemt_scheme](docs/assets/telemt.png)
⚓ 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)
⚓ 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
- 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)
- [Architecture](docs/Architecture)
- [Quick Start Guide](#quick-start-guide)
- [Config parameters](docs/Config_params)
- [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)
- [Quick Start Guide RU](docs/Quick_start/QUICK_START_GUIDE.ru.md)
- [Quick Start Guide EN](docs/Quick_start/QUICK_START_GUIDE.en.md)
## FAQ
- [FAQ RU](docs/FAQ.ru.md)
- [FAQ EN](docs/FAQ.en.md)
### 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
```
## Build
```bash
# Cloning repo
@@ -207,7 +79,7 @@ telemt config.toml
```
### OpenBSD
- Build and service setup guide: [OpenBSD Guide (EN)](docs/OPENBSD.en.md)
- Build and service setup guide: [OpenBSD Guide (EN)](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md)
- Example rc.d script: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd)
- Status: OpenBSD sandbox hardening with `pledge(2)` and `unveil(2)` is not implemented yet.
@@ -218,24 +90,3 @@ telemt config.toml
- No garbage collector
- 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)
## 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...

123
README.ru.md Normal file
View File

@@ -0,0 +1,123 @@
# Telemt — MTProxy на Rust + Tokio
***Решает проблемы раньше, чем другие узнают об их существовании***
> [!Примечание]
>
> Исправленный TLS ClientHello доступен в **Telegram Desktop** начиная с версии **6.7.2**: для работы с EE-MTProxy обновите клиент.
>
> Исправленный TLS ClientHello доступен в **Telegram Android** начиная с версии **12.6.4**; **официальный релиз для iOS находится в процессе разработки**.
<p align="center">
<a href="https://t.me/telemtrs">
<img src="docs/assets/telegram_button.png" alt="Мы в Telegram" width="200" />
</a>
</p>
**Telemt** — это быстрый, безопасный и функциональный сервер, написанный на Rust. Он полностью реализует официальный алгоритм прокси Telegram и добавляет множество улучшений для продакшена:
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + жизненный цикл генераций](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md);
- [Полноценный API с управлением](https://github.com/telemt/telemt/blob/main/docs/API.md);
- Защита от повторных атак (Anti-Replay on Sliding Window);
- Метрики в формате Prometheus;
- TLS-fronting и TCP-splicing для маскировки от DPI.
![telemt_scheme](docs/assets/telemt.png)
## Особенности
⚓ Реализация **TLS-fronting** максимально приближена к поведению реального HTTPS-трафика.
***Middle-End Pool*** оптимизирован для высокой производительности.
- Поддержка всех режимов MTProto proxy:
- Classic;
- Secure (префикс `dd`);
- Fake TLS (префикс `ee` + SNI fronting);
- Защита от replay-атак;
- Маскировка трафика (перенаправление неизвестных подключений на реальные сайты);
- Настраиваемые keepalive, таймауты, IPv6 и «быстрый режим»;
- Корректное завершение работы (Ctrl+C);
- Подробное логирование через `trace` и `debug`.
# Навигация
- [FAQ](#faq)
- [Архитектура](docs/Architecture)
- [Быстрый старт](#quick-start-guide)
- [Параметры конфигурационного файла](docs/Config_params)
- [Сборка](#build)
- [Почему Rust?](#why-rust)
- [Известные проблемы](#issues)
- [Планы](#roadmap)
## Быстрый старт
- [Quick Start Guide RU](docs/Quick_start/QUICK_START_GUIDE.ru.md)
- [Quick Start Guide EN](docs/Quick_start/QUICK_START_GUIDE.en.md)
## FAQ
- [FAQ RU](docs/FAQ.ru.md)
- [FAQ EN](docs/FAQ.en.md)
## Сборка
```bash
# Клонируйте репозиторий
git clone https://github.com/telemt/telemt
# Смените каталог на telemt
cd telemt
# Начните процесс сборки
cargo build --release
# Устройства с небольшим объёмом оперативной памяти (1 ГБ, например NanoPi Neo3 / Raspberry Pi Zero 2):
# используется параметр lto = «thin» для уменьшения пикового потребления памяти.
# Если ваш пользовательский набор инструментов переопределяет профили, не используйте Fat LTO.
# Перейдите в каталог /bin
mv ./target/release/telemt /bin
# Сделайте файл исполняемым
chmod +x /bin/telemt
# Запустите!
telemt config.toml
```
### Устройства с малым объемом RAM
Для устройств с ~1 ГБ RAM (например Raspberry Pi):
- используется облегчённая оптимизация линковщика (thin LTO);
- не рекомендуется включать fat LTO.
## OpenBSD
- Руководство по сборке и настройке на английском языке [OpenBSD Guide (EN)](docs/Quick_start/OPENBSD_QUICK_START_GUIDE.en.md);
- Пример rc.d скрипта: [contrib/openbsd/telemt.rcd](contrib/openbsd/telemt.rcd);
- Поддержка sandbox с `pledge(2)` и `unveil(2)` пока не реализована.
## Почему Rust?
- Надёжность для долгоживущих процессов;
- Детерминированное управление ресурсами (RAII);
- Отсутствие сборщика мусора;
- Безопасность памяти;
- Асинхронная архитектура Tokio.
## Известные проблемы
- ✅ [Поддержка SOCKS5 как upstream](https://github.com/telemt/telemt/issues/1) -> добавлен Upstream Management;
- ✅ [Проблема зависания загрузки медиа на iOS](https://github.com/telemt/telemt/issues/2).
## Планы
- Публичный IP в ссылках;
- Перезагрузка конфигурации на лету;
- Привязка к устройству или IP для входящих и исходящих соединений;
- Поддержка рекламных тегов по SNI / секретному ключу;
- Улучшенная обработка ошибок;
- Zero-copy оптимизации;
- Проверка состояния дата-центров;
- Отсутствие глобального изменяемого состояния;
- Изоляция клиентов и справедливое распределение трафика;
- «Политика секретов» — маршрутизация по SNI / секрету;
- Балансировщик с несколькими источниками и отработка отказов;
- Строгие FSM для handshake;
- Улучшенная защита от replay-атак;
- Веб-интерфейс: статистика, состояние работоспособности, задержка, пользовательский опыт...

View File

Before

Width:  |  Height:  |  Size: 650 KiB

After

Width:  |  Height:  |  Size: 650 KiB

View File

Before

Width:  |  Height:  |  Size: 838 KiB

After

Width:  |  Height:  |  Size: 838 KiB

View File

@@ -14,10 +14,10 @@ This document lists all configuration keys accepted by `config.toml`.
| Key | Type | Default |
| --- | ---- | ------- |
| [`include`](#cfg-top-include) | `String` (special directive) | `null` |
| [`include`](#cfg-top-include) | `String` (special directive) | |
| [`show_link`](#cfg-top-show_link) | `"*"` or `String[]` | `[]` (`ShowLink::None`) |
| [`dc_overrides`](#cfg-top-dc_overrides) | `Map<String, String or String[]>` | `{}` |
| [`default_dc`](#cfg-top-default_dc) | `u8` or `null` | `null` (effective fallback: `2` in ME routing) |
| [`default_dc`](#cfg-top-default_dc) | `u8` | — (effective fallback: `2` in ME routing) |
<a id="cfg-top-include"></a>
- `include`
@@ -68,17 +68,17 @@ This document lists all configuration keys accepted by `config.toml`.
| Key | Type | Default |
| --- | ---- | ------- |
| [`data_path`](#cfg-general-data_path) | `String` or `null` | `null` |
| [`data_path`](#cfg-general-data_path) | `String` | — |
| [`prefer_ipv6`](#cfg-general-prefer_ipv6) | `bool` | `false` |
| [`fast_mode`](#cfg-general-fast_mode) | `bool` | `true` |
| [`use_middle_proxy`](#cfg-general-use_middle_proxy) | `bool` | `true` |
| [`proxy_secret_path`](#cfg-general-proxy_secret_path) | `String` or `null` | `"proxy-secret"` |
| [`proxy_config_v4_cache_path`](#cfg-general-proxy_config_v4_cache_path) | `String` or `null` | `"cache/proxy-config-v4.txt"` |
| [`proxy_config_v6_cache_path`](#cfg-general-proxy_config_v6_cache_path) | `String` or `null` | `"cache/proxy-config-v6.txt"` |
| [`ad_tag`](#cfg-general-ad_tag) | `String` or `null` | `null` |
| [`middle_proxy_nat_ip`](#cfg-general-middle_proxy_nat_ip) | `IpAddr` or `null` | `null` |
| [`proxy_secret_path`](#cfg-general-proxy_secret_path) | `String` | `"proxy-secret"` |
| [`proxy_config_v4_cache_path`](#cfg-general-proxy_config_v4_cache_path) | `String` | `"cache/proxy-config-v4.txt"` |
| [`proxy_config_v6_cache_path`](#cfg-general-proxy_config_v6_cache_path) | `String` | `"cache/proxy-config-v6.txt"` |
| [`ad_tag`](#cfg-general-ad_tag) | `String` | — |
| [`middle_proxy_nat_ip`](#cfg-general-middle_proxy_nat_ip) | `IpAddr` | — |
| [`middle_proxy_nat_probe`](#cfg-general-middle_proxy_nat_probe) | `bool` | `true` |
| [`middle_proxy_nat_stun`](#cfg-general-middle_proxy_nat_stun) | `String` or `null` | `null` |
| [`middle_proxy_nat_stun`](#cfg-general-middle_proxy_nat_stun) | `String` | — |
| [`middle_proxy_nat_stun_servers`](#cfg-general-middle_proxy_nat_stun_servers) | `String[]` | `[]` |
| [`stun_nat_probe_concurrency`](#cfg-general-stun_nat_probe_concurrency) | `usize` | `8` |
| [`middle_proxy_pool_size`](#cfg-general-middle_proxy_pool_size) | `usize` | `8` |
@@ -144,7 +144,7 @@ This document lists all configuration keys accepted by `config.toml`.
| [`upstream_unhealthy_fail_threshold`](#cfg-general-upstream_unhealthy_fail_threshold) | `u32` | `5` |
| [`upstream_connect_failfast_hard_errors`](#cfg-general-upstream_connect_failfast_hard_errors) | `bool` | `false` |
| [`stun_iface_mismatch_ignore`](#cfg-general-stun_iface_mismatch_ignore) | `bool` | `false` |
| [`unknown_dc_log_path`](#cfg-general-unknown_dc_log_path) | `String` or `null` | `"unknown-dc.txt"` |
| [`unknown_dc_log_path`](#cfg-general-unknown_dc_log_path) | `String` | `"unknown-dc.txt"` |
| [`unknown_dc_file_log_enabled`](#cfg-general-unknown_dc_file_log_enabled) | `bool` | `false` |
| [`log_level`](#cfg-general-log_level) | `"debug"`, `"verbose"`, `"normal"`, or `"silent"` | `"normal"` |
| [`disable_colors`](#cfg-general-disable_colors) | `bool` | `false` |
@@ -163,7 +163,7 @@ This document lists all configuration keys accepted by `config.toml`.
| [`me_route_inline_recovery_attempts`](#cfg-general-me_route_inline_recovery_attempts) | `u32` | `3` |
| [`me_route_inline_recovery_wait_ms`](#cfg-general-me_route_inline_recovery_wait_ms) | `u64` | `3000` |
| [`fast_mode_min_tls_record`](#cfg-general-fast_mode_min_tls_record) | `usize` | `0` |
| [`update_every`](#cfg-general-update_every) | `u64` or `null` | `300` |
| [`update_every`](#cfg-general-update_every) | `u64` | `300` |
| [`me_reinit_every_secs`](#cfg-general-me_reinit_every_secs) | `u64` | `900` |
| [`me_hardswap_warmup_delay_min_ms`](#cfg-general-me_hardswap_warmup_delay_min_ms) | `u64` | `1000` |
| [`me_hardswap_warmup_delay_max_ms`](#cfg-general-me_hardswap_warmup_delay_max_ms) | `u64` | `2000` |
@@ -205,7 +205,7 @@ This document lists all configuration keys accepted by `config.toml`.
<a id="cfg-general-data_path"></a>
- `data_path`
- **Constraints / validation**: `String` or `null`.
- **Constraints / validation**: `String` (optional).
- **Description**: Optional runtime data directory path.
- **Example**:
@@ -245,7 +245,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-proxy_secret_path"></a>
- `proxy_secret_path`
- **Constraints / validation**: `String` or `null`. If `null`, the effective cache path is `"proxy-secret"`. Empty values are accepted but will likely fail at runtime (invalid file path).
- **Constraints / validation**: `String`. When omitted, the default path is `"proxy-secret"`. Empty values are accepted by TOML/serde but will likely fail at runtime (invalid file path).
- **Description**: Path to Telegram infrastructure `proxy-secret` cache file used by ME handshake/RPC auth. Telemt always tries a fresh download from `https://core.telegram.org/getProxySecret` first, caches it to this path on success, and falls back to reading the cached file (any age) on download failure.
- **Example**:
@@ -255,7 +255,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-proxy_config_v4_cache_path"></a>
- `proxy_config_v4_cache_path`
- **Constraints / validation**: `String` or `null`. When set, must not be empty/whitespace-only.
- **Constraints / validation**: `String`. When set, must not be empty/whitespace-only.
- **Description**: Optional disk cache path for raw `getProxyConfig` (IPv4) snapshot. At startup Telemt tries to fetch a fresh snapshot first; on fetch failure or empty snapshot it falls back to this cache file when present and non-empty.
- **Example**:
@@ -265,7 +265,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-proxy_config_v6_cache_path"></a>
- `proxy_config_v6_cache_path`
- **Constraints / validation**: `String` or `null`. When set, must not be empty/whitespace-only.
- **Constraints / validation**: `String`. When set, must not be empty/whitespace-only.
- **Description**: Optional disk cache path for raw `getProxyConfigV6` (IPv6) snapshot. At startup Telemt tries to fetch a fresh snapshot first; on fetch failure or empty snapshot it falls back to this cache file when present and non-empty.
- **Example**:
@@ -275,7 +275,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-ad_tag"></a>
- `ad_tag`
- **Constraints / validation**: `String` or `null`. When set, must be exactly 32 hex characters; invalid values are disabled during config load.
- **Constraints / validation**: `String` (optional). When set, must be exactly 32 hex characters; invalid values are disabled during config load.
- **Description**: Global fallback sponsored-channel `ad_tag` (used when user has no override in `access.user_ad_tags`). An all-zero tag is accepted but has no effect (and is warned about) until replaced with a real tag from `@MTProxybot`.
- **Example**:
@@ -285,7 +285,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-middle_proxy_nat_ip"></a>
- `middle_proxy_nat_ip`
- **Constraints / validation**: `IpAddr` or `null`.
- **Constraints / validation**: `IpAddr` (optional).
- **Description**: Manual public NAT IP override used as ME address material when set.
- **Example**:
@@ -967,8 +967,8 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-unknown_dc_log_path"></a>
- `unknown_dc_log_path`
- **Constraints / validation**: `String` or `null`. Must be a safe path (no `..` components, parent directory must exist); unsafe paths are rejected at runtime.
- **Description**: Log file path for unknown (non-standard) DC requests when `unknown_dc_file_log_enabled = true`. Set to `null` to disable file logging.
- **Constraints / validation**: `String` (optional). Must be a safe path (no `..` components, parent directory must exist); unsafe paths are rejected at runtime.
- **Description**: Log file path for unknown (non-standard) DC requests when `unknown_dc_file_log_enabled = true`. Omit this key to disable file logging.
- **Example**:
```toml
@@ -1157,7 +1157,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-update_every"></a>
- `update_every`
- **Constraints / validation**: `u64` (seconds) or `null`. If set, must be `> 0`. If `null`, legacy `proxy_secret_auto_reload_secs` and `proxy_config_auto_reload_secs` are used and their effective minimum must be `> 0`.
- **Constraints / validation**: `u64` (seconds). If set, must be `> 0`. If this key is not explicitly set, legacy `proxy_secret_auto_reload_secs` and `proxy_config_auto_reload_secs` may be used (their effective minimum must be `> 0`).
- **Description**: Unified refresh interval for ME updater tasks (`getProxyConfig`, `getProxyConfigV6`, `getProxySecret`). When set, it overrides legacy proxy reload intervals.
- **Example**:
@@ -1450,7 +1450,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-proxy_secret_auto_reload_secs"></a>
- `proxy_secret_auto_reload_secs`
- **Constraints / validation**: Deprecated. Use `general.update_every`. When `general.update_every` is `null`, the effective legacy refresh interval is `min(proxy_secret_auto_reload_secs, proxy_config_auto_reload_secs)` and must be `> 0`.
- **Constraints / validation**: Deprecated. Use `general.update_every`. When `general.update_every` is not explicitly set, the effective legacy refresh interval is `min(proxy_secret_auto_reload_secs, proxy_config_auto_reload_secs)` and must be `> 0`.
- **Description**: Deprecated legacy proxy-secret refresh interval. Used only when `general.update_every` is not set.
- **Example**:
@@ -1463,7 +1463,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-proxy_config_auto_reload_secs"></a>
- `proxy_config_auto_reload_secs`
- **Constraints / validation**: Deprecated. Use `general.update_every`. When `general.update_every` is `null`, the effective legacy refresh interval is `min(proxy_secret_auto_reload_secs, proxy_config_auto_reload_secs)` and must be `> 0`.
- **Constraints / validation**: Deprecated. Use `general.update_every`. When `general.update_every` is not explicitly set, the effective legacy refresh interval is `min(proxy_secret_auto_reload_secs, proxy_config_auto_reload_secs)` and must be `> 0`.
- **Description**: Deprecated legacy ME config refresh interval. Used only when `general.update_every` is not set.
- **Example**:
@@ -1624,8 +1624,8 @@ This document lists all configuration keys accepted by `config.toml`.
| Key | Type | Default |
| --- | ---- | ------- |
| [`show`](#cfg-general-links-show) | `"*"` or `String[]` | `"*"` |
| [`public_host`](#cfg-general-links-public_host) | `String` or `null` | `null` |
| [`public_port`](#cfg-general-links-public_port) | `u16` or `null` | `null` |
| [`public_host`](#cfg-general-links-public_host) | `String` | — |
| [`public_port`](#cfg-general-links-public_port) | `u16` | — |
<a id="cfg-general-links-show"></a>
- `show`
@@ -1641,7 +1641,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-links-public_host"></a>
- `public_host`
- **Constraints / validation**: `String` or `null`.
- **Constraints / validation**: `String` (optional).
- **Description**: Public hostname/IP override used for generated `tg://` links (overrides detected IP).
- **Example**:
@@ -1651,7 +1651,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-general-links-public_port"></a>
- `public_port`
- **Constraints / validation**: `u16` or `null`.
- **Constraints / validation**: `u16` (optional).
- **Description**: Public port override used for generated `tg://` links (overrides `server.port`).
- **Example**:
@@ -1708,7 +1708,7 @@ This document lists all configuration keys accepted by `config.toml`.
| Key | Type | Default |
| --- | ---- | ------- |
| [`ipv4`](#cfg-network-ipv4) | `bool` | `true` |
| [`ipv6`](#cfg-network-ipv6) | `bool` or `null` | `false` |
| [`ipv6`](#cfg-network-ipv6) | `bool` | `false` |
| [`prefer`](#cfg-network-prefer) | `u8` | `4` |
| [`multipath`](#cfg-network-multipath) | `bool` | `false` |
| [`stun_use`](#cfg-network-stun_use) | `bool` | `true` |
@@ -1730,8 +1730,8 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-network-ipv6"></a>
- `ipv6`
- **Constraints / validation**: `bool` or `null`. `null` means "auto-detect IPv6 availability".
- **Description**: Enables/disables IPv6 when explicitly set; when `null`, Telemt will auto-detect IPv6 availability at runtime.
- **Constraints / validation**: `bool`.
- **Description**: Enables/disables IPv6 networking. When omitted, defaults to `false`.
- **Example**:
```toml
@@ -1741,9 +1741,6 @@ This document lists all configuration keys accepted by `config.toml`.
# or: disable IPv6 explicitly
# ipv6 = false
# or: let Telemt auto-detect
# ipv6 = null
```
<a id="cfg-network-prefer"></a>
- `prefer`
@@ -1842,16 +1839,16 @@ This document lists all configuration keys accepted by `config.toml`.
| Key | Type | Default |
| --- | ---- | ------- |
| [`port`](#cfg-server-port) | `u16` | `443` |
| [`listen_addr_ipv4`](#cfg-server-listen_addr_ipv4) | `String` or `null` | `"0.0.0.0"` |
| [`listen_addr_ipv6`](#cfg-server-listen_addr_ipv6) | `String` or `null` | `"::"` |
| [`listen_unix_sock`](#cfg-server-listen_unix_sock) | `String` or `null` | `null` |
| [`listen_unix_sock_perm`](#cfg-server-listen_unix_sock_perm) | `String` or `null` | `null` |
| [`listen_tcp`](#cfg-server-listen_tcp) | `bool` or `null` | `null` (auto) |
| [`listen_addr_ipv4`](#cfg-server-listen_addr_ipv4) | `String` | `"0.0.0.0"` |
| [`listen_addr_ipv6`](#cfg-server-listen_addr_ipv6) | `String` | `"::"` |
| [`listen_unix_sock`](#cfg-server-listen_unix_sock) | `String` | — |
| [`listen_unix_sock_perm`](#cfg-server-listen_unix_sock_perm) | `String` | — |
| [`listen_tcp`](#cfg-server-listen_tcp) | `bool` | — (auto) |
| [`proxy_protocol`](#cfg-server-proxy_protocol) | `bool` | `false` |
| [`proxy_protocol_header_timeout_ms`](#cfg-server-proxy_protocol_header_timeout_ms) | `u64` | `500` |
| [`proxy_protocol_trusted_cidrs`](#cfg-server-proxy_protocol_trusted_cidrs) | `IpNetwork[]` | `[]` |
| [`metrics_port`](#cfg-server-metrics_port) | `u16` or `null` | `null` |
| [`metrics_listen`](#cfg-server-metrics_listen) | `String` or `null` | `null` |
| [`metrics_port`](#cfg-server-metrics_port) | `u16` | — |
| [`metrics_listen`](#cfg-server-metrics_listen) | `String` | — |
| [`metrics_whitelist`](#cfg-server-metrics_whitelist) | `IpNetwork[]` | `["127.0.0.1/32", "::1/128"]` |
| [`max_connections`](#cfg-server-max_connections) | `u32` | `10000` |
| [`accept_permit_timeout_ms`](#cfg-server-accept_permit_timeout_ms) | `u64` | `250` |
@@ -1868,8 +1865,8 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-server-listen_addr_ipv4"></a>
- `listen_addr_ipv4`
- **Constraints / validation**: `String` or `null`. When set, must be a valid IPv4 address string.
- **Description**: IPv4 bind address for TCP listener (`null` disables IPv4 bind).
- **Constraints / validation**: `String` (optional). When set, must be a valid IPv4 address string.
- **Description**: IPv4 bind address for TCP listener (omit this key to disable IPv4 bind).
- **Example**:
```toml
@@ -1878,8 +1875,8 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-server-listen_addr_ipv6"></a>
- `listen_addr_ipv6`
- **Constraints / validation**: `String` or `null`. When set, must be a valid IPv6 address string.
- **Description**: IPv6 bind address for TCP listener (`null` disables IPv6 bind).
- **Constraints / validation**: `String` (optional). When set, must be a valid IPv6 address string.
- **Description**: IPv6 bind address for TCP listener (omit this key to disable IPv6 bind).
- **Example**:
```toml
@@ -1888,7 +1885,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-server-listen_unix_sock"></a>
- `listen_unix_sock`
- **Constraints / validation**: `String` or `null`. Must not be empty when set. Unix only.
- **Constraints / validation**: `String` (optional). Must not be empty when set. Unix only.
- **Description**: Unix socket path for listener. When set, `server.listen_tcp` defaults to `false` (unless explicitly overridden).
- **Example**:
@@ -1898,8 +1895,8 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-server-listen_unix_sock_perm"></a>
- `listen_unix_sock_perm`
- **Constraints / validation**: `String` or `null`. When set, should be an octal permission string like `"0666"` or `"0777"`.
- **Description**: Optional Unix socket file permissions applied after bind (chmod). `null` means "no change" (inherits umask).
- **Constraints / validation**: `String` (optional). When set, should be an octal permission string like `"0666"` or `"0777"`.
- **Description**: Optional Unix socket file permissions applied after bind (chmod). When omitted, permissions are not changed (inherits umask).
- **Example**:
```toml
@@ -1909,7 +1906,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-server-listen_tcp"></a>
- `listen_tcp`
- **Constraints / validation**: `bool` or `null`. `null` means auto:
- **Constraints / validation**: `bool` (optional). When omitted, Telemt auto-detects:
- `true` when `listen_unix_sock` is not set
- `false` when `listen_unix_sock` is set
- **Description**: Explicit TCP listener enable/disable override.
@@ -1957,7 +1954,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-server-metrics_port"></a>
- `metrics_port`
- **Constraints / validation**: `u16` or `null`.
- **Constraints / validation**: `u16` (optional).
- **Description**: Prometheus-compatible metrics endpoint port. When set, enables the metrics listener (bind behavior can be overridden by `metrics_listen`).
- **Example**:
@@ -1967,7 +1964,7 @@ This document lists all configuration keys accepted by `config.toml`.
```
<a id="cfg-server-metrics_listen"></a>
- `metrics_listen`
- **Constraints / validation**: `String` or `null`. When set, must be in `IP:PORT` format.
- **Constraints / validation**: `String` (optional). When set, must be in `IP:PORT` format.
- **Description**: Full metrics bind address (`IP:PORT`), overrides `metrics_port` and binds on the specified address only.
- **Example**:
@@ -2010,6 +2007,105 @@ This document lists all configuration keys accepted by `config.toml`.
Note: When `server.proxy_protocol` is enabled, incoming PROXY protocol headers are parsed from the first bytes of the connection and the client source address is replaced with `src_addr` from the header. For security, the peer source IP (the direct connection address) is verified against `server.proxy_protocol_trusted_cidrs`; if this list is empty, PROXY headers are rejected and the connection is considered untrusted.
## [server.conntrack_control]
Note: The conntrack-control worker runs **only on Linux**. On other operating systems it is not started; if `inline_conntrack_control` is `true`, a warning is logged. Effective operation also requires **CAP_NET_ADMIN** and a usable backend (`nft` or `iptables` / `ip6tables` on `PATH`). The `conntrack` utility is used for optional table entry deletes under pressure.
| Key | Type | Default |
| --- | ---- | ------- |
| [`inline_conntrack_control`](#cfg-server-conntrack_control-inline_conntrack_control) | `bool` | `true` |
| [`mode`](#cfg-server-conntrack_control-mode) | `String` | `"tracked"` |
| [`backend`](#cfg-server-conntrack_control-backend) | `String` | `"auto"` |
| [`profile`](#cfg-server-conntrack_control-profile) | `String` | `"balanced"` |
| [`hybrid_listener_ips`](#cfg-server-conntrack_control-hybrid_listener_ips) | `IpAddr[]` | `[]` |
| [`pressure_high_watermark_pct`](#cfg-server-conntrack_control-pressure_high_watermark_pct) | `u8` | `85` |
| [`pressure_low_watermark_pct`](#cfg-server-conntrack_control-pressure_low_watermark_pct) | `u8` | `70` |
| [`delete_budget_per_sec`](#cfg-server-conntrack_control-delete_budget_per_sec) | `u64` | `4096` |
<a id="cfg-server-conntrack_control-inline_conntrack_control"></a>
- `inline_conntrack_control`
- **Constraints / validation**: `bool`.
- **Description**: Master switch for the runtime conntrack-control task: reconciles **raw/notrack** netfilter rules for listener ingress (see `mode`), samples load every second, and may run **`conntrack -D`** deletes for qualifying close events while **pressure mode** is active (see `delete_budget_per_sec`). When `false`, notrack rules are cleared and pressure-driven deletes are disabled.
- **Example**:
```toml
[server.conntrack_control]
inline_conntrack_control = true
```
<a id="cfg-server-conntrack_control-mode"></a>
- `mode`
- **Constraints / validation**: One of `tracked`, `notrack`, `hybrid` (case-insensitive; serialized lowercase).
- **Description**: **`tracked`**: do not install telemt notrack rules (connections stay in conntrack). **`notrack`**: mark matching ingress TCP to `server.port` as notrack — targets are derived from `[[server.listeners]]` if any, otherwise from `server.listen_addr_ipv4` / `server.listen_addr_ipv6` (unspecified addresses mean “any” for that family). **`hybrid`**: notrack only for addresses listed in `hybrid_listener_ips` (must be non-empty; validated at load).
- **Example**:
```toml
[server.conntrack_control]
mode = "notrack"
```
<a id="cfg-server-conntrack_control-backend"></a>
- `backend`
- **Constraints / validation**: One of `auto`, `nftables`, `iptables` (case-insensitive; serialized lowercase).
- **Description**: Which command set applies notrack rules. **`auto`**: use `nft` if present on `PATH`, else `iptables`/`ip6tables` if present. **`nftables`** / **`iptables`**: force that backend; missing binary means rules cannot be applied. The nft path uses table `inet telemt_conntrack` and a prerouting raw hook; iptables uses chain `TELEMT_NOTRACK` in the `raw` table.
- **Example**:
```toml
[server.conntrack_control]
backend = "auto"
```
<a id="cfg-server-conntrack_control-profile"></a>
- `profile`
- **Constraints / validation**: One of `conservative`, `balanced`, `aggressive` (case-insensitive; serialized lowercase).
- **Description**: When **conntrack pressure mode** is active (`pressure_*` watermarks), caps idle and activity timeouts to reduce conntrack churn: e.g. **client first-byte idle** (`client.rs`), **direct relay activity timeout** (`direct_relay.rs`), and **middle-relay idle policy** caps (`middle_relay.rs` via `ConntrackPressureProfile::*_cap_secs` / `direct_activity_timeout_secs`). More aggressive profiles use shorter caps.
- **Example**:
```toml
[server.conntrack_control]
profile = "balanced"
```
<a id="cfg-server-conntrack_control-hybrid_listener_ips"></a>
- `hybrid_listener_ips`
- **Constraints / validation**: `IpAddr[]`. Required to be **non-empty** when `mode = "hybrid"`. Ignored for `tracked` / `notrack`.
- **Description**: Explicit listener addresses that receive notrack rules in hybrid mode (split into IPv4 vs IPv6 rules by the implementation).
- **Example**:
```toml
[server.conntrack_control]
mode = "hybrid"
hybrid_listener_ips = ["203.0.113.10", "2001:db8::1"]
```
<a id="cfg-server-conntrack_control-pressure_high_watermark_pct"></a>
- `pressure_high_watermark_pct`
- **Constraints / validation**: Must be within `[1, 100]`.
- **Description**: Pressure mode **enters** when any of: connection fill vs `server.max_connections` (percentage, if `max_connections > 0`), **file-descriptor** usage vs process soft `RLIMIT_NOFILE`, **non-zero** `accept_permit_timeout` events in the last sample window, or **ME c2me send-full** counter delta. Entry compares relevant percentages against this high watermark (see `update_pressure_state` in `conntrack_control.rs`).
- **Example**:
```toml
[server.conntrack_control]
pressure_high_watermark_pct = 85
```
<a id="cfg-server-conntrack_control-pressure_low_watermark_pct"></a>
- `pressure_low_watermark_pct`
- **Constraints / validation**: Must be **strictly less than** `pressure_high_watermark_pct`.
- **Description**: Pressure mode **clears** only after **three** consecutive one-second samples where all signals are at or below this low watermark and the accept-timeout / ME-queue deltas are zero (hysteresis).
- **Example**:
```toml
[server.conntrack_control]
pressure_low_watermark_pct = 70
```
<a id="cfg-server-conntrack_control-delete_budget_per_sec"></a>
- `delete_budget_per_sec`
- **Constraints / validation**: Must be `> 0`.
- **Description**: Maximum number of **`conntrack -D`** attempts **per second** while pressure mode is active (token bucket refilled each second). Deletes run only for close events with reasons **timeout**, **pressure**, or **reset**; each attempt consumes a token regardless of outcome.
- **Example**:
```toml
[server.conntrack_control]
delete_budget_per_sec = 4096
```
## [server.api]
Note: This section also accepts the legacy alias `[server.admin_api]` (same schema as `[server.api]`).
@@ -2158,9 +2254,9 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
| Key | Type | Default |
| --- | ---- | ------- |
| [`ip`](#cfg-server-listeners-ip) | `IpAddr` | — |
| [`announce`](#cfg-server-listeners-announce) | `String` or `null` | — |
| [`announce_ip`](#cfg-server-listeners-announce_ip) | `IpAddr` or `null` | — |
| [`proxy_protocol`](#cfg-server-listeners-proxy_protocol) | `bool` or `null` | `null` |
| [`announce`](#cfg-server-listeners-announce) | `String` | — |
| [`announce_ip`](#cfg-server-listeners-announce_ip) | `IpAddr` | — |
| [`proxy_protocol`](#cfg-server-listeners-proxy_protocol) | `bool` | — |
| [`reuse_allow`](#cfg-server-listeners-reuse_allow) | `bool` | `false` |
<a id="cfg-server-listeners-ip"></a>
@@ -2175,7 +2271,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
```
<a id="cfg-server-listeners-announce"></a>
- `announce`
- **Constraints / validation**: `String` or `null`. Must not be empty when set.
- **Constraints / validation**: `String` (optional). Must not be empty when set.
- **Description**: Public IP/domain announced in proxy links for this listener. Takes precedence over `announce_ip`.
- **Example**:
@@ -2186,7 +2282,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
```
<a id="cfg-server-listeners-announce_ip"></a>
- `announce_ip`
- **Constraints / validation**: `IpAddr` or `null`. Deprecated. Use `announce`.
- **Constraints / validation**: `IpAddr` (optional). Deprecated. Use `announce`.
- **Description**: Deprecated legacy announce IP. During config load it is migrated to `announce` when `announce` is not set.
- **Example**:
@@ -2197,7 +2293,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
```
<a id="cfg-server-listeners-proxy_protocol"></a>
- `proxy_protocol`
- **Constraints / validation**: `bool` or `null`. When set, overrides `server.proxy_protocol` for this listener.
- **Constraints / validation**: `bool` (optional). When set, overrides `server.proxy_protocol` for this listener.
- **Description**: Per-listener PROXY protocol override.
- **Example**:
@@ -2351,9 +2447,9 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
| [`tls_fetch_scope`](#cfg-censorship-tls_fetch_scope) | `String` | `""` |
| [`tls_fetch`](#cfg-censorship-tls_fetch) | `Table` | built-in defaults |
| [`mask`](#cfg-censorship-mask) | `bool` | `true` |
| [`mask_host`](#cfg-censorship-mask_host) | `String` or `null` | `null` |
| [`mask_host`](#cfg-censorship-mask_host) | `String` | — |
| [`mask_port`](#cfg-censorship-mask_port) | `u16` | `443` |
| [`mask_unix_sock`](#cfg-censorship-mask_unix_sock) | `String` or `null` | `null` |
| [`mask_unix_sock`](#cfg-censorship-mask_unix_sock) | `String` | — |
| [`fake_cert_len`](#cfg-censorship-fake_cert_len) | `usize` | `2048` |
| [`tls_emulation`](#cfg-censorship-tls_emulation) | `bool` | `true` |
| [`tls_front_dir`](#cfg-censorship-tls_front_dir) | `String` | `"tlsfront"` |
@@ -2440,8 +2536,8 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
```
<a id="cfg-censorship-mask_host"></a>
- `mask_host`
- **Constraints / validation**: `String` or `null`.
- If `mask_unix_sock` is set, `mask_host` must be `null` (mutually exclusive).
- **Constraints / validation**: `String` (optional).
- If `mask_unix_sock` is set, `mask_host` must be omitted (mutually exclusive).
- If `mask_host` is not set and `mask_unix_sock` is not set, Telemt defaults `mask_host` to `tls_domain`.
- **Description**: Upstream mask host for TLS fronting relay.
- **Example**:
@@ -2462,7 +2558,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
```
<a id="cfg-censorship-mask_unix_sock"></a>
- `mask_unix_sock`
- **Constraints / validation**: `String` or `null`.
- **Constraints / validation**: `String` (optional).
- Must not be empty when set.
- Unix only; rejected on non-Unix platforms.
- On Unix, must be \(\le 107\) bytes (path length limit).
@@ -2882,6 +2978,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
| [`users`](#cfg-access-users) | `Map<String, String>` | `{"default": "000…000"}` |
| [`user_ad_tags`](#cfg-access-user_ad_tags) | `Map<String, String>` | `{}` |
| [`user_max_tcp_conns`](#cfg-access-user_max_tcp_conns) | `Map<String, usize>` | `{}` |
| [`user_max_tcp_conns_global_each`](#cfg-access-user_max_tcp_conns_global_each) | `usize` | `0` |
| [`user_expirations`](#cfg-access-user_expirations) | `Map<String, DateTime<Utc>>` | `{}` |
| [`user_data_quota`](#cfg-access-user_data_quota) | `Map<String, u64>` | `{}` |
| [`user_max_unique_ips`](#cfg-access-user_max_unique_ips) | `Map<String, usize>` | `{}` |
@@ -2926,6 +3023,20 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
[access.user_max_tcp_conns]
alice = 500
```
<a id="cfg-access-user_max_tcp_conns_global_each"></a>
- `user_max_tcp_conns_global_each`
- **Constraints / validation**: `usize`. `0` disables the inherited limit.
- **Description**: Global per-user maximum concurrent TCP connections, applied when a user has **no positive** entry in `[access.user_max_tcp_conns]` (a missing key, or a value of `0`, both fall through to this setting). Per-user limits greater than `0` in `user_max_tcp_conns` take precedence.
- **Example**:
```toml
[access]
user_max_tcp_conns_global_each = 200
[access.user_max_tcp_conns]
alice = 500 # uses 500, not the global cap
# bob has no entry → uses 200
```
<a id="cfg-access-user_expirations"></a>
- `user_expirations`
- **Constraints / validation**: `Map<String, DateTime<Utc>>`. Each value must be a valid RFC3339 / ISO-8601 datetime.
@@ -3027,13 +3138,13 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
| [`weight`](#cfg-upstreams-weight) | `u16` | `1` |
| [`enabled`](#cfg-upstreams-enabled) | `bool` | `true` |
| [`scopes`](#cfg-upstreams-scopes) | `String` | `""` |
| [`interface`](#cfg-upstreams-interface) | `String` or `null` | `null` |
| [`bind_addresses`](#cfg-upstreams-bind_addresses) | `String[]` or `null` | `null` |
| [`interface`](#cfg-upstreams-interface) | `String` | — |
| [`bind_addresses`](#cfg-upstreams-bind_addresses) | `String[]` | — |
| [`url`](#cfg-upstreams-url) | `String` | — |
| [`address`](#cfg-upstreams-address) | `String` | — |
| [`user_id`](#cfg-upstreams-user_id) | `String` or `null` | `null` |
| [`username`](#cfg-upstreams-username) | `String` or `null` | `null` |
| [`password`](#cfg-upstreams-password) | `String` or `null` | `null` |
| [`user_id`](#cfg-upstreams-user_id) | `String` | — |
| [`username`](#cfg-upstreams-username) | `String` | — |
| [`password`](#cfg-upstreams-password) | `String` | — |
<a id="cfg-upstreams-type"></a>
- `type`
@@ -3090,7 +3201,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
```
<a id="cfg-upstreams-interface"></a>
- `interface`
- **Constraints / validation**: `String` or `null`.
- **Constraints / validation**: `String` (optional).
- For `"direct"`: may be an IP address (used as explicit local bind) or an OS interface name (resolved to an IP at runtime; Unix only).
- For `"socks4"`/`"socks5"`: supported only when `address` is an `IP:port` literal; when `address` is a hostname, interface binding is ignored.
- For `"shadowsocks"`: passed to the shadowsocks connector as an optional outbound bind hint.
@@ -3109,7 +3220,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
```
<a id="cfg-upstreams-bind_addresses"></a>
- `bind_addresses`
- **Constraints / validation**: `String[]` or `null`. Applies only to `type = "direct"`.
- **Constraints / validation**: `String[]` (optional). Applies only to `type = "direct"`.
- Each entry should be an IP address string.
- At runtime, Telemt selects an address that matches the target family (IPv4 vs IPv6). If `bind_addresses` is set and none match the target family, the connect attempt fails.
- **Description**: Explicit local source addresses for outgoing direct TCP connects. When multiple addresses are provided, selection is round-robin.
@@ -3150,7 +3261,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
```
<a id="cfg-upstreams-user_id"></a>
- `user_id`
- **Constraints / validation**: `String` or `null`. Only for `type = "socks4"`.
- **Constraints / validation**: `String` (optional). Only for `type = "socks4"`.
- **Description**: SOCKS4 CONNECT user ID. Note: when a request scope is selected, Telemt may override this with the selected scope value.
- **Example**:
@@ -3162,7 +3273,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
```
<a id="cfg-upstreams-username"></a>
- `username`
- **Constraints / validation**: `String` or `null`. Only for `type = "socks5"`.
- **Constraints / validation**: `String` (optional). Only for `type = "socks5"`.
- **Description**: SOCKS5 username (for username/password authentication). Note: when a request scope is selected, Telemt may override this with the selected scope value.
- **Example**:
@@ -3174,7 +3285,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
```
<a id="cfg-upstreams-password"></a>
- `password`
- **Constraints / validation**: `String` or `null`. Only for `type = "socks5"`.
- **Constraints / validation**: `String` (optional). Only for `type = "socks5"`.
- **Description**: SOCKS5 password (for username/password authentication). Note: when a request scope is selected, Telemt may override this with the selected scope value.
- **Example**:

File diff suppressed because it is too large Load Diff

View File

@@ -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,13 +31,130 @@ 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:
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
## 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 many people can use one link
### How many people can use one link
By default, an unlimited number of people can use a single link.
However, you can limit the number of unique IP addresses for each user:
```toml
@@ -47,8 +163,7 @@ 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
### 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 +179,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 +188,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 +202,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

View File

@@ -32,11 +32,145 @@ use_middle_proxy = true
hello = "ad_tag"
hello2 = "ad_tag2"
```
## Распознаваемость для DPI и сканеров
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах: мы уже отправили первоначальные изменения разработчикам Telegram Desktop и работаем над обновлениями для других клиентов.
- Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов;
- Исходя из этого: если `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
## Что такое 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 +238,7 @@ max_connections = 10000 # 0 - без ограничений, 10000 - по у
```
### Upstream Manager
Для настройки исходящих подключений (апстримов) добавьте соответствующие параметры в секцию `[[upstreams]]` файла конфигурации:
Для настройки исходящих подключений (Upstreams) добавьте соответствующие параметры в секцию `[[upstreams]]` файла конфигурации:
#### Привязка к исходящему IP-адресу
```toml
@@ -119,20 +253,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
```

View File

@@ -1,92 +0,0 @@
# Öffentliche TELEMT-Lizenz 3
***Alle Rechte vorbehalten (c) 2026 Telemt***
Hiermit wird jeder Person, die eine Kopie dieser Software und der dazugehörigen Dokumentation (nachfolgend "Software") erhält, unentgeltlich die Erlaubnis erteilt, die Software ohne Einschränkungen zu nutzen, einschließlich des Rechts, die Software zu verwenden, zu vervielfältigen, zu ändern, abgeleitete Werke zu erstellen, zu verbinden, zu veröffentlichen, zu verbreiten, zu unterlizenzieren und/oder Kopien der Software zu verkaufen sowie diese Rechte auch denjenigen einzuräumen, denen die Software zur Verfügung gestellt wird, vorausgesetzt, dass sämtliche Urheberrechtshinweise sowie die Bedingungen und Bestimmungen dieser Lizenz eingehalten werden.
### Begriffsbestimmungen
Für die Zwecke dieser Lizenz gelten die folgenden Definitionen:
**"Software" (Software)** — die Telemt-Software einschließlich Quellcode, Dokumentation und sämtlicher zugehöriger Dateien, die unter den Bedingungen dieser Lizenz verbreitet werden.
**"Contributor" (Contributor)** — jede natürliche oder juristische Person, die Code, Patches, Dokumentation oder andere Materialien eingereicht hat, die von den Maintainers des Projekts angenommen und in die Software aufgenommen wurden.
**"Beitrag" (Contribution)** — jedes urheberrechtlich geschützte Werk, das bewusst zur Aufnahme in die Software eingereicht wurde.
**"Modifizierte Version" (Modified Version)** — jede Version der Software, die gegenüber der ursprünglichen Software geändert, angepasst, erweitert oder anderweitig modifiziert wurde.
**"Maintainers" (Maintainers)** — natürliche oder juristische Personen, die für das offizielle Telemt-Projekt und dessen offizielle Veröffentlichungen verantwortlich sind.
### 1 Urheberrechtshinweis (Attribution)
Bei der Weitergabe der Software, sowohl in Form des Quellcodes als auch in binärer Form, MÜSSEN folgende Elemente erhalten bleiben:
- der oben genannte Urheberrechtshinweis;
- der vollständige Text dieser Lizenz;
- sämtliche bestehenden Hinweise auf Urheberschaft.
### 2 Hinweis auf Modifikationen
Wenn Änderungen an der Software vorgenommen werden, MUSS die Person, die diese Änderungen vorgenommen hat, eindeutig darauf hinweisen, dass die Software modifiziert wurde, und eine kurze Beschreibung der vorgenommenen Änderungen beifügen.
Modifizierte Versionen der Software DÜRFEN NICHT als die originale Version von Telemt dargestellt werden.
### 3 Marken und Bezeichnungen
Diese Lizenz GEWÄHRT KEINE Rechte zur Nutzung der Bezeichnung **"Telemt"**, des Telemt-Logos oder sonstiger Marken, Kennzeichen oder Branding-Elemente von Telemt.
Weiterverbreitete oder modifizierte Versionen der Software DÜRFEN die Bezeichnung Telemt nicht in einer Weise verwenden, die bei Nutzern den Eindruck eines offiziellen Ursprungs oder einer Billigung durch das Telemt-Projekt erwecken könnte, sofern hierfür keine ausdrückliche Genehmigung der Maintainers vorliegt.
Die Verwendung der Bezeichnung **Telemt** zur Beschreibung einer modifizierten Version der Software ist nur zulässig, wenn diese Version eindeutig als modifiziert oder inoffiziell gekennzeichnet ist.
Jegliche Verbreitung, die Nutzer vernünftigerweise darüber täuschen könnte, dass es sich um eine offizielle Veröffentlichung von Telemt handelt, ist untersagt.
### 4 Transparenz bei der Verbreitung von Binärversionen
Im Falle der Verbreitung kompilierter Binärversionen der Software wird der Verbreiter HIERMIT ERMUTIGT (encouraged), soweit dies vernünftigerweise möglich ist, Zugang zum entsprechenden Quellcode sowie zu den Build-Anweisungen bereitzustellen.
Diese Praxis trägt zur Transparenz bei und ermöglicht es Empfängern, die Integrität und Reproduzierbarkeit der verbreiteten Builds zu überprüfen.
## 5 Gewährung einer Patentlizenz und Beendigung von Rechten
Jeder Contributor gewährt den Empfängern der Software eine unbefristete, weltweite, nicht-exklusive, unentgeltliche, lizenzgebührenfreie und unwiderrufliche Patentlizenz für:
- die Herstellung,
- die Beauftragung der Herstellung,
- die Nutzung,
- das Anbieten zum Verkauf,
- den Verkauf,
- den Import,
- sowie jede sonstige Verbreitung der Software.
Diese Patentlizenz erstreckt sich ausschließlich auf solche Patentansprüche, die notwendigerweise durch den jeweiligen Beitrag des Contributors allein oder in Kombination mit der Software verletzt würden.
Leitet eine Person ein Patentverfahren ein oder beteiligt sich daran, einschließlich Gegenklagen oder Kreuzklagen, mit der Behauptung, dass die Software oder ein darin enthaltener Beitrag ein Patent verletzt, **erlöschen sämtliche durch diese Lizenz gewährten Rechte für diese Person unmittelbar mit Einreichung der Klage**.
Darüber hinaus erlöschen alle durch diese Lizenz gewährten Rechte **automatisch**, wenn eine Person ein gerichtliches Verfahren einleitet, in dem behauptet wird, dass die Software selbst ein Patent oder andere Rechte des geistigen Eigentums verletzt.
### 6 Beteiligung und Beiträge zur Entwicklung
Sofern ein Contributor nicht ausdrücklich etwas anderes erklärt, gilt jeder Beitrag, der bewusst zur Aufnahme in die Software eingereicht wird, als unter den Bedingungen dieser Lizenz lizenziert.
Durch die Einreichung eines Beitrags gewährt der Contributor den Maintainers des Telemt-Projekts sowie allen Empfängern der Software die in dieser Lizenz beschriebenen Rechte in Bezug auf diesen Beitrag.
### 7 Urheberhinweis bei Netzwerk- und Servicenutzung
Wird die Software zur Bereitstellung eines öffentlich zugänglichen Netzwerkdienstes verwendet, MUSS der Betreiber dieses Dienstes einen Hinweis auf die Urheberschaft von Telemt an mindestens einer der folgenden Stellen anbringen:
* in der Servicedokumentation;
* in der Dienstbeschreibung;
* auf einer Seite "Über" oder einer vergleichbaren Informationsseite;
* in anderen für Nutzer zugänglichen Materialien, die in angemessenem Zusammenhang mit dem Dienst stehen.
Ein solcher Hinweis DARF NICHT den Eindruck erwecken, dass der Dienst vom Telemt-Projekt oder dessen Maintainers unterstützt oder offiziell gebilligt wird.
### 8 Haftungsausschluss und salvatorische Klausel
DIE SOFTWARE WIRD "WIE BESEHEN" BEREITGESTELLT, OHNE JEGLICHE AUSDRÜCKLICHE ODER STILLSCHWEIGENDE GEWÄHRLEISTUNG, EINSCHLIESSLICH, ABER NICHT BESCHRÄNKT AUF GEWÄHRLEISTUNGEN DER MARKTGÄNGIGKEIT, DER EIGNUNG FÜR EINEN BESTIMMTEN ZWECK UND DER NICHTVERLETZUNG VON RECHTEN.
IN KEINEM FALL HAFTEN DIE AUTOREN ODER RECHTEINHABER FÜR IRGENDWELCHE ANSPRÜCHE, SCHÄDEN ODER SONSTIGE HAFTUNG, DIE AUS VERTRAG, UNERLAUBTER HANDLUNG ODER AUF ANDERE WEISE AUS DER SOFTWARE ODER DER NUTZUNG DER SOFTWARE ENTSTEHEN.
SOLLTE EINE BESTIMMUNG DIESER LIZENZ ALS UNWIRKSAM ODER NICHT DURCHSETZBAR ANGESEHEN WERDEN, IST DIESE BESTIMMUNG SO AUSZULEGEN, DASS SIE DEM URSPRÜNGLICHEN WILLEN DER PARTEIEN MÖGLICHST NAHEKOMMT; DIE ÜBRIGEN BESTIMMUNGEN BLEIBEN DAVON UNBERÜHRT UND IN VOLLER WIRKUNG.

View File

@@ -1,143 +0,0 @@
###### TELEMT Public License 3 ######
##### Copyright (c) 2026 Telemt #####
Permission is hereby granted, free of charge, to any person obtaining a copy
of this Software and associated documentation files (the "Software"),
to use, reproduce, modify, prepare derivative works of, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit
persons to whom the Software is furnished to do so, provided that all
copyright notices, license terms, and conditions set forth in this License
are preserved and complied with.
### Official Translations
The canonical version of this License is the English version.
Official translations are provided for informational purposes only
and for convenience, and do not have legal force. In case of any
discrepancy, the English version of this License shall prevail.
Available versions:
- English in Markdown: docs/LICENSE/LICENSE.md
- German: docs/LICENSE/LICENSE.de.md
- Russian: docs/LICENSE/LICENSE.ru.md
### Definitions
For the purposes of this License:
"Software" means the Telemt software, including source code, documentation,
and any associated files distributed under this License.
"Contributor" means any person or entity that submits code, patches,
documentation, or other contributions to the Software that are accepted
into the Software by the maintainers.
"Contribution" means any work of authorship intentionally submitted
to the Software for inclusion in the Software.
"Modified Version" means any version of the Software that has been
changed, adapted, extended, or otherwise modified from the original
Software.
"Maintainers" means the individuals or entities responsible for
the official Telemt project and its releases.
#### 1 Attribution
Redistributions of the Software, in source or binary form, MUST RETAIN the
above copyright notice, this license text, and any existing attribution
notices.
#### 2 Modification Notice
If you modify the Software, you MUST clearly state that the Software has been
modified and include a brief description of the changes made.
Modified versions MUST NOT be presented as the original Telemt.
#### 3 Trademark and Branding
This license DOES NOT grant permission to use the name "Telemt",
the Telemt logo, or any Telemt trademarks or branding.
Redistributed or modified versions of the Software MAY NOT use the Telemt
name in a way that suggests endorsement or official origin without explicit
permission from the Telemt maintainers.
Use of the name "Telemt" to describe a modified version of the Software
is permitted only if the modified version is clearly identified as a
modified or unofficial version.
Any distribution that could reasonably confuse users into believing that
the software is an official Telemt release is prohibited.
#### 4 Binary Distribution Transparency
If you distribute compiled binaries of the Software,
you are ENCOURAGED to provide access to the corresponding
source code and build instructions where reasonably possible.
This helps preserve transparency and allows recipients to verify the
integrity and reproducibility of distributed builds.
#### 5 Patent Grant and Defensive Termination Clause
Each contributor grants you a perpetual, worldwide, non-exclusive,
no-charge, royalty-free, irrevocable patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Software.
This patent license applies only to those patent claims necessarily
infringed by the contributors contribution alone or by combination of
their contribution with the Software.
If you initiate or participate in any patent litigation, including
cross-claims or counterclaims, alleging that the Software or any
contribution incorporated within the Software constitutes patent
infringement, then **all rights granted to you under this license shall
terminate immediately** as of the date such litigation is filed.
Additionally, if you initiate legal action alleging that the
Software itself infringes your patent or other intellectual
property rights, then all rights granted to you under this
license SHALL TERMINATE automatically.
#### 6 Contributions
Unless you explicitly state otherwise, any Contribution intentionally
submitted for inclusion in the Software shall be licensed under the terms
of this License.
By submitting a Contribution, you grant the Telemt maintainers and all
recipients of the Software the rights described in this License with
respect to that Contribution.
#### 7 Network Use Attribution
If the Software is used to provide a publicly accessible network service,
the operator of such service MUST provide attribution to Telemt in at least
one of the following locations:
- service documentation
- service description
- an "About" or similar informational page
- other user-visible materials reasonably associated with the service
Such attribution MUST NOT imply endorsement by the Telemt project or its
maintainers.
#### 8 Disclaimer of Warranty and Severability Clause
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE
IF ANY PROVISION OF THIS LICENSE IS HELD TO BE INVALID OR UNENFORCEABLE,
SUCH PROVISION SHALL BE INTERPRETED TO REFLECT THE ORIGINAL INTENT
OF THE PARTIES AS CLOSELY AS POSSIBLE, AND THE REMAINING PROVISIONS
SHALL REMAIN IN FULL FORCE AND EFFECT

View File

@@ -1,90 +0,0 @@
# Публичная лицензия TELEMT 3
***Все права защищёны (c) 2026 Telemt***
Настоящим любому лицу, получившему копию данного программного обеспечения и сопутствующей документации (далее — "Программное обеспечение"), безвозмездно предоставляется разрешение использовать Программное обеспечение без ограничений, включая право использовать, воспроизводить, изменять, создавать производные произведения, объединять, публиковать, распространять, сублицензировать и (или) продавать копии Программного обеспечения, а также предоставлять такие права лицам, которым предоставляется Программное обеспечение, при условии соблюдения всех уведомлений об авторских правах, условий и положений настоящей Лицензии.
### Определения
Для целей настоящей Лицензии применяются следующие определения:
**"Программное обеспечение" (Software)** — программное обеспечение Telemt, включая исходный код, документацию и любые связанные файлы, распространяемые на условиях настоящей Лицензии.
**"Контрибьютор" (Contributor)** — любое физическое или юридическое лицо, направившее код, исправления (патчи), документацию или иные материалы, которые были приняты мейнтейнерами проекта и включены в состав Программного обеспечения.
**"Вклад" (Contribution)** — любое произведение авторского права, намеренно представленное для включения в состав Программного обеспечения.
**"Модифицированная версия" (Modified Version)** — любая версия Программного обеспечения, которая была изменена, адаптирована, расширена или иным образом модифицирована по сравнению с исходным Программным обеспечением.
**"Мейнтейнеры" (Maintainers)** — физические или юридические лица, ответственные за официальный проект Telemt и его официальные релизы.
### 1 Указание авторства
При распространении Программного обеспечения, как в форме исходного кода, так и в бинарной форме, ДОЛЖНЫ СОХРАНЯТЬСЯ:
- указанное выше уведомление об авторских правах;
- текст настоящей Лицензии;
- любые существующие уведомления об авторстве.
### 2 Уведомление о модификации
В случае внесения изменений в Программное обеспечение лицо, осуществившее такие изменения, ОБЯЗАНО явно указать, что Программное обеспечение было модифицировано, а также включить краткое описание внесённых изменений.
Модифицированные версии Программного обеспечения НЕ ДОЛЖНЫ представляться как оригинальная версия Telemt.
### 3 Товарные знаки и обозначения
Настоящая Лицензия НЕ ПРЕДОСТАВЛЯЕТ права использовать наименование **"Telemt"**, логотип Telemt, а также любые товарные знаки, фирменные обозначения или элементы бренда Telemt.
Распространяемые или модифицированные версии Программного обеспечения НЕ ДОЛЖНЫ использовать наименование Telemt таким образом, который может создавать у пользователей впечатление официального происхождения либо одобрения со стороны проекта Telemt без явного разрешения мейнтейнеров проекта.
Использование наименования **Telemt** для описания модифицированной версии Программного обеспечения допускается только при условии, что такая версия ясно обозначена как модифицированная или неофициальная.
Запрещается любое распространение, которое может разумно вводить пользователей в заблуждение относительно того, что программное обеспечение является официальным релизом Telemt.
### 4 Прозрачность распространения бинарных версий
В случае распространения скомпилированных бинарных версий Программного обеспечения распространитель НАСТОЯЩИМ ПОБУЖДАЕТСЯ предоставлять доступ к соответствующему исходному коду и инструкциям по сборке, если это разумно возможно.
Такая практика способствует прозрачности распространения и позволяет получателям проверять целостность и воспроизводимость распространяемых сборок.
### 5 Предоставление патентной лицензии и прекращение прав
Каждый контрибьютор предоставляет получателям Программного обеспечения бессрочную, всемирную, неисключительную, безвозмездную, не требующую выплаты роялти и безотзывную патентную лицензию на:
- изготовление,
- поручение изготовления,
- использование,
- предложение к продаже,
- продажу,
- импорт,
- и иное распространение Программного обеспечения.
Такая патентная лицензия распространяется исключительно на те патентные требования, которые неизбежно нарушаются соответствующим вкладом контрибьютора как таковым либо его сочетанием с Программным обеспечением.
Если лицо инициирует либо участвует в каком-либо судебном разбирательстве по патентному спору, включая встречные или перекрёстные иски, утверждая, что Программное обеспечение либо любой вклад, включённый в него, нарушает патент, **все права, предоставленные такому лицу настоящей Лицензией, немедленно прекращаются** с даты подачи соответствующего иска.
Кроме того, если лицо инициирует судебное разбирательство, утверждая, что само Программное обеспечение нарушает его патентные либо иные права интеллектуальной собственности, все права, предоставленные настоящей Лицензией, **автоматически прекращаются**.
### 6 Участие и вклад в разработку
Если контрибьютор явно не указал иное, любой Вклад, намеренно представленный для включения в Программное обеспечение, считается лицензированным на условиях настоящей Лицензии.
Путём предоставления Вклада контрибьютор предоставляет мейнтейнером проекта Telemt и всем получателям Программного обеспечения права, предусмотренные настоящей Лицензией, в отношении такого Вклада.
### 7 Указание авторства при сетевом и сервисном использовании
В случае использования Программного обеспечения для предоставления публично доступного сетевого сервиса оператор такого сервиса ОБЯЗАН обеспечить указание авторства Telemt как минимум в одном из следующих мест:
- документация сервиса;
- описание сервиса;
- страница "О программе" или аналогичная информационная страница;
- иные материалы, доступные пользователям и разумно связанные с данным сервисом.
Такое указание авторства НЕ ДОЛЖНО создавать впечатление одобрения или официальной поддержки со стороны проекта Telemt либо его мейнтейнеров.
### 8 Отказ от гарантий и делимость положений
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ "КАК ЕСТЬ", БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ ГАРАНТИЯМИ КОММЕРЧЕСКОЙ ПРИГОДНОСТИ, ПРИГОДНОСТИ ДЛЯ КОНКРЕТНОЙ ЦЕЛИ И НЕНАРУШЕНИЯ ПРАВ.
НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ТРЕБОВАНИЯМ, УБЫТКАМ ИЛИ ИНОЙ ОТВЕТСТВЕННОСТИ, ВОЗНИКАЮЩЕЙ В РЕЗУЛЬТАТЕ ДОГОВОРА, ДЕЛИКТА ИЛИ ИНЫМ ОБРАЗОМ, СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ЕГО ИСПОЛЬЗОВАНИЕМ.
В СЛУЧАЕ ЕСЛИ КАКОЕ-ЛИБО ПОЛОЖЕНИЕ НАСТОЯЩЕЙ ЛИЦЕНЗИИ ПРИЗНАЁТСЯ НЕДЕЙСТВИТЕЛЬНЫМ ИЛИ НЕПРИМЕНИМЫМ, ТАКОЕ ПОЛОЖЕНИЕ ПОДЛЕЖИТ ТОЛКОВАНИЮ МАКСИМАЛЬНО БЛИЗКО К ИСХОДНОМУ НАМЕРЕНИЮ СТОРОН, ПРИ ЭТОМ ОСТАЛЬНЫЕ ПОЛОЖЕНИЯ СОХРАНЯЮТ ПОЛНУЮ ЮРИДИЧЕСКУЮ СИЛУ.

View File

@@ -0,0 +1,120 @@
# TELEMT License 3.3
***Copyright (c) 2026 Telemt***
Permission is hereby granted, free of charge, to any person obtaining a copy of this Software and associated documentation files (the "Software"), to use, reproduce, modify, prepare derivative works of, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, provided that all copyright notices, license terms, and conditions set forth in this License are preserved and complied with.
### Official Translations
The canonical version of this License is the English version.
Official translations are provided for informational purposes only and for convenience, and do not have legal force. In case of any discrepancy, the English version of this License shall prevail.
| Language | Location |
|-------------|----------|
| English | [docs/LICENSE/TELEMT-LICENSE.en.md](https://github.com/telemt/telemt/tree/main/docs/LICENSE/TELEMT-LICENSE.en.md)|
| German | [docs/LICENSE/TELEMT-LICENSE.de.md](https://github.com/telemt/telemt/tree/main/docs/LICENSE/TELEMT-LICENSE.de.md)|
| Russian | [docs/LICENSE/TELEMT-LICENSE.ru.md](https://github.com/telemt/telemt/tree/main/docs/LICENSE/TELEMT-LICENSE.ru.md)|
### License Versioning Policy
This License is version 3.3 of the TELEMT License.
Each version of the Software is licensed under the License that accompanies its corresponding source code distribution.
Future versions of the Software may be distributed under a different version of the TELEMT Public License or under a different license, as determined by the Telemt maintainers.
Any such change of license applies only to the versions of the Software distributed with the new license and SHALL NOT retroactively affect any previously released versions of the Software.
Recipients of the Software are granted rights only under the License provided with the version of the Software they received.
Redistributions of the Software, including Modified Versions, MUST preserve the copyright notices, license text, and conditions of this License for all portions of the Software derived from Telemt.
Additional terms or licenses may be applied to modifications or additional code added by a redistributor, provided that such terms do not restrict or alter the rights granted under this License for the original Telemt Software.
Nothing in this section limits the rights granted under this License for versions of the Software already released.
### Definitions
For the purposes of this License:
**"Software"** means the Telemt software, including source code, documentation, and any associated files distributed under this License.
**"Contributor"** means any person or entity that submits code, patches, documentation, or other contributions to the Software that are accepted into the Software by the maintainers.
**"Contribution"** means any work of authorship intentionally submitted to the Software for inclusion in the Software.
**"Modified Version"** means any version of the Software that has been changed, adapted, extended, or otherwise modified from the original Software.
**"Maintainers"** means the individuals or entities responsible for the official Telemt project and its releases.
### 1 Attribution
Redistributions of the Software, in source or binary form, MUST RETAIN:
- the above copyright notice;
- this license text;
- any existing attribution notices.
### 2 Modification Notice
If you modify the Software, you MUST clearly state that the Software has been modified and include a brief description of the changes made.
Modified versions MUST NOT be presented as the original Telemt.
### 3 Trademark and Branding
This license DOES NOT grant permission to use the name "Telemt", the Telemt logo, or any Telemt trademarks or branding.
Redistributed or modified versions of the Software MAY NOT use the Telemt name in a way that suggests endorsement or official origin without explicit permission from the Telemt maintainers.
Use of the name "Telemt" to describe a modified version of the Software is permitted only if the modified version is clearly identified as a modified or unofficial version.
Any distribution that could reasonably confuse users into believing that the software is an official Telemt release is prohibited.
### 4 Binary Distribution Transparency
If you distribute compiled binaries of the Software, you are ENCOURAGED to provide access to the corresponding source code and build instructions where reasonably possible.
This helps preserve transparency and allows recipients to verify the integrity and reproducibility of distributed builds.
### 5 Patent Grant and Defensive Termination Clause
Each contributor grants you a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to:
- make,
- have made,
- use,
- offer to sell,
- sell,
- import,
- and otherwise transfer the Software.
This patent license applies only to those patent claims necessarily infringed by the contributors contribution alone or by combination of their contribution with the Software.
If you initiate or participate in any patent litigation, including cross-claims or counterclaims, alleging that the Software or any contribution incorporated within the Software constitutes patent infringement, then **all rights granted to you under this license shall terminate immediately** as of the date such litigation is filed.
Additionally, if you initiate legal action alleging that the Software itself infringes your patent or other intellectual property rights, then all rights granted to you under this license SHALL TERMINATE automatically.
### 6 Contributions
Unless you explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Software shall be licensed under the terms of this License.
By submitting a Contribution, you grant the Telemt maintainers and all recipients of the Software the rights described in this License with respect to that Contribution.
### 7 Network Use Attribution
If the Software is used to provide a publicly accessible network service, the operator of such service SHOULD provide attribution to Telemt in at least one of the following locations:
- service documentation;
- service description;
- an "About" or similar informational page;
- other user-visible materials reasonably associated with the service.
Such attribution MUST NOT imply endorsement by the Telemt project or its maintainers.
### 8 Disclaimer of Warranty and Severability Clause
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
IF ANY PROVISION OF THIS LICENSE IS HELD TO BE INVALID OR UNENFORCEABLE, SUCH PROVISION SHALL BE INTERPRETED TO REFLECT THE ORIGINAL INTENT OF THE PARTIES AS CLOSELY AS POSSIBLE, AND THE REMAINING PROVISIONS SHALL REMAIN IN FULL FORCE AND EFFECT.

View File

@@ -0,0 +1,120 @@
# TELEMT Лицензия 3.3
***Copyright (c) 2026 Telemt***
Настоящим безвозмездно предоставляется разрешение любому лицу, получившему копию данного программного обеспечения и сопутствующей документации (далее — "Программное обеспечение"), использовать, воспроизводить, изменять, создавать производные произведения, объединять, публиковать, распространять, сублицензировать и/или продавать копии Программного обеспечения, а также разрешать лицам, которым предоставляется Программное обеспечение, осуществлять указанные действия при условии соблюдения и сохранения всех уведомлений об авторском праве, условий и положений настоящей Лицензии.
### Официальные переводы
Канонической версией настоящей Лицензии является версия на английском языке.
Официальные переводы предоставляются исключительно в информационных целях и для удобства и не имеют юридической силы. В случае любых расхождений приоритет имеет английская версия.
| Язык | Расположение |
|------------|--------------|
| Русский | [docs/LICENSE/TELEMT-LICENSE.ru.md](https://github.com/telemt/telemt/tree/main/docs/LICENSE/TELEMT-LICENSE.ru.md)|
| Английский | [docs/LICENSE/TELEMT-LICENSE.en.md](https://github.com/telemt/telemt/tree/main/docs/LICENSE/TELEMT-LICENSE.en.md)|
| Немецкий | [docs/LICENSE/TELEMT-LICENSE.de.md](https://github.com/telemt/telemt/tree/main/docs/LICENSE/TELEMT-LICENSE.de.md)|
### Политика версионирования лицензии
Настоящая Лицензия является версией 3.3 Лицензии TELEMT.
Каждая версия Программного обеспечения лицензируется в соответствии с Лицензией, сопровождающей соответствующее распространение исходного кода.
Будущие версии Программного обеспечения могут распространяться в соответствии с иной версией Лицензии TELEMT Public License либо под иной лицензией, определяемой мейнтейнерами Telemt.
Любое такое изменение лицензии применяется исключительно к версиям Программного обеспечения, распространяемым с новой лицензией, и НЕ распространяется ретроактивно на ранее выпущенные версии Программного обеспечения.
Получатели Программного обеспечения приобретают права исключительно в соответствии с Лицензией, предоставленной вместе с полученной ими версией Программного обеспечения.
При распространении Программного обеспечения, включая Модифицированные версии, ОБЯЗАТЕЛЬНО сохранение уведомлений об авторском праве, текста лицензии и условий настоящей Лицензии в отношении всех частей Программного обеспечения, производных от Telemt.
Дополнительные условия или лицензии могут применяться к модификациям или дополнительному коду, добавленному распространителем, при условии, что такие условия не ограничивают и не изменяют права, предоставленные настоящей Лицензией в отношении оригинального Программного обеспечения Telemt.
Ничто в настоящем разделе не ограничивает права, предоставленные настоящей Лицензией в отношении уже выпущенных версий Программного обеспечения.
### Определения
Для целей настоящей Лицензии:
**"Программное обеспечение"** означает программное обеспечение Telemt, включая исходный код, документацию и любые сопутствующие файлы, распространяемые в соответствии с настоящей Лицензией.
**"Контрибьютор"** означает любое физическое или юридическое лицо, которое предоставляет код, исправления, документацию или иные материалы в качестве вклада в Программное обеспечение, принятые мейнтейнерами для включения в Программное обеспечение.
**"Вклад"** означает любое произведение, сознательно представленное для включения в Программное обеспечение.
**"Модифицированная версия"** означает любую версию Программного обеспечения, которая была изменена, адаптирована, расширена или иным образом модифицирована по сравнению с оригинальным Программным обеспечением.
**"Мейнтейнеры"** означает физических или юридических лиц, ответственных за официальный проект Telemt и его релизы.
### 1. Атрибуция
При распространении Программного обеспечения, как в виде исходного кода, так и в бинарной форме, ОБЯЗАТЕЛЬНО СОХРАНЕНИЕ:
- указанного выше уведомления об авторском праве;
- текста настоящей Лицензии;
- всех существующих уведомлений об атрибуции.
### 2. Уведомление о модификациях
В случае внесения изменений в Программное обеспечение вы ОБЯЗАНЫ явно указать факт модификации Программного обеспечения и включить краткое описание внесённых изменений.
Модифицированные версии НЕ ДОЛЖНЫ представляться как оригинальное Программное обеспечение Telemt.
### 3. Товарные знаки и брендинг
Настоящая Лицензия НЕ предоставляет право на использование наименования "Telemt", логотипа Telemt или любых товарных знаков и элементов брендинга Telemt.
Распространяемые или модифицированные версии Программного обеспечения НЕ МОГУТ использовать наименование Telemt таким образом, который может создавать впечатление одобрения или официального происхождения без явного разрешения мейнтейнеров Telemt.
Использование наименования "Telemt" для описания модифицированной версии Программного обеспечения допускается только при условии, что такая версия чётко обозначена как модифицированная или неофициальная.
Запрещается любое распространение, способное разумно ввести пользователей в заблуждение относительно того, что программное обеспечение является официальным релизом Telemt.
### 4. Прозрачность распространения бинарных файлов
В случае распространения скомпилированных бинарных файлов Программного обеспечения рекомендуется (ENCOURAGED) предоставлять доступ к соответствующему исходному коду и инструкциям по сборке, если это разумно возможно.
Это способствует обеспечению прозрачности и позволяет получателям проверять целостность и воспроизводимость распространяемых сборок.
### 5. Патентная лицензия и условие защитного прекращения
Каждый контрибьютор предоставляет вам бессрочную, всемирную, неисключительную, безвозмездную, без лицензионных отчислений, безотзывную патентную лицензию на:
- изготовление,
- поручение изготовления,
- использование,
- предложение к продаже,
- продажу,
- импорт,
- а также иные формы передачи Программного обеспечения.
Данная патентная лицензия распространяется исключительно на те патентные притязания, которые неизбежно нарушаются вкладом контрибьютора отдельно либо в сочетании его вклада с Программным обеспечением.
Если вы инициируете или участвуете в любом патентном судебном разбирательстве, включая встречные иски или требования, утверждая, что Программное обеспечение или любой Вклад, включённый в Программное обеспечение, нарушает патент, то **все предоставленные вам настоящей Лицензией права немедленно прекращаются** с даты подачи такого иска.
Дополнительно, если вы инициируете судебное разбирательство, утверждая, что само Программное обеспечение нарушает ваш патент или иные права интеллектуальной собственности, все права, предоставленные вам настоящей Лицензией, ПРЕКРАЩАЮТСЯ автоматически.
### 6. Вклады
Если вы прямо не указали иное, любой Вклад, сознательно представленный для включения в Программное обеспечение, лицензируется на условиях настоящей Лицензии.
Предоставляя Вклад, вы предоставляете мейнтейнерам Telemt и всем получателям Программного обеспечения права, предусмотренные настоящей Лицензией, в отношении такого Вклада.
### 7. Атрибуция при сетевом использовании
Если Программное обеспечение используется для предоставления общедоступного сетевого сервиса, оператор такого сервиса ДОЛЖЕН (SHOULD) обеспечить указание атрибуции Telemt как минимум в одном из следующих мест:
- документация сервиса;
- описание сервиса;
- раздел "О программе" или аналогичная информационная страница;
- иные материалы, доступные пользователю и разумно связанные с сервисом.
Такая атрибуция НЕ ДОЛЖНА подразумевать одобрение со стороны проекта Telemt или его мейнтейнеров.
### 8. Отказ от гарантий и оговорка о делимости
ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ "КАК ЕСТЬ", БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ, ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, В ЧАСТНОСТИ, ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ, СООТВЕТСТВИЯ ОПРЕДЕЛЁННОЙ ЦЕЛИ И ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ.
НИ ПРИ КАКИХ ОБСТОЯТЕЛЬСТВАХ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО КАКИМ-ЛИБО ТРЕБОВАНИЯМ, УБЫТКАМ ИЛИ ИНОЙ ОТВЕТСТВЕННОСТИ, ВОЗНИКАЮЩИМ В РАМКАХ ДОГОВОРА, ДЕЛИКТА ИЛИ ИНЫМ ОБРАЗОМ, ИЗ, В СВЯЗИ С ИЛИ В РЕЗУЛЬТАТЕ ИСПОЛЬЗОВАНИЯ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ ИЛИ ИНЫХ ДЕЙСТВИЙ С НИМ.
ЕСЛИ ЛЮБОЕ ПОЛОЖЕНИЕ НАСТОЯЩЕЙ ЛИЦЕНЗИИ ПРИЗНАЁТСЯ НЕДЕЙСТВИТЕЛЬНЫМ ИЛИ НЕПРИМЕНИМЫМ, ТАКОЕ ПОЛОЖЕНИЕ ПОДЛЕЖИТ ТОЛКОВАНИЮ МАКСИМАЛЬНО БЛИЗКО К ИСХОДНОМУ НАМЕРЕНИЮ СТОРОН, А ОСТАЛЬНЫЕ ПОЛОЖЕНИЯ СОХРАНЯЮТ ПОЛНУЮ СИЛУ И ДЕЙСТВИЕ.

View File

@@ -1,3 +1,19 @@
# Very quick start
### One-command installation / update on re-run
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
### 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
@@ -128,8 +144,8 @@ WorkingDirectory=/opt/telemt
ExecStart=/bin/telemt /etc/telemt/telemt.toml
Restart=on-failure
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
@@ -150,7 +166,7 @@ systemctl daemon-reload
**7.** To get the link(s), enter:
```bash
curl -s http://127.0.0.1:9091/v1/users | jq
curl -s http://127.0.0.1:9091/v1/users | jq -r '.data[] | "[\(.username)]", (.links.classic[]? | "classic: \(.)"), (.links.secure[]? | "secure: \(.)"), (.links.tls[]? | "tls: \(.)"), ""'
```
> Any number of people can use one link.

View File

@@ -1,4 +1,20 @@
# Telemt через Systemd
# Очень быстрый старт
### Установка одной командой / обновление при повторном запуске
```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
```
### Установка нужной версии
```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 вручную
## Установка
@@ -128,8 +144,8 @@ WorkingDirectory=/opt/telemt
ExecStart=/bin/telemt /etc/telemt/telemt.toml
Restart=on-failure
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
NoNewPrivileges=true
[Install]
@@ -150,7 +166,7 @@ systemctl daemon-reload
**7.** Для получения ссылки/ссылок введите
```bash
curl -s http://127.0.0.1:9091/v1/users | jq
curl -s http://127.0.0.1:9091/v1/users | jq -r '.data[] | "[\(.username)]", (.links.classic[]? | "classic: \(.)"), (.links.secure[]? | "secure: \(.)"), (.links.tls[]? | "tls: \(.)"), ""'
```
> Одной ссылкой может пользоваться сколько угодно человек.

View File

@@ -0,0 +1,273 @@
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
## Concept
- **Server A** (_e.g., RU_):\
Entry point, accepts Telegram proxy user traffic via **Xray** (port `443\tcp`)\
and sends it through the tunnel to Server **B**.\
Public port for Telegram clients — `443\tcp`
- **Server B** (_e.g., NL_):\
Exit point, runs the **Xray server** (to terminate the tunnel entry point) and **telemt**.\
The server must have unrestricted access to Telegram Data Centers.\
Public port for VLESS/REALITY (incoming) — `443\tcp`\
Internal telemt port (where decrypted Xray traffic ends up) — `8443\tcp`
The tunnel works over the `VLESS-XTLS-Reality` (or `VLESS/xhttp/reality`) protocol. The original client IP address is preserved thanks to the PROXYv2 protocol, which Xray on Server A dynamically injects via a local loopback before wrapping the traffic into Reality, transparently delivering the real IPs to telemt on Server B.
---
## Step 1. Setup Xray Tunnel (A <-> B)
You must install **Xray-core** (version 1.8.4 or newer recommended) on both servers.
Official installation script (run on both servers):
```bash
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
```
### Key and Parameter Generation (Run Once)
For configuration, you need a unique UUID and Xray Reality keys. Run on any server with Xray installed:
1. **Client UUID:**
```bash
xray uuid
# Save the output (e.g.: 12345678-abcd-1234-abcd-1234567890ab) — this is <XRAY_UUID>
```
2. **X25519 Keypair (Private & Public) for Reality:**
```bash
xray x25519
# Save the Private key (<SERVER_B_PRIVATE_KEY>) and Public key (<SERVER_B_PUBLIC_KEY>)
```
3. **Short ID (Reality identifier):**
```bash
openssl rand -hex 16
# Save the output (e.g.: 0123456789abcdef0123456789abcdef) — this is <SHORT_ID>
```
4. **Random Path (for xhttp):**
```bash
openssl rand -hex 8
# Save the output (e.g., abc123def456) to replace <YOUR_RANDOM_PATH> in configs
```
---
### Configuration for Server B (_EU_):
Create or edit the file `/usr/local/etc/xray/config.json`.
This Xray instance will listen on the public `443` port and proxy valid Reality traffic, while routing "disguised" traffic (e.g., direct web browser scans) to `yahoo.com`.
```bash
nano /usr/local/etc/xray/config.json
```
File content:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "vless-in",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "<XRAY_UUID>"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"dest": "yahoo.com:443",
"serverNames": [
"yahoo.com"
],
"privateKey": "<SERVER_B_PRIVATE_KEY>",
"shortIds": [
"<SHORT_ID>"
]
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>",
"mode": "auto"
}
}
}
],
"outbounds": [
{
"tag": "tunnel-to-telemt",
"protocol": "freedom",
"settings": {
"destination": "127.0.0.1:8443"
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": [
"vless-in"
],
"outboundTag": "tunnel-to-telemt"
}
]
}
}
```
Open the firewall port (if enabled):
```bash
sudo ufw allow 443/tcp
```
Restart and setup Xray to run at boot:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
### Configuration for Server A (_RU_):
Similarly, edit `/usr/local/etc/xray/config.json`.
Here Xray acts as the public entry point: it listens on `443\tcp`, uses a local loopback (via internal port `10444`) to prepend the `PROXYv2` header, and encapsulates the payload via Reality to Server B, instructing Server B to deliver it to its *local* `127.0.0.1:8443` port (where telemt will listen).
```bash
nano /usr/local/etc/xray/config.json
```
File content:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "public-in",
"port": 443,
"listen": "0.0.0.0",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 10444,
"network": "tcp"
}
},
{
"tag": "tunnel-in",
"port": 10444,
"listen": "127.0.0.1",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 8443,
"network": "tcp"
}
}
],
"outbounds": [
{
"tag": "local-injector",
"protocol": "freedom",
"settings": {
"proxyProtocol": 2
}
},
{
"tag": "vless-out",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "<PUBLIC_IP_SERVER_B>",
"port": 443,
"users": [
{
"id": "<XRAY_UUID>",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"serverName": "yahoo.com",
"publicKey": "<SERVER_B_PUBLIC_KEY>",
"shortId": "<SHORT_ID>",
"spiderX": "/",
"fingerprint": "chrome"
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>"
}
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": ["public-in"],
"outboundTag": "local-injector"
},
{
"type": "field",
"inboundTag": ["tunnel-in"],
"outboundTag": "vless-out"
}
]
}
}
```
*Replace `<PUBLIC_IP_SERVER_B>` with the public IP address of Server B.*
Open the firewall port for clients (if enabled):
```bash
sudo ufw allow 443/tcp
```
Restart and setup Xray to run at boot:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
## Step 2. Install telemt on Server B (_EU_)
telemt installation is heavily covered in the [Quick Start Guide](../QUICK_START_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.

View File

@@ -0,0 +1,272 @@
<img src="https://gist.githubusercontent.com/avbor/1f8a128e628f47249aae6e058a57610b/raw/19013276c035e91058e0a9799ab145f8e70e3ff5/scheme.svg">
## Концепция
- **Сервер A** (_РФ_):\
Точка входа, принимает трафик пользователей Telegram-прокси напрямую через **Xray** (порт `443\tcp`)\
и отправляет его в туннель на Сервер **B**.\
Порт для клиентов Telegram — `443\tcp`
- **Сервер B** (_условно Нидерланды_):\
Точка выхода, на нем работает **Xray-сервер** (принимает подключения точки входа) и **telemt**.\
На сервере должен быть неограниченный доступ до серверов Telegram.\
Порт для VLESS/REALITY (вход) — `443\tcp`\
Внутренний порт telemt (куда пробрасывается трафик) — `8443\tcp`
Туннель работает по протоколу VLESS-XTLS-Reality (или VLESS/xhttp/reality). Оригинальный IP-адрес клиента сохраняется благодаря протоколу PROXYv2, который Xray на Сервере А добавляет через локальный loopback перед упаковкой в туннель, благодаря чему прозрачно доходит до telemt.
---
## Шаг 1. Настройка туннеля Xray (A <-> B)
На обоих серверах необходимо установить **Xray-core** (рекомендуется версия 1.8.4 или новее).
Официальный скрипт установки (выполнить на обоих серверах):
```bash
bash -c "$(curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh)" @ install
```
### Генерация ключей и параметров (выполнить один раз)
Для конфигурации потребуются уникальные ID и ключи Xray Reality. Выполните на любом сервере с установленным Xray:
1. **UUID клиента:**
```bash
xray uuid
# Сохраните вывод (например: 12345678-abcd-1234-abcd-1234567890ab) — это <XRAY_UUID>
```
2. **Пара ключей X25519 (Private & Public) для Reality:**
```bash
xray x25519
# Сохраните Private key (<SERVER_B_PRIVATE_KEY>) и Public key (<SERVER_B_PUBLIC_KEY>)
```
3. **Short ID (идентификатор Reality):**
```bash
openssl rand -hex 16
# Сохраните вывод (например: 0123456789abcdef0123456789abcdef) — это <SHORT_ID>
```
4. **Random Path (путь для xhttp):**
```bash
openssl rand -hex 8
# Сохраните вывод (например, abc123def456), чтобы заменить <YOUR_RANDOM_PATH> в конфигах
```
---
### Конфигурация Сервера B (_Нидерланды_):
Создаем или редактируем файл `/usr/local/etc/xray/config.json`.
Этот Xray-сервер будет слушать порт `443` и прозрачно пропускать валидный Reality трафик дальше, а "замаскированный" трафик (например, если кто-то стучится в лоб веб-браузером) пойдет на `yahoo.com`.
```bash
nano /usr/local/etc/xray/config.json
```
Содержимое файла:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "vless-in",
"port": 443,
"protocol": "vless",
"settings": {
"clients": [
{
"id": "<XRAY_UUID>"
}
],
"decryption": "none"
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"dest": "yahoo.com:443",
"serverNames": [
"yahoo.com"
],
"privateKey": "<SERVER_B_PRIVATE_KEY>",
"shortIds": [
"<SHORT_ID>"
]
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>",
"mode": "auto"
}
}
}
],
"outbounds": [
{
"tag": "tunnel-to-telemt",
"protocol": "freedom",
"settings": {
"destination": "127.0.0.1:8443"
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": [
"vless-in"
],
"outboundTag": "tunnel-to-telemt"
}
]
}
}
```
Открываем порт на фаерволе (если включен):
```bash
sudo ufw allow 443/tcp
```
Перезапускаем Xray:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
### Конфигурация Сервера A (_РФ_):
Аналогично, редактируем `/usr/local/etc/xray/config.json`.
Здесь Xray выступает публичной точкой: он принимает трафик на внешний порт `443\tcp`, пропускает через локальный loopback (порт `10444`) для добавления PROXYv2-заголовка, и упаковывает в Reality до Сервера B, прося тот доставить данные на *свой локальный* порт `127.0.0.1:8443` (именно там будет слушать telemt).
```bash
nano /usr/local/etc/xray/config.json
```
Содержимое файла:
```json
{
"log": {
"loglevel": "error",
"access": "none"
},
"inbounds": [
{
"tag": "public-in",
"port": 443,
"listen": "0.0.0.0",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 10444,
"network": "tcp"
}
},
{
"tag": "tunnel-in",
"port": 10444,
"listen": "127.0.0.1",
"protocol": "dokodemo-door",
"settings": {
"address": "127.0.0.1",
"port": 8443,
"network": "tcp"
}
}
],
"outbounds": [
{
"tag": "local-injector",
"protocol": "freedom",
"settings": {
"proxyProtocol": 2
}
},
{
"tag": "vless-out",
"protocol": "vless",
"settings": {
"vnext": [
{
"address": "<PUBLIC_IP_SERVER_B>",
"port": 443,
"users": [
{
"id": "<XRAY_UUID>",
"encryption": "none"
}
]
}
]
},
"streamSettings": {
"network": "xhttp",
"security": "reality",
"realitySettings": {
"serverName": "yahoo.com",
"publicKey": "<SERVER_B_PUBLIC_KEY>",
"shortId": "<SHORT_ID>",
"spiderX": "/",
"fingerprint": "chrome"
},
"xhttpSettings": {
"path": "/<YOUR_RANDOM_PATH>"
}
}
}
],
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"inboundTag": ["public-in"],
"outboundTag": "local-injector"
},
{
"type": "field",
"inboundTag": ["tunnel-in"],
"outboundTag": "vless-out"
}
]
}
}
```
*Замените `<PUBLIC_IP_SERVER_B>` на внешний IP-адрес Сервера B.*
Открываем порт на фаерволе для клиентов:
```bash
sudo ufw allow 443/tcp
```
Перезапускаем Xray:
```bash
sudo systemctl restart xray
sudo systemctl enable xray
```
---
## Шаг 2. Установка и настройка telemt на Сервере B (_Нидерланды_)
Установка telemt описана [в основной инструкции](../QUICK_START_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`, и клиенты смогут подключаться по выданным ссылкам.

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/assets/telemt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

View File

@@ -8,38 +8,257 @@ CONFIG_DIR="${CONFIG_DIR:-/etc/telemt}"
CONFIG_FILE="${CONFIG_FILE:-${CONFIG_DIR}/telemt.toml}"
WORK_DIR="${WORK_DIR:-/opt/telemt}"
TLS_DOMAIN="${TLS_DOMAIN:-petrovich.ru}"
SERVER_PORT="${SERVER_PORT:-443}"
USER_SECRET=""
AD_TAG=""
SERVICE_NAME="telemt"
TEMP_DIR=""
SUDO=""
CONFIG_PARENT_DIR=""
SERVICE_START_FAILED=0
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"
shift 2 ;;
TLS_DOMAIN="$2"; DOMAIN_PROVIDED=1; shift 2 ;;
-p|--port)
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
printf '[ERROR] %s %s\n' "$1" "$L_ERR_PORT_REQ" >&2; exit 1
fi
case "$2" in
*[!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] %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 %s\n' "$1" "$L_ERR_SECRET_REQ" >&2; exit 1
fi
case "$2" in
*[!0-9a-fA-F]*) printf '[ERROR] %s\n' "$L_ERR_SECRET_HEX" >&2; exit 1 ;;
esac
if [ "${#2}" -ne 32 ]; then
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 %s\n' "$1" "$L_ERR_ADTAG_REQ" >&2; exit 1
fi
AD_TAG="$2"; AD_TAG_PROVIDED=1; shift 2 ;;
uninstall|--uninstall)
if [ "$ACTION" != "purge" ]; then ACTION="uninstall"; fi
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'
@@ -59,12 +278,33 @@ cleanup() {
trap cleanup EXIT INT TERM
show_help() {
say "Usage: $0 [ <version> | install | uninstall | purge ] [ -d <domain> ] [ --help ]"
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 (keeps config and user)"
say " purge Remove everything including configuration, data, and user"
say " -d, --domain Set TLS domain (default: petrovich.ru)"
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
}
@@ -81,13 +321,13 @@ get_realpath() {
path_in="$1"
case "$path_in" in /*) ;; *) path_in="$(pwd)/$path_in" ;; esac
if command -v realpath >/dev/null 2>&1; then
if command -v realpath >/dev/null 2>&1; then
if realpath_out="$(realpath -m "$path_in" 2>/dev/null)"; then
printf '%s\n' "$realpath_out"
return
fi
fi
if command -v readlink >/dev/null 2>&1; then
resolved_path="$(readlink -f "$path_in" 2>/dev/null || true)"
if [ -n "$resolved_path" ]; then
@@ -120,18 +360,22 @@ get_svc_mgr() {
else echo "none"; fi
}
is_config_exists() {
if [ -n "$SUDO" ]; then
$SUDO sh -c '[ -f "$1" ]' _ "$CONFIG_FILE"
else
[ -f "$CONFIG_FILE" ]
fi
}
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. Only alphanumeric, _, ., -, and / allowed." ;;
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")"
@@ -145,58 +389,71 @@ verify_common() {
if [ "$(id -u)" -eq 0 ]; then
SUDO=""
else
command -v sudo >/dev/null 2>&1 || die "This script requires root or sudo. Neither found."
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. Aborting to prevent hang."
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 path in "$CONFIG_DIR" "$CONFIG_PARENT_DIR" "$WORK_DIR"; do
check_path="$(get_realpath "$path")"
case "$check_path" in
/|/bin|/sbin|/usr|/usr/bin|/usr/sbin|/usr/local|/usr/local/bin|/usr/local/sbin|/usr/local/etc|/usr/local/share|/etc|/var|/var/lib|/var/log|/var/run|/home|/root|/tmp|/lib|/lib64|/opt|/run|/boot|/dev|/sys|/proc)
die "Safety check failed: '$path' (resolved to '$check_path') is a critical system directory." ;;
esac
done
check_install_dir="$(get_realpath "$INSTALL_DIR")"
case "$check_install_dir" in
/|/etc|/var|/home|/root|/tmp|/usr|/usr/local|/opt|/boot|/dev|/sys|/proc|/run)
die "Safety check failed: INSTALL_DIR '$INSTALL_DIR' is a critical system directory." ;;
esac
for cmd in id uname grep find rm chown chmod mv mktemp mkdir tr dd sed ps head sleep cat tar gzip rmdir; do
command -v "$cmd" >/dev/null 2>&1 || die "Required command not found: $cmd"
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 "$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; then
if command -v apk >/dev/null 2>&1; then
$SUDO apk add --no-cache libcap-utils >/dev/null 2>&1 || $SUDO apk add --no-cache libcap >/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 apt-get update -q >/dev/null 2>&1 || true
$SUDO apt-get install -y -q libcap2-bin >/dev/null 2>&1 || true
$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 >/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
}
check_port_availability() {
port_info=""
if command -v ss >/dev/null 2>&1; then
port_info=$($SUDO ss -tulnp 2>/dev/null | grep -E ":${SERVER_PORT}([[:space:]]|$)" || true)
elif command -v netstat >/dev/null 2>&1; then
port_info=$($SUDO netstat -tulnp 2>/dev/null | grep -E ":${SERVER_PORT}([[:space:]]|$)" || true)
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] $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 " -> $L_INFO_PORT_IGNORE"
else
say "[ERROR] $L_ERR_PORT_IN_USE $SERVER_PORT:"
printf ' %s\n' "$port_info"
die "$L_ERR_PORT_FREE"
fi
fi
}
detect_arch() {
sys_arch="$(uname -m)"
case "$sys_arch" in
@@ -208,7 +465,7 @@ detect_arch() {
fi
;;
aarch64|arm64) echo "aarch64" ;;
*) die "Unsupported architecture: $sys_arch" ;;
*) die "$L_ERR_UNSUP_ARCH $sys_arch" ;;
esac
}
@@ -232,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
@@ -244,16 +501,16 @@ 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 root:telemt "$CONFIG_DIR" && $SUDO chmod 750 "$CONFIG_DIR"
$SUDO chown telemt:telemt "$CONFIG_DIR" && $SUDO chmod 750 "$CONFIG_DIR"
if [ "$CONFIG_PARENT_DIR" != "$CONFIG_DIR" ] && [ "$CONFIG_PARENT_DIR" != "." ] && [ "$CONFIG_PARENT_DIR" != "/" ]; then
$SUDO chown root:telemt "$CONFIG_PARENT_DIR" && $SUDO chmod 750 "$CONFIG_PARENT_DIR"
fi
@@ -271,21 +528,23 @@ 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 rm -f "$bin_dst" 2>/dev/null || true
$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=+ep "$bin_dst" 2>/dev/null || true
$SUDO setcap cap_net_bind_service,cap_net_admin=+ep "$bin_dst" 2>/dev/null || true
fi
}
@@ -301,11 +560,20 @@ generate_secret() {
}
generate_config_content() {
conf_secret="$1"
conf_tag="$2"
escaped_tls_domain="$(printf '%s\n' "$TLS_DOMAIN" | tr -d '[:cntrl:]' | sed 's/\\/\\\\/g; s/"/\\"/g')"
cat <<EOF
[general]
use_middle_proxy = false
use_middle_proxy = true
EOF
if [ -n "$conf_tag" ]; then
echo "ad_tag = \"${conf_tag}\""
fi
cat <<EOF
[general.modes]
classic = false
@@ -313,7 +581,7 @@ secure = false
tls = true
[server]
port = 443
port = ${SERVER_PORT}
[server.api]
enabled = true
@@ -324,28 +592,65 @@ whitelist = ["127.0.0.1/32"]
tls_domain = "${escaped_tls_domain}"
[access.users]
hello = "$1"
hello = "${conf_secret}"
EOF
}
install_config() {
if [ -n "$SUDO" ]; then
if $SUDO sh -c '[ -f "$1" ]' _ "$CONFIG_FILE"; then
say " -> Config already exists at $CONFIG_FILE. Skipping creation."
return 0
fi
elif [ -f "$CONFIG_FILE" ]; then
say " -> Config already exists at $CONFIG_FILE. Skipping creation."
if is_config_exists; then
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')"
awk -v port="$SERVER_PORT" -v secret="$USER_SECRET" -v domain="$escaped_domain" -v ad_tag="$AD_TAG" \
-v flag_p="$PORT_PROVIDED" -v flag_s="$SECRET_PROVIDED" -v flag_d="$DOMAIN_PROVIDED" -v flag_a="$AD_TAG_PROVIDED" '
BEGIN { ad_tag_handled = 0 }
flag_p == "1" && /^[ \t]*port[ \t]*=/ { print "port = " port; next }
flag_s == "1" && /^[ \t]*hello[ \t]*=/ { print "hello = \"" secret "\""; next }
flag_d == "1" && /^[ \t]*tls_domain[ \t]*=/ { print "tls_domain = \"" domain "\""; next }
flag_a == "1" && /^[ \t]*ad_tag[ \t]*=/ {
if (!ad_tag_handled) {
print "ad_tag = \"" ad_tag "\"";
ad_tag_handled = 1;
}
next
}
flag_a == "1" && /^\[general\]/ {
print;
if (!ad_tag_handled) {
print "ad_tag = \"" ad_tag "\"";
ad_tag_handled = 1;
}
next
}
{ print }
' "$tmp_conf" > "${tmp_conf}.new" && mv "${tmp_conf}.new" "$tmp_conf"
[ "$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"
return 0
fi
toml_secret="$(generate_secret)" || die "Failed to generate secret."
if [ -z "$USER_SECRET" ]; then
USER_SECRET="$(generate_secret)" || die "$L_ERR_GEN_SEC"
fi
generate_config_content "$toml_secret" | 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 " -> Generated secret for default user 'hello': $toml_secret"
say " -> $L_INFO_CONF_OK"
say " -> $L_INFO_CONF_SEC $USER_SECRET"
}
generate_systemd_content() {
@@ -362,9 +667,10 @@ Group=telemt
WorkingDirectory=$WORK_DIR
ExecStart="${INSTALL_DIR}/${BIN_NAME}" "${CONFIG_FILE}"
Restart=on-failure
RestartSec=5
LimitNOFILE=65536
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
[Install]
WantedBy=multi-user.target
@@ -395,9 +701,9 @@ install_service() {
$SUDO systemctl daemon-reload || true
$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
@@ -405,17 +711,17 @@ install_service() {
$SUDO chown root:root "/etc/init.d/${SERVICE_NAME}" && $SUDO chmod 0755 "/etc/init.d/${SERVICE_NAME}"
$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"
else
say " -> Service manager not found. Start manually: su -s /bin/sh telemt -c '$cmd'"
if [ -n "$SUDO" ]; then
say " -> $L_INFO_MANUAL_START sudo -u telemt $cmd"
else
say " -> $L_INFO_MANUAL_START su -s /bin/sh telemt -c '$cmd'"
fi
fi
}
@@ -429,9 +735,10 @@ kill_user_procs() {
if command -v pgrep >/dev/null 2>&1; then
pids="$(pgrep -u telemt 2>/dev/null || true)"
else
pids="$(ps -u telemt -o pid= 2>/dev/null || true)"
pids="$(ps -ef 2>/dev/null | awk '$1=="telemt"{print $2}' || true)"
[ -z "$pids" ] && pids="$(ps 2>/dev/null | awk '$2=="telemt"{print $1}' || true)"
fi
if [ -n "$pids" ]; then
for pid in $pids; do
case "$pid" in ''|*[!0-9]*) continue ;; *) $SUDO kill "$pid" 2>/dev/null || true ;; esac
@@ -445,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
@@ -461,27 +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"
if [ "$CONFIG_PARENT_DIR" != "$CONFIG_DIR" ] && [ "$CONFIG_PARENT_DIR" != "." ] && [ "$CONFIG_PARENT_DIR" != "/" ]; then
$SUDO rmdir "$CONFIG_PARENT_DIR" 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
$SUDO userdel telemt 2>/dev/null || $SUDO deluser telemt 2>/dev/null || true
$SUDO groupdel telemt 2>/dev/null || $SUDO delgroup telemt 2>/dev/null || true
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
}
@@ -490,95 +800,126 @@ 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"
verify_common; verify_install_deps
say "$L_I_STAGE_1"
verify_common
verify_install_deps
if [ "$TARGET_VERSION" != "latest" ]; 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" ] && [ "$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
ARCH="$(detect_arch)"; LIBC="$(detect_libc)"
FILE_NAME="${BIN_NAME}-${ARCH}-linux-${LIBC}.tar.gz"
if [ "$TARGET_VERSION" = "latest" ]; then
DL_URL="https://github.com/${REPO}/releases/latest/download/${FILE_NAME}"
else
else
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
DL_URL="https://github.com/${REPO}/releases/latest/download/${FILE_NAME}"
else
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 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
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>"
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
printf 'To get your user connection links (for Telegram), run:\n'
if command -v jq >/dev/null 2>&1; then
printf ' curl -s http://127.0.0.1:9091/v1/users | jq -r '\''.data[] | "User: \\(.username)\\n\\(.links.tls[0] // empty)\\n"'\''\n'
else
printf ' curl -s http://127.0.0.1:9091/v1/users\n'
printf ' (Tip: Install '\''jq'\'' for a much cleaner output)\n'
fi
printf '\n====================================================================\n'
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
CLIENT_SECRET="ee${USER_SECRET}${HEX_DOMAIN}"
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'
;;
esac

View File

@@ -540,6 +540,10 @@ fn overlay_hot_fields(old: &ProxyConfig, new: &ProxyConfig) -> ProxyConfig {
cfg.access.user_max_unique_ips_mode = new.access.user_max_unique_ips_mode;
cfg.access.user_max_unique_ips_window_secs = new.access.user_max_unique_ips_window_secs;
if cfg.rebuild_runtime_user_auth().is_err() {
cfg.runtime_user_auth = None;
}
cfg
}

View File

@@ -4,6 +4,7 @@ use std::collections::{BTreeSet, HashMap, HashSet};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use rand::RngExt;
use serde::{Deserialize, Serialize};
@@ -15,6 +16,13 @@ use crate::error::{ProxyError, Result};
use super::defaults::*;
use super::types::*;
const ACCESS_SECRET_BYTES: usize = 16;
const MAX_ME_WRITER_CMD_CHANNEL_CAPACITY: usize = 16_384;
const MAX_ME_ROUTE_CHANNEL_CAPACITY: usize = 8_192;
const MAX_ME_C2ME_CHANNEL_CAPACITY: usize = 8_192;
const MIN_MAX_CLIENT_FRAME_BYTES: usize = 4 * 1024;
const MAX_MAX_CLIENT_FRAME_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone)]
pub(crate) struct LoadedConfig {
pub(crate) config: ProxyConfig,
@@ -22,6 +30,111 @@ pub(crate) struct LoadedConfig {
pub(crate) rendered_hash: u64,
}
/// Precomputed, immutable user authentication data used by handshake hot paths.
#[derive(Debug, Clone, Default)]
pub(crate) struct UserAuthSnapshot {
entries: Vec<UserAuthEntry>,
by_name: HashMap<String, u32>,
sni_index: HashMap<u64, Vec<u32>>,
sni_initial_index: HashMap<u8, Vec<u32>>,
}
#[derive(Debug, Clone)]
pub(crate) struct UserAuthEntry {
pub(crate) user: String,
pub(crate) secret: [u8; ACCESS_SECRET_BYTES],
}
impl UserAuthSnapshot {
fn from_users(users: &HashMap<String, String>) -> Result<Self> {
let mut entries = Vec::with_capacity(users.len());
let mut by_name = HashMap::with_capacity(users.len());
let mut sni_index = HashMap::with_capacity(users.len());
let mut sni_initial_index = HashMap::with_capacity(users.len());
for (user, secret_hex) in users {
let decoded = hex::decode(secret_hex).map_err(|_| ProxyError::InvalidSecret {
user: user.clone(),
reason: "Must be 32 hex characters".to_string(),
})?;
if decoded.len() != ACCESS_SECRET_BYTES {
return Err(ProxyError::InvalidSecret {
user: user.clone(),
reason: "Must be 32 hex characters".to_string(),
});
}
let user_id = u32::try_from(entries.len()).map_err(|_| {
ProxyError::Config("Too many users for runtime auth snapshot".to_string())
})?;
let mut secret = [0u8; ACCESS_SECRET_BYTES];
secret.copy_from_slice(&decoded);
entries.push(UserAuthEntry {
user: user.clone(),
secret,
});
by_name.insert(user.clone(), user_id);
sni_index
.entry(Self::sni_lookup_hash(user))
.or_insert_with(Vec::new)
.push(user_id);
if let Some(initial) = user
.as_bytes()
.first()
.map(|byte| byte.to_ascii_lowercase())
{
sni_initial_index
.entry(initial)
.or_insert_with(Vec::new)
.push(user_id);
}
}
Ok(Self {
entries,
by_name,
sni_index,
sni_initial_index,
})
}
pub(crate) fn entries(&self) -> &[UserAuthEntry] {
&self.entries
}
pub(crate) fn user_id_by_name(&self, user: &str) -> Option<u32> {
self.by_name.get(user).copied()
}
pub(crate) fn entry_by_id(&self, user_id: u32) -> Option<&UserAuthEntry> {
let idx = usize::try_from(user_id).ok()?;
self.entries.get(idx)
}
pub(crate) fn sni_candidates(&self, sni: &str) -> Option<&[u32]> {
self.sni_index
.get(&Self::sni_lookup_hash(sni))
.map(Vec::as_slice)
}
pub(crate) fn sni_initial_candidates(&self, sni: &str) -> Option<&[u32]> {
let initial = sni
.as_bytes()
.first()
.map(|byte| byte.to_ascii_lowercase())?;
self.sni_initial_index.get(&initial).map(Vec::as_slice)
}
fn sni_lookup_hash(value: &str) -> u64 {
let mut hasher = DefaultHasher::new();
for byte in value.bytes() {
hasher.write_u8(byte.to_ascii_lowercase());
}
hasher.finish()
}
}
fn normalize_config_path(path: &Path) -> PathBuf {
path.canonicalize().unwrap_or_else(|_| {
if path.is_absolute() {
@@ -196,6 +309,10 @@ pub struct ProxyConfig {
/// If not set, defaults to 2 (matching Telegram's official `default 2;` in proxy-multi.conf).
#[serde(default)]
pub default_dc: Option<u8>,
/// Precomputed authentication snapshot for handshake hot paths.
#[serde(skip)]
pub(crate) runtime_user_auth: Option<Arc<UserAuthSnapshot>>,
}
impl ProxyConfig {
@@ -514,18 +631,41 @@ impl ProxyConfig {
"general.me_writer_cmd_channel_capacity must be > 0".to_string(),
));
}
if config.general.me_writer_cmd_channel_capacity > MAX_ME_WRITER_CMD_CHANNEL_CAPACITY {
return Err(ProxyError::Config(format!(
"general.me_writer_cmd_channel_capacity must be within [1, {MAX_ME_WRITER_CMD_CHANNEL_CAPACITY}]"
)));
}
if config.general.me_route_channel_capacity == 0 {
return Err(ProxyError::Config(
"general.me_route_channel_capacity must be > 0".to_string(),
));
}
if config.general.me_route_channel_capacity > MAX_ME_ROUTE_CHANNEL_CAPACITY {
return Err(ProxyError::Config(format!(
"general.me_route_channel_capacity must be within [1, {MAX_ME_ROUTE_CHANNEL_CAPACITY}]"
)));
}
if config.general.me_c2me_channel_capacity == 0 {
return Err(ProxyError::Config(
"general.me_c2me_channel_capacity must be > 0".to_string(),
));
}
if config.general.me_c2me_channel_capacity > MAX_ME_C2ME_CHANNEL_CAPACITY {
return Err(ProxyError::Config(format!(
"general.me_c2me_channel_capacity must be within [1, {MAX_ME_C2ME_CHANNEL_CAPACITY}]"
)));
}
if !(MIN_MAX_CLIENT_FRAME_BYTES..=MAX_MAX_CLIENT_FRAME_BYTES)
.contains(&config.general.max_client_frame)
{
return Err(ProxyError::Config(format!(
"general.max_client_frame must be within [{MIN_MAX_CLIENT_FRAME_BYTES}, {MAX_MAX_CLIENT_FRAME_BYTES}]"
)));
}
if config.general.me_c2me_send_timeout_ms > 60_000 {
return Err(ProxyError::Config(
@@ -1164,6 +1304,7 @@ impl ProxyConfig {
.or_insert_with(|| vec!["91.105.192.100:443".to_string()]);
validate_upstreams(&config)?;
config.rebuild_runtime_user_auth()?;
Ok(LoadedConfig {
config,
@@ -1172,6 +1313,16 @@ impl ProxyConfig {
})
}
pub(crate) fn rebuild_runtime_user_auth(&mut self) -> Result<()> {
let snapshot = UserAuthSnapshot::from_users(&self.access.users)?;
self.runtime_user_auth = Some(Arc::new(snapshot));
Ok(())
}
pub(crate) fn runtime_user_auth(&self) -> Option<&UserAuthSnapshot> {
self.runtime_user_auth.as_deref()
}
pub fn validate(&self) -> Result<()> {
if self.access.users.is_empty() {
return Err(ProxyError::Config("No users configured".to_string()));
@@ -1223,6 +1374,10 @@ mod load_mask_shape_security_tests;
#[path = "tests/load_mask_classifier_prefetch_timeout_security_tests.rs"]
mod load_mask_classifier_prefetch_timeout_security_tests;
#[cfg(test)]
#[path = "tests/load_memory_envelope_tests.rs"]
mod load_memory_envelope_tests;
#[cfg(test)]
mod tests {
use super::*;
@@ -1635,6 +1790,22 @@ mod tests {
cfg_mask.censorship.unknown_sni_action,
UnknownSniAction::Mask
);
let cfg_accept: ProxyConfig = toml::from_str(
r#"
[server]
[general]
[network]
[access]
[censorship]
unknown_sni_action = "accept"
"#,
)
.unwrap();
assert_eq!(
cfg_accept.censorship.unknown_sni_action,
UnknownSniAction::Accept
);
}
#[test]

View File

@@ -0,0 +1,117 @@
use super::*;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn write_temp_config(contents: &str) -> PathBuf {
let nonce = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time must be after unix epoch")
.as_nanos();
let path = std::env::temp_dir().join(format!("telemt-load-memory-envelope-{nonce}.toml"));
fs::write(&path, contents).expect("temp config write must succeed");
path
}
fn remove_temp_config(path: &PathBuf) {
let _ = fs::remove_file(path);
}
#[test]
fn load_rejects_writer_cmd_capacity_above_upper_bound() {
let path = write_temp_config(
r#"
[general]
me_writer_cmd_channel_capacity = 16385
"#,
);
let err =
ProxyConfig::load(&path).expect_err("writer command capacity above hard cap must fail");
let msg = err.to_string();
assert!(
msg.contains("general.me_writer_cmd_channel_capacity must be within [1, 16384]"),
"error must explain writer command capacity hard cap, got: {msg}"
);
remove_temp_config(&path);
}
#[test]
fn load_rejects_route_channel_capacity_above_upper_bound() {
let path = write_temp_config(
r#"
[general]
me_route_channel_capacity = 8193
"#,
);
let err =
ProxyConfig::load(&path).expect_err("route channel capacity above hard cap must fail");
let msg = err.to_string();
assert!(
msg.contains("general.me_route_channel_capacity must be within [1, 8192]"),
"error must explain route channel hard cap, got: {msg}"
);
remove_temp_config(&path);
}
#[test]
fn load_rejects_c2me_channel_capacity_above_upper_bound() {
let path = write_temp_config(
r#"
[general]
me_c2me_channel_capacity = 8193
"#,
);
let err = ProxyConfig::load(&path).expect_err("c2me channel capacity above hard cap must fail");
let msg = err.to_string();
assert!(
msg.contains("general.me_c2me_channel_capacity must be within [1, 8192]"),
"error must explain c2me channel hard cap, got: {msg}"
);
remove_temp_config(&path);
}
#[test]
fn load_rejects_max_client_frame_above_upper_bound() {
let path = write_temp_config(
r#"
[general]
max_client_frame = 16777217
"#,
);
let err = ProxyConfig::load(&path).expect_err("max_client_frame above hard cap must fail");
let msg = err.to_string();
assert!(
msg.contains("general.max_client_frame must be within [4096, 16777216]"),
"error must explain max_client_frame hard cap, got: {msg}"
);
remove_temp_config(&path);
}
#[test]
fn load_accepts_memory_limits_at_hard_upper_bounds() {
let path = write_temp_config(
r#"
[general]
me_writer_cmd_channel_capacity = 16384
me_route_channel_capacity = 8192
me_c2me_channel_capacity = 8192
max_client_frame = 16777216
"#,
);
let cfg = ProxyConfig::load(&path).expect("hard upper bound values must be accepted");
assert_eq!(cfg.general.me_writer_cmd_channel_capacity, 16384);
assert_eq!(cfg.general.me_route_channel_capacity, 8192);
assert_eq!(cfg.general.me_c2me_channel_capacity, 8192);
assert_eq!(cfg.general.max_client_frame, 16 * 1024 * 1024);
remove_temp_config(&path);
}

View File

@@ -1502,6 +1502,7 @@ pub enum UnknownSniAction {
#[default]
Drop,
Mask,
Accept,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]

View File

@@ -339,31 +339,35 @@ fn is_process_running(pid: i32) -> bool {
/// Drops privileges to the specified user and group.
///
/// This should be called after binding privileged ports but before
/// entering the main event loop.
pub fn drop_privileges(user: Option<&str>, group: Option<&str>) -> Result<(), DaemonError> {
// Look up group first (need to do this while still root)
/// This should be called after binding privileged ports but before entering
/// the main event loop.
pub fn drop_privileges(
user: Option<&str>,
group: Option<&str>,
pid_file: Option<&PidFile>,
) -> Result<(), DaemonError> {
let target_gid = if let Some(group_name) = group {
Some(lookup_group(group_name)?)
} else if let Some(user_name) = user {
// If no group specified but user is, use user's primary group
Some(lookup_user_primary_gid(user_name)?)
} else {
None
};
// Look up user
let target_uid = if let Some(user_name) = user {
Some(lookup_user(user_name)?)
} else {
None
};
// Drop privileges: set GID first, then UID
// (Setting UID first would prevent us from setting GID)
if (target_uid.is_some() || target_gid.is_some())
&& let Some(file) = pid_file.and_then(|pid| pid.file.as_ref())
{
unistd::fchown(file, target_uid, target_gid).map_err(DaemonError::PrivilegeDrop)?;
}
if let Some(gid) = target_gid {
unistd::setgid(gid).map_err(DaemonError::PrivilegeDrop)?;
// Also set supplementary groups to just this one
unistd::setgroups(&[gid]).map_err(DaemonError::PrivilegeDrop)?;
info!(gid = gid.as_raw(), "Dropped group privileges");
}
@@ -371,6 +375,38 @@ pub fn drop_privileges(user: Option<&str>, group: Option<&str>) -> Result<(), Da
if let Some(uid) = target_uid {
unistd::setuid(uid).map_err(DaemonError::PrivilegeDrop)?;
info!(uid = uid.as_raw(), "Dropped user privileges");
if uid.as_raw() != 0
&& let Some(pid) = pid_file
{
let parent = pid.path.parent().unwrap_or(Path::new("."));
let probe_path = parent.join(format!(
".telemt_pid_probe_{}_{}",
std::process::id(),
getpid().as_raw()
));
OpenOptions::new()
.write(true)
.create_new(true)
.mode(0o600)
.open(&probe_path)
.map_err(|e| {
DaemonError::PidFile(format!(
"cannot create probe in PID directory {} as uid {} (pid cleanup will fail): {}",
parent.display(),
uid.as_raw(),
e
))
})?;
fs::remove_file(&probe_path).map_err(|e| {
DaemonError::PidFile(format!(
"cannot remove probe in PID directory {} as uid {} (pid cleanup will fail): {}",
parent.display(),
uid.as_raw(),
e
))
})?;
}
}
Ok(())

View File

@@ -763,7 +763,11 @@ async fn run_inner(
// 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()) {
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);
}
@@ -782,6 +786,7 @@ async fn run_inner(
&startup_tracker,
stats.clone(),
beobachten.clone(),
shared_state.clone(),
ip_tracker.clone(),
config_rx.clone(),
)

View File

@@ -13,6 +13,7 @@ use crate::crypto::SecureRandom;
use crate::ip_tracker::UserIpTracker;
use crate::metrics;
use crate::network::probe::NetworkProbe;
use crate::proxy::shared_state::ProxySharedState;
use crate::startup::{
COMPONENT_CONFIG_WATCHER_START, COMPONENT_METRICS_START, COMPONENT_RUNTIME_READY,
StartupTracker,
@@ -287,6 +288,7 @@ pub(crate) async fn spawn_metrics_if_configured(
startup_tracker: &Arc<StartupTracker>,
stats: Arc<Stats>,
beobachten: Arc<BeobachtenStore>,
shared_state: Arc<ProxySharedState>,
ip_tracker: Arc<UserIpTracker>,
config_rx: watch::Receiver<Arc<ProxyConfig>>,
) {
@@ -320,6 +322,7 @@ pub(crate) async fn spawn_metrics_if_configured(
.await;
let stats = stats.clone();
let beobachten = beobachten.clone();
let shared_state = shared_state.clone();
let config_rx_metrics = config_rx.clone();
let ip_tracker_metrics = ip_tracker.clone();
let whitelist = config.server.metrics_whitelist.clone();
@@ -331,6 +334,7 @@ pub(crate) async fn spawn_metrics_if_configured(
listen_backlog,
stats,
beobachten,
shared_state,
ip_tracker_metrics,
config_rx_metrics,
whitelist,

View File

@@ -15,6 +15,7 @@ use tracing::{debug, info, warn};
use crate::config::ProxyConfig;
use crate::ip_tracker::UserIpTracker;
use crate::proxy::shared_state::ProxySharedState;
use crate::stats::Stats;
use crate::stats::beobachten::BeobachtenStore;
use crate::transport::{ListenOptions, create_listener};
@@ -25,6 +26,7 @@ pub async fn serve(
listen_backlog: u32,
stats: Arc<Stats>,
beobachten: Arc<BeobachtenStore>,
shared_state: Arc<ProxySharedState>,
ip_tracker: Arc<UserIpTracker>,
config_rx: tokio::sync::watch::Receiver<Arc<ProxyConfig>>,
whitelist: Vec<IpNetwork>,
@@ -45,7 +47,13 @@ pub async fn serve(
Ok(listener) => {
info!("Metrics endpoint: http://{}/metrics and /beobachten", addr);
serve_listener(
listener, stats, beobachten, ip_tracker, config_rx, whitelist,
listener,
stats,
beobachten,
shared_state,
ip_tracker,
config_rx,
whitelist,
)
.await;
}
@@ -94,13 +102,20 @@ pub async fn serve(
}
(Some(listener), None) | (None, Some(listener)) => {
serve_listener(
listener, stats, beobachten, ip_tracker, config_rx, whitelist,
listener,
stats,
beobachten,
shared_state,
ip_tracker,
config_rx,
whitelist,
)
.await;
}
(Some(listener4), Some(listener6)) => {
let stats_v6 = stats.clone();
let beobachten_v6 = beobachten.clone();
let shared_state_v6 = shared_state.clone();
let ip_tracker_v6 = ip_tracker.clone();
let config_rx_v6 = config_rx.clone();
let whitelist_v6 = whitelist.clone();
@@ -109,6 +124,7 @@ pub async fn serve(
listener6,
stats_v6,
beobachten_v6,
shared_state_v6,
ip_tracker_v6,
config_rx_v6,
whitelist_v6,
@@ -116,7 +132,13 @@ pub async fn serve(
.await;
});
serve_listener(
listener4, stats, beobachten, ip_tracker, config_rx, whitelist,
listener4,
stats,
beobachten,
shared_state,
ip_tracker,
config_rx,
whitelist,
)
.await;
}
@@ -142,6 +164,7 @@ async fn serve_listener(
listener: TcpListener,
stats: Arc<Stats>,
beobachten: Arc<BeobachtenStore>,
shared_state: Arc<ProxySharedState>,
ip_tracker: Arc<UserIpTracker>,
config_rx: tokio::sync::watch::Receiver<Arc<ProxyConfig>>,
whitelist: Arc<Vec<IpNetwork>>,
@@ -162,15 +185,27 @@ async fn serve_listener(
let stats = stats.clone();
let beobachten = beobachten.clone();
let shared_state = shared_state.clone();
let ip_tracker = ip_tracker.clone();
let config_rx_conn = config_rx.clone();
tokio::spawn(async move {
let svc = service_fn(move |req| {
let stats = stats.clone();
let beobachten = beobachten.clone();
let shared_state = shared_state.clone();
let ip_tracker = ip_tracker.clone();
let config = config_rx_conn.borrow().clone();
async move { handle(req, &stats, &beobachten, &ip_tracker, &config).await }
async move {
handle(
req,
&stats,
&beobachten,
&shared_state,
&ip_tracker,
&config,
)
.await
}
});
if let Err(e) = http1::Builder::new()
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
@@ -186,11 +221,12 @@ async fn handle<B>(
req: Request<B>,
stats: &Stats,
beobachten: &BeobachtenStore,
shared_state: &ProxySharedState,
ip_tracker: &UserIpTracker,
config: &ProxyConfig,
) -> Result<Response<Full<Bytes>>, Infallible> {
if req.uri().path() == "/metrics" {
let body = render_metrics(stats, config, ip_tracker).await;
let body = render_metrics(stats, shared_state, config, ip_tracker).await;
let resp = Response::builder()
.status(StatusCode::OK)
.header("content-type", "text/plain; version=0.0.4; charset=utf-8")
@@ -225,7 +261,12 @@ fn render_beobachten(beobachten: &BeobachtenStore, config: &ProxyConfig) -> Stri
beobachten.snapshot_text(ttl)
}
async fn render_metrics(stats: &Stats, config: &ProxyConfig, ip_tracker: &UserIpTracker) -> String {
async fn render_metrics(
stats: &Stats,
shared_state: &ProxySharedState,
config: &ProxyConfig,
ip_tracker: &UserIpTracker,
) -> String {
use std::fmt::Write;
let mut out = String::with_capacity(4096);
let telemetry = stats.telemetry_policy();
@@ -234,6 +275,17 @@ async fn render_metrics(stats: &Stats, config: &ProxyConfig, ip_tracker: &UserIp
let me_allows_normal = telemetry.me_level.allows_normal();
let me_allows_debug = telemetry.me_level.allows_debug();
let _ = writeln!(
out,
"# HELP telemt_build_info Build information for the running telemt binary"
);
let _ = writeln!(out, "# TYPE telemt_build_info gauge");
let _ = writeln!(
out,
"telemt_build_info{{version=\"{}\"}} 1",
env!("CARGO_PKG_VERSION")
);
let _ = writeln!(out, "# HELP telemt_uptime_seconds Proxy uptime");
let _ = writeln!(out, "# TYPE telemt_uptime_seconds gauge");
let _ = writeln!(out, "telemt_uptime_seconds {:.1}", stats.uptime_secs());
@@ -359,6 +411,42 @@ async fn render_metrics(stats: &Stats, config: &ProxyConfig, ip_tracker: &UserIp
}
);
let _ = writeln!(
out,
"# HELP telemt_auth_expensive_checks_total Expensive authentication candidate checks executed during handshake validation"
);
let _ = writeln!(out, "# TYPE telemt_auth_expensive_checks_total counter");
let _ = writeln!(
out,
"telemt_auth_expensive_checks_total {}",
if core_enabled {
shared_state
.handshake
.auth_expensive_checks_total
.load(std::sync::atomic::Ordering::Relaxed)
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_auth_budget_exhausted_total Handshake validations that hit authentication candidate budget limits"
);
let _ = writeln!(out, "# TYPE telemt_auth_budget_exhausted_total counter");
let _ = writeln!(
out,
"telemt_auth_budget_exhausted_total {}",
if core_enabled {
shared_state
.handshake
.auth_budget_exhausted_total
.load(std::sync::atomic::Ordering::Relaxed)
} else {
0
}
);
let _ = writeln!(
out,
"# HELP telemt_accept_permit_timeout_total Accepted connections dropped due to permit wait timeout"
@@ -2847,6 +2935,7 @@ mod tests {
#[tokio::test]
async fn test_render_metrics_format() {
let stats = Arc::new(Stats::new());
let shared_state = ProxySharedState::new();
let tracker = UserIpTracker::new();
let mut config = ProxyConfig::default();
config
@@ -2858,6 +2947,14 @@ mod tests {
stats.increment_connects_all();
stats.increment_connects_bad();
stats.increment_handshake_timeouts();
shared_state
.handshake
.auth_expensive_checks_total
.fetch_add(9, std::sync::atomic::Ordering::Relaxed);
shared_state
.handshake
.auth_budget_exhausted_total
.fetch_add(2, std::sync::atomic::Ordering::Relaxed);
stats.increment_upstream_connect_attempt_total();
stats.increment_upstream_connect_attempt_total();
stats.increment_upstream_connect_success_total();
@@ -2901,11 +2998,17 @@ mod tests {
.await
.unwrap();
let output = render_metrics(&stats, &config, &tracker).await;
let output = render_metrics(&stats, shared_state.as_ref(), &config, &tracker).await;
assert!(output.contains(&format!(
"telemt_build_info{{version=\"{}\"}} 1",
env!("CARGO_PKG_VERSION")
)));
assert!(output.contains("telemt_connections_total 2"));
assert!(output.contains("telemt_connections_bad_total 1"));
assert!(output.contains("telemt_handshake_timeouts_total 1"));
assert!(output.contains("telemt_auth_expensive_checks_total 9"));
assert!(output.contains("telemt_auth_budget_exhausted_total 2"));
assert!(output.contains("telemt_upstream_connect_attempt_total 2"));
assert!(output.contains("telemt_upstream_connect_success_total 1"));
assert!(output.contains("telemt_upstream_connect_fail_total 1"));
@@ -2960,12 +3063,15 @@ mod tests {
#[tokio::test]
async fn test_render_empty_stats() {
let stats = Stats::new();
let shared_state = ProxySharedState::new();
let tracker = UserIpTracker::new();
let config = ProxyConfig::default();
let output = render_metrics(&stats, &config, &tracker).await;
let output = render_metrics(&stats, &shared_state, &config, &tracker).await;
assert!(output.contains("telemt_connections_total 0"));
assert!(output.contains("telemt_connections_bad_total 0"));
assert!(output.contains("telemt_handshake_timeouts_total 0"));
assert!(output.contains("telemt_auth_expensive_checks_total 0"));
assert!(output.contains("telemt_auth_budget_exhausted_total 0"));
assert!(output.contains("telemt_user_unique_ips_current{user="));
assert!(output.contains("telemt_user_unique_ips_recent_window{user="));
}
@@ -2973,6 +3079,7 @@ mod tests {
#[tokio::test]
async fn test_render_uses_global_each_unique_ip_limit() {
let stats = Stats::new();
let shared_state = ProxySharedState::new();
stats.increment_user_connects("alice");
stats.increment_user_curr_connects("alice");
let tracker = UserIpTracker::new();
@@ -2983,7 +3090,7 @@ mod tests {
let mut config = ProxyConfig::default();
config.access.user_max_unique_ips_global_each = 2;
let output = render_metrics(&stats, &config, &tracker).await;
let output = render_metrics(&stats, &shared_state, &config, &tracker).await;
assert!(output.contains("telemt_user_unique_ips_limit{user=\"alice\"} 2"));
assert!(output.contains("telemt_user_unique_ips_utilization{user=\"alice\"} 0.500000"));
@@ -2992,13 +3099,16 @@ mod tests {
#[tokio::test]
async fn test_render_has_type_annotations() {
let stats = Stats::new();
let shared_state = ProxySharedState::new();
let tracker = UserIpTracker::new();
let config = ProxyConfig::default();
let output = render_metrics(&stats, &config, &tracker).await;
let output = render_metrics(&stats, &shared_state, &config, &tracker).await;
assert!(output.contains("# TYPE telemt_uptime_seconds gauge"));
assert!(output.contains("# TYPE telemt_connections_total counter"));
assert!(output.contains("# TYPE telemt_connections_bad_total counter"));
assert!(output.contains("# TYPE telemt_handshake_timeouts_total counter"));
assert!(output.contains("# TYPE telemt_auth_expensive_checks_total counter"));
assert!(output.contains("# TYPE telemt_auth_budget_exhausted_total counter"));
assert!(output.contains("# TYPE telemt_upstream_connect_attempt_total counter"));
assert!(output.contains("# TYPE telemt_me_rpc_proxy_req_signal_sent_total counter"));
assert!(output.contains("# TYPE telemt_me_idle_close_by_peer_total counter"));
@@ -3035,6 +3145,7 @@ mod tests {
async fn test_endpoint_integration() {
let stats = Arc::new(Stats::new());
let beobachten = Arc::new(BeobachtenStore::new());
let shared_state = ProxySharedState::new();
let tracker = UserIpTracker::new();
let mut config = ProxyConfig::default();
stats.increment_connects_all();
@@ -3042,9 +3153,16 @@ mod tests {
stats.increment_connects_all();
let req = Request::builder().uri("/metrics").body(()).unwrap();
let resp = handle(req, &stats, &beobachten, &tracker, &config)
.await
.unwrap();
let resp = handle(
req,
&stats,
&beobachten,
shared_state.as_ref(),
&tracker,
&config,
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = resp.into_body().collect().await.unwrap().to_bytes();
assert!(
@@ -3052,6 +3170,14 @@ mod tests {
.unwrap()
.contains("telemt_connections_total 3")
);
assert!(
std::str::from_utf8(body.as_ref())
.unwrap()
.contains(&format!(
"telemt_build_info{{version=\"{}\"}} 1",
env!("CARGO_PKG_VERSION")
))
);
config.general.beobachten = true;
config.general.beobachten_minutes = 10;
@@ -3061,9 +3187,16 @@ mod tests {
Duration::from_secs(600),
);
let req_beob = Request::builder().uri("/beobachten").body(()).unwrap();
let resp_beob = handle(req_beob, &stats, &beobachten, &tracker, &config)
.await
.unwrap();
let resp_beob = handle(
req_beob,
&stats,
&beobachten,
shared_state.as_ref(),
&tracker,
&config,
)
.await
.unwrap();
assert_eq!(resp_beob.status(), StatusCode::OK);
let body_beob = resp_beob.into_body().collect().await.unwrap().to_bytes();
let beob_text = std::str::from_utf8(body_beob.as_ref()).unwrap();
@@ -3071,9 +3204,16 @@ mod tests {
assert!(beob_text.contains("203.0.113.10-1"));
let req404 = Request::builder().uri("/other").body(()).unwrap();
let resp404 = handle(req404, &stats, &beobachten, &tracker, &config)
.await
.unwrap();
let resp404 = handle(
req404,
&stats,
&beobachten,
shared_state.as_ref(),
&tracker,
&config,
)
.await
.unwrap();
assert_eq!(resp404.status(), StatusCode::NOT_FOUND);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -56,6 +56,8 @@ const ME_D2C_FLUSH_BATCH_MAX_BYTES_MIN: usize = 4096;
const ME_D2C_FRAME_BUF_SHRINK_HYSTERESIS_FACTOR: usize = 2;
const ME_D2C_SINGLE_WRITE_COALESCE_MAX_BYTES: usize = 128 * 1024;
const QUOTA_RESERVE_SPIN_RETRIES: usize = 32;
const QUOTA_RESERVE_BACKOFF_MIN_MS: u64 = 1;
const QUOTA_RESERVE_BACKOFF_MAX_MS: u64 = 16;
#[derive(Default)]
pub(crate) struct DesyncDedupRotationState {
@@ -573,6 +575,7 @@ async fn reserve_user_quota_with_yield(
bytes: u64,
limit: u64,
) -> std::result::Result<u64, QuotaReserveError> {
let mut backoff_ms = QUOTA_RESERVE_BACKOFF_MIN_MS;
loop {
for _ in 0..QUOTA_RESERVE_SPIN_RETRIES {
match user_stats.quota_try_reserve(bytes, limit) {
@@ -585,6 +588,10 @@ async fn reserve_user_quota_with_yield(
}
tokio::task::yield_now().await;
tokio::time::sleep(Duration::from_millis(backoff_ms)).await;
backoff_ms = backoff_ms
.saturating_mul(2)
.min(QUOTA_RESERVE_BACKOFF_MAX_MS);
}
}

View File

@@ -270,6 +270,8 @@ const QUOTA_NEAR_LIMIT_BYTES: u64 = 64 * 1024;
const QUOTA_LARGE_CHARGE_BYTES: u64 = 16 * 1024;
const QUOTA_ADAPTIVE_INTERVAL_MIN_BYTES: u64 = 4 * 1024;
const QUOTA_ADAPTIVE_INTERVAL_MAX_BYTES: u64 = 64 * 1024;
const QUOTA_RESERVE_SPIN_RETRIES: usize = 64;
const QUOTA_RESERVE_MAX_ROUNDS: usize = 8;
#[inline]
fn quota_adaptive_interval_bytes(remaining_before: u64) -> u64 {
@@ -314,6 +316,56 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
if n > 0 {
let n_to_charge = n as u64;
if let (Some(limit), Some(remaining)) = (this.quota_limit, remaining_before) {
let mut reserved_total = None;
let mut reserve_rounds = 0usize;
while reserved_total.is_none() {
let mut saw_contention = false;
for _ in 0..QUOTA_RESERVE_SPIN_RETRIES {
match this.user_stats.quota_try_reserve(n_to_charge, limit) {
Ok(total) => {
reserved_total = Some(total);
break;
}
Err(crate::stats::QuotaReserveError::LimitExceeded) => {
this.quota_exceeded.store(true, Ordering::Release);
buf.set_filled(before);
return Poll::Ready(Err(quota_io_error()));
}
Err(crate::stats::QuotaReserveError::Contended) => {
saw_contention = true;
}
}
}
if reserved_total.is_none() {
reserve_rounds = reserve_rounds.saturating_add(1);
if reserve_rounds >= QUOTA_RESERVE_MAX_ROUNDS {
this.quota_exceeded.store(true, Ordering::Release);
buf.set_filled(before);
return Poll::Ready(Err(quota_io_error()));
}
if saw_contention {
std::thread::yield_now();
}
}
}
if should_immediate_quota_check(remaining, n_to_charge) {
this.quota_bytes_since_check = 0;
} else {
this.quota_bytes_since_check =
this.quota_bytes_since_check.saturating_add(n_to_charge);
let interval = quota_adaptive_interval_bytes(remaining);
if this.quota_bytes_since_check >= interval {
this.quota_bytes_since_check = 0;
}
}
if reserved_total.unwrap_or(0) >= limit {
this.quota_exceeded.store(true, Ordering::Release);
}
}
// C→S: client sent data
this.counters
.c2s_bytes
@@ -326,27 +378,6 @@ impl<S: AsyncRead + Unpin> AsyncRead for StatsIo<S> {
this.stats
.increment_user_msgs_from_handle(this.user_stats.as_ref());
if let (Some(limit), Some(remaining)) = (this.quota_limit, remaining_before) {
this.stats
.quota_charge_post_write(this.user_stats.as_ref(), n_to_charge);
if should_immediate_quota_check(remaining, n_to_charge) {
this.quota_bytes_since_check = 0;
if this.user_stats.quota_used() >= limit {
this.quota_exceeded.store(true, Ordering::Release);
}
} else {
this.quota_bytes_since_check =
this.quota_bytes_since_check.saturating_add(n_to_charge);
let interval = quota_adaptive_interval_bytes(remaining);
if this.quota_bytes_since_check >= interval {
this.quota_bytes_since_check = 0;
if this.user_stats.quota_used() >= limit {
this.quota_exceeded.store(true, Ordering::Release);
}
}
}
}
trace!(user = %this.user, bytes = n, "C->S");
}
Poll::Ready(Ok(()))
@@ -368,18 +399,79 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
}
let mut remaining_before = None;
let mut reserved_bytes = 0u64;
let mut write_buf = buf;
if let Some(limit) = this.quota_limit {
let used_before = this.user_stats.quota_used();
let remaining = limit.saturating_sub(used_before);
if remaining == 0 {
this.quota_exceeded.store(true, Ordering::Release);
return Poll::Ready(Err(quota_io_error()));
if !buf.is_empty() {
let mut reserve_rounds = 0usize;
while reserved_bytes == 0 {
let used_before = this.user_stats.quota_used();
let remaining = limit.saturating_sub(used_before);
if remaining == 0 {
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 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];
break;
}
Err(crate::stats::QuotaReserveError::LimitExceeded) => {
break;
}
Err(crate::stats::QuotaReserveError::Contended) => {
saw_contention = true;
}
}
}
if reserved_bytes == 0 {
reserve_rounds = reserve_rounds.saturating_add(1);
if reserve_rounds >= QUOTA_RESERVE_MAX_ROUNDS {
this.quota_exceeded.store(true, Ordering::Release);
return Poll::Ready(Err(quota_io_error()));
}
if saw_contention {
std::thread::yield_now();
}
}
}
} else {
let used_before = this.user_stats.quota_used();
let remaining = limit.saturating_sub(used_before);
if remaining == 0 {
this.quota_exceeded.store(true, Ordering::Release);
return Poll::Ready(Err(quota_io_error()));
}
remaining_before = Some(remaining);
}
remaining_before = Some(remaining);
}
match Pin::new(&mut this.inner).poll_write(cx, buf) {
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,
}
}
}
if n > 0 {
let n_to_charge = n as u64;
@@ -396,8 +488,6 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
.increment_user_msgs_to_handle(this.user_stats.as_ref());
if let (Some(limit), Some(remaining)) = (this.quota_limit, remaining_before) {
this.stats
.quota_charge_post_write(this.user_stats.as_ref(), n_to_charge);
if should_immediate_quota_check(remaining, n_to_charge) {
this.quota_bytes_since_check = 0;
if this.user_stats.quota_used() >= limit {
@@ -420,7 +510,42 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
}
Poll::Ready(Ok(n))
}
other => other,
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,
}
}
}
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,
}
}
}
Poll::Pending
}
}
}

View File

@@ -1,7 +1,7 @@
use std::collections::HashSet;
use std::collections::hash_map::RandomState;
use std::net::{IpAddr, SocketAddr};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Instant;
@@ -11,6 +11,8 @@ use tokio::sync::mpsc;
use crate::proxy::handshake::{AuthProbeSaturationState, AuthProbeState};
use crate::proxy::middle_relay::{DesyncDedupRotationState, RelayIdleCandidateRegistry};
const HANDSHAKE_RECENT_USER_RING_LEN: usize = 64;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ConntrackCloseReason {
NormalEof,
@@ -41,6 +43,13 @@ pub(crate) struct HandshakeSharedState {
pub(crate) auth_probe_eviction_hasher: RandomState,
pub(crate) invalid_secret_warned: Mutex<HashSet<(String, String)>>,
pub(crate) unknown_sni_warn_next_allowed: Mutex<Option<Instant>>,
pub(crate) sticky_user_by_ip: DashMap<IpAddr, u32>,
pub(crate) sticky_user_by_ip_prefix: DashMap<u64, u32>,
pub(crate) sticky_user_by_sni_hash: DashMap<u64, u32>,
pub(crate) recent_user_ring: Box<[AtomicU32]>,
pub(crate) recent_user_ring_seq: AtomicU64,
pub(crate) auth_expensive_checks_total: AtomicU64,
pub(crate) auth_budget_exhausted_total: AtomicU64,
}
pub(crate) struct MiddleRelaySharedState {
@@ -69,6 +78,16 @@ impl ProxySharedState {
auth_probe_eviction_hasher: RandomState::new(),
invalid_secret_warned: Mutex::new(HashSet::new()),
unknown_sni_warn_next_allowed: Mutex::new(None),
sticky_user_by_ip: DashMap::new(),
sticky_user_by_ip_prefix: DashMap::new(),
sticky_user_by_sni_hash: DashMap::new(),
recent_user_ring: std::iter::repeat_with(|| AtomicU32::new(0))
.take(HANDSHAKE_RECENT_USER_RING_LEN)
.collect::<Vec<_>>()
.into_boxed_slice(),
recent_user_ring_seq: AtomicU64::new(0),
auth_expensive_checks_total: AtomicU64::new(0),
auth_budget_exhausted_total: AtomicU64::new(0),
},
middle_relay: MiddleRelaySharedState {
desync_dedup: DashMap::new(),

View File

@@ -5,6 +5,7 @@ use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
use tokio::sync::Barrier;
@@ -1006,6 +1007,64 @@ async fn tls_unknown_sni_mask_policy_falls_back_to_bad_client() {
assert!(matches!(result, HandshakeResult::BadClient { .. }));
}
#[tokio::test]
async fn tls_unknown_sni_accept_policy_continues_auth_path() {
let secret = [0x4Bu8; 16];
let mut config = test_config_with_secret_hex("4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b");
config.censorship.unknown_sni_action = UnknownSniAction::Accept;
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let rng = SecureRandom::new();
let peer: SocketAddr = "198.51.100.210:44326".parse().unwrap();
let handshake =
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "unknown.example", &[b"h2"]);
let result = handle_tls_handshake(
&handshake,
tokio::io::empty(),
tokio::io::sink(),
peer,
&config,
&replay_checker,
&rng,
None,
)
.await;
assert!(matches!(result, HandshakeResult::Success(_)));
}
#[tokio::test]
async fn tls_unknown_sni_accept_policy_still_requires_valid_secret() {
let mut config = test_config_with_secret_hex("4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c");
config.censorship.unknown_sni_action = UnknownSniAction::Accept;
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let rng = SecureRandom::new();
let peer: SocketAddr = "198.51.100.211:44326".parse().unwrap();
let attacker_secret = [0x4Du8; 16];
let handshake = make_valid_tls_client_hello_with_sni_and_alpn(
&attacker_secret,
0,
"unknown.example",
&[b"h2"],
);
let result = handle_tls_handshake(
&handshake,
tokio::io::empty(),
tokio::io::sink(),
peer,
&config,
&replay_checker,
&rng,
None,
)
.await;
assert!(matches!(result, HandshakeResult::BadClient { .. }));
}
#[tokio::test]
async fn tls_missing_sni_keeps_legacy_auth_path() {
let secret = [0x4Au8; 16];
@@ -1032,6 +1091,170 @@ async fn tls_missing_sni_keeps_legacy_auth_path() {
assert!(matches!(result, HandshakeResult::Success(_)));
}
#[tokio::test]
async fn tls_runtime_snapshot_updates_sticky_and_recent_hints() {
let secret = [0x5Au8; 16];
let mut config = test_config_with_secret_hex("5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a5a");
config.rebuild_runtime_user_auth().unwrap();
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let rng = SecureRandom::new();
let shared = ProxySharedState::new();
let peer: SocketAddr = "198.51.100.212:44326".parse().unwrap();
let handshake = make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "user", &[b"h2"]);
let result = handle_tls_handshake_with_shared(
&handshake,
tokio::io::empty(),
tokio::io::sink(),
peer,
&config,
&replay_checker,
&rng,
None,
shared.as_ref(),
)
.await;
assert!(matches!(result, HandshakeResult::Success(_)));
assert_eq!(
shared
.handshake
.sticky_user_by_ip
.get(&peer.ip())
.map(|entry| *entry),
Some(0),
"successful runtime-snapshot auth must seed sticky ip cache"
);
assert_eq!(
shared.handshake.sticky_user_by_ip_prefix.len(),
1,
"successful runtime-snapshot auth must seed sticky prefix cache"
);
assert!(
shared
.handshake
.auth_expensive_checks_total
.load(Ordering::Relaxed)
>= 1,
"runtime-snapshot path must account expensive candidate checks"
);
}
#[tokio::test]
async fn tls_overload_budget_limits_candidate_scan_depth() {
let mut config = ProxyConfig::default();
config.access.users.clear();
config.access.ignore_time_skew = true;
for idx in 0..32u8 {
config.access.users.insert(
format!("user-{idx}"),
format!("{:032x}", u128::from(idx) + 1),
);
}
config.rebuild_runtime_user_auth().unwrap();
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let rng = SecureRandom::new();
let shared = ProxySharedState::new();
let now = Instant::now();
{
let mut saturation = shared.handshake.auth_probe_saturation.lock().unwrap();
*saturation = Some(AuthProbeSaturationState {
fail_streak: AUTH_PROBE_BACKOFF_START_FAILS,
blocked_until: now + Duration::from_millis(200),
last_seen: now,
});
}
let peer: SocketAddr = "198.51.100.213:44326".parse().unwrap();
let attacker_secret = [0xEFu8; 16];
let handshake = make_valid_tls_handshake(&attacker_secret, 0);
let result = handle_tls_handshake_with_shared(
&handshake,
tokio::io::empty(),
tokio::io::sink(),
peer,
&config,
&replay_checker,
&rng,
None,
shared.as_ref(),
)
.await;
assert!(matches!(result, HandshakeResult::BadClient { .. }));
assert_eq!(
shared
.handshake
.auth_budget_exhausted_total
.load(Ordering::Relaxed),
1,
"overload mode must account budget exhaustion when scan is capped"
);
assert_eq!(
shared
.handshake
.auth_expensive_checks_total
.load(Ordering::Relaxed),
OVERLOAD_CANDIDATE_BUDGET_UNHINTED as u64,
"overload scan depth must stay within capped candidate budget"
);
}
#[tokio::test]
async fn mtproto_runtime_snapshot_prefers_preferred_user_hint() {
let mut config = ProxyConfig::default();
config.general.modes.secure = true;
config.access.users.clear();
config.access.ignore_time_skew = true;
config.access.users.insert(
"alpha".to_string(),
"11111111111111111111111111111111".to_string(),
);
config.access.users.insert(
"beta".to_string(),
"22222222222222222222222222222222".to_string(),
);
config.rebuild_runtime_user_auth().unwrap();
let handshake =
make_valid_mtproto_handshake("22222222222222222222222222222222", ProtoTag::Secure, 2);
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
let peer: SocketAddr = "198.51.100.214:44326".parse().unwrap();
let shared = ProxySharedState::new();
let result = handle_mtproto_handshake_with_shared(
&handshake,
tokio::io::empty(),
tokio::io::sink(),
peer,
&config,
&replay_checker,
false,
Some("beta"),
shared.as_ref(),
)
.await;
match result {
HandshakeResult::Success((_, _, success)) => {
assert_eq!(success.user, "beta");
}
_ => panic!("mtproto runtime snapshot auth must succeed for preferred user"),
}
assert_eq!(
shared
.handshake
.auth_expensive_checks_total
.load(Ordering::Relaxed),
1,
"preferred user hint must produce single-candidate success in snapshot path"
);
}
#[tokio::test]
async fn alpn_enforce_rejects_unsupported_client_alpn() {
let secret = [0x33u8; 16];

View File

@@ -7,6 +7,7 @@ use crate::protocol::constants::{
};
use crate::protocol::tls::{TLS_DIGEST_LEN, TLS_DIGEST_POS, gen_fake_x25519_key};
use crate::tls_front::types::{CachedTlsData, ParsedCertificateInfo, TlsProfileSource};
use crc32fast::Hasher;
const MIN_APP_DATA: usize = 64;
const MAX_APP_DATA: usize = MAX_TLS_CIPHERTEXT_SIZE;
@@ -98,6 +99,31 @@ fn build_compact_cert_info_payload(cert_info: &ParsedCertificateInfo) -> Option<
Some(payload)
}
fn hash_compact_cert_info_payload(cert_payload: Vec<u8>) -> Option<Vec<u8>> {
if cert_payload.is_empty() {
return None;
}
let mut hashed = Vec::with_capacity(cert_payload.len());
let mut seed_hasher = Hasher::new();
seed_hasher.update(&cert_payload);
let mut state = seed_hasher.finalize();
while hashed.len() < cert_payload.len() {
let mut hasher = Hasher::new();
hasher.update(&state.to_le_bytes());
hasher.update(&cert_payload);
state = hasher.finalize();
let block = state.to_le_bytes();
let remaining = cert_payload.len() - hashed.len();
let copy_len = remaining.min(block.len());
hashed.extend_from_slice(&block[..copy_len]);
}
Some(hashed)
}
/// Build a ServerHello + CCS + ApplicationData sequence using cached TLS metadata.
pub fn build_emulated_server_hello(
secret: &[u8],
@@ -190,7 +216,8 @@ pub fn build_emulated_server_hello(
let compact_payload = cached
.cert_info
.as_ref()
.and_then(build_compact_cert_info_payload);
.and_then(build_compact_cert_info_payload)
.and_then(hash_compact_cert_info_payload);
let selected_payload: Option<&[u8]> = if use_full_cert_payload {
cached
.cert_payload
@@ -221,7 +248,6 @@ pub fn build_emulated_server_hello(
marker.extend_from_slice(proto);
marker
});
let mut payload_offset = 0usize;
for (idx, size) in sizes.into_iter().enumerate() {
let mut rec = Vec::with_capacity(5 + size);
rec.push(TLS_RECORD_APPLICATION);
@@ -231,11 +257,10 @@ pub fn build_emulated_server_hello(
if let Some(payload) = selected_payload {
if size > 17 {
let body_len = size - 17;
let remaining = payload.len().saturating_sub(payload_offset);
let remaining = payload.len();
let copy_len = remaining.min(body_len);
if copy_len > 0 {
rec.extend_from_slice(&payload[payload_offset..payload_offset + copy_len]);
payload_offset += copy_len;
rec.extend_from_slice(&payload[..copy_len]);
}
if body_len > copy_len {
rec.extend_from_slice(&rng.bytes(body_len - copy_len));
@@ -317,7 +342,10 @@ mod tests {
CachedTlsData, ParsedServerHello, TlsBehaviorProfile, TlsCertPayload, TlsProfileSource,
};
use super::build_emulated_server_hello;
use super::{
build_compact_cert_info_payload, build_emulated_server_hello,
hash_compact_cert_info_payload,
};
use crate::crypto::SecureRandom;
use crate::protocol::constants::{
TLS_RECORD_APPLICATION, TLS_RECORD_CHANGE_CIPHER, TLS_RECORD_HANDSHAKE,
@@ -432,7 +460,21 @@ mod tests {
);
let payload = first_app_data_payload(&response);
assert!(payload.starts_with(b"CN=example.com"));
let expected_hashed_payload = build_compact_cert_info_payload(
cached
.cert_info
.as_ref()
.expect("test fixture must provide certificate info"),
)
.and_then(hash_compact_cert_info_payload)
.expect("compact certificate info payload must be present for this test");
let copied_prefix_len = expected_hashed_payload
.len()
.min(payload.len().saturating_sub(17));
assert_eq!(
&payload[..copied_prefix_len],
&expected_hashed_payload[..copied_prefix_len]
);
}
#[test]

View File

@@ -23,6 +23,60 @@ use super::codec::{RpcChecksumMode, WriterCommand, rpc_crc};
use super::registry::RouteResult;
use super::{ConnRegistry, MeResponse};
const DATA_ROUTE_MAX_ATTEMPTS: usize = 3;
const DATA_ROUTE_QUEUE_FULL_STARVATION_THRESHOLD: u8 = 3;
fn should_close_on_route_result_for_data(result: RouteResult) -> bool {
matches!(result, RouteResult::NoConn | RouteResult::ChannelClosed)
}
fn should_close_on_route_result_for_ack(result: RouteResult) -> bool {
matches!(result, RouteResult::NoConn | RouteResult::ChannelClosed)
}
fn is_data_route_queue_full(result: RouteResult) -> bool {
matches!(
result,
RouteResult::QueueFullBase | RouteResult::QueueFullHigh
)
}
fn should_close_on_queue_full_streak(streak: u8) -> bool {
streak >= DATA_ROUTE_QUEUE_FULL_STARVATION_THRESHOLD
}
async fn route_data_with_retry(
reg: &ConnRegistry,
conn_id: u64,
flags: u32,
data: Bytes,
timeout_ms: u64,
) -> RouteResult {
let mut attempt = 0usize;
loop {
let routed = reg
.route_with_timeout(
conn_id,
MeResponse::Data {
flags,
data: data.clone(),
},
timeout_ms,
)
.await;
match routed {
RouteResult::QueueFullBase | RouteResult::QueueFullHigh => {
attempt = attempt.saturating_add(1);
if attempt >= DATA_ROUTE_MAX_ATTEMPTS {
return routed;
}
tokio::task::yield_now().await;
}
_ => return routed,
}
}
}
pub(crate) async fn reader_loop(
mut rd: tokio::io::ReadHalf<TcpStream>,
dk: [u8; 32],
@@ -43,6 +97,7 @@ pub(crate) async fn reader_loop(
) -> Result<()> {
let mut raw = enc_leftover;
let mut expected_seq: i32 = 0;
let mut data_route_queue_full_streak = HashMap::<u64, u8>::new();
loop {
let mut tmp = [0u8; 65_536];
@@ -127,27 +182,39 @@ pub(crate) async fn reader_loop(
trace!(cid, flags, len = data.len(), "RPC_PROXY_ANS");
let route_wait_ms = reader_route_data_wait_ms.load(Ordering::Relaxed);
let routed = reg
.route_with_timeout(cid, MeResponse::Data { flags, data }, route_wait_ms)
.await;
if !matches!(routed, RouteResult::Routed) {
match routed {
RouteResult::NoConn => stats.increment_me_route_drop_no_conn(),
RouteResult::ChannelClosed => {
stats.increment_me_route_drop_channel_closed()
}
RouteResult::QueueFullBase => {
stats.increment_me_route_drop_queue_full();
stats.increment_me_route_drop_queue_full_base();
}
RouteResult::QueueFullHigh => {
stats.increment_me_route_drop_queue_full();
stats.increment_me_route_drop_queue_full_high();
}
RouteResult::Routed => {}
let routed =
route_data_with_retry(reg.as_ref(), cid, flags, data, route_wait_ms).await;
if matches!(routed, RouteResult::Routed) {
data_route_queue_full_streak.remove(&cid);
continue;
}
match routed {
RouteResult::NoConn => stats.increment_me_route_drop_no_conn(),
RouteResult::ChannelClosed => stats.increment_me_route_drop_channel_closed(),
RouteResult::QueueFullBase => {
stats.increment_me_route_drop_queue_full();
stats.increment_me_route_drop_queue_full_base();
}
RouteResult::QueueFullHigh => {
stats.increment_me_route_drop_queue_full();
stats.increment_me_route_drop_queue_full_high();
}
RouteResult::Routed => {}
}
if should_close_on_route_result_for_data(routed) {
data_route_queue_full_streak.remove(&cid);
reg.unregister(cid).await;
send_close_conn(&tx, cid).await;
continue;
}
if is_data_route_queue_full(routed) {
let streak = data_route_queue_full_streak.entry(cid).or_insert(0);
*streak = streak.saturating_add(1);
if should_close_on_queue_full_streak(*streak) {
data_route_queue_full_streak.remove(&cid);
reg.unregister(cid).await;
send_close_conn(&tx, cid).await;
}
}
} else if pt == RPC_SIMPLE_ACK_U32 && body.len() >= 12 {
let cid = u64::from_le_bytes(body[0..8].try_into().unwrap());
@@ -171,19 +238,23 @@ pub(crate) async fn reader_loop(
}
RouteResult::Routed => {}
}
reg.unregister(cid).await;
send_close_conn(&tx, cid).await;
if should_close_on_route_result_for_ack(routed) {
reg.unregister(cid).await;
send_close_conn(&tx, cid).await;
}
}
} else if pt == RPC_CLOSE_EXT_U32 && body.len() >= 8 {
let cid = u64::from_le_bytes(body[0..8].try_into().unwrap());
debug!(cid, "RPC_CLOSE_EXT from ME");
let _ = reg.route_nowait(cid, MeResponse::Close).await;
reg.unregister(cid).await;
data_route_queue_full_streak.remove(&cid);
} else if pt == RPC_CLOSE_CONN_U32 && body.len() >= 8 {
let cid = u64::from_le_bytes(body[0..8].try_into().unwrap());
debug!(cid, "RPC_CLOSE_CONN from ME");
let _ = reg.route_nowait(cid, MeResponse::Close).await;
reg.unregister(cid).await;
data_route_queue_full_streak.remove(&cid);
} else if pt == RPC_PING_U32 && body.len() >= 8 {
let ping_id = i64::from_le_bytes(body[0..8].try_into().unwrap());
trace!(ping_id, "RPC_PING -> RPC_PONG");
@@ -243,6 +314,93 @@ pub(crate) async fn reader_loop(
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use crate::transport::middle_proxy::ConnRegistry;
use super::{
MeResponse, RouteResult, is_data_route_queue_full, route_data_with_retry,
should_close_on_queue_full_streak, should_close_on_route_result_for_ack,
should_close_on_route_result_for_data,
};
#[test]
fn data_route_only_fatal_results_close_immediately() {
assert!(!should_close_on_route_result_for_data(RouteResult::Routed));
assert!(!should_close_on_route_result_for_data(
RouteResult::QueueFullBase
));
assert!(!should_close_on_route_result_for_data(
RouteResult::QueueFullHigh
));
assert!(should_close_on_route_result_for_data(RouteResult::NoConn));
assert!(should_close_on_route_result_for_data(
RouteResult::ChannelClosed
));
}
#[test]
fn data_route_queue_full_uses_starvation_threshold() {
assert!(is_data_route_queue_full(RouteResult::QueueFullBase));
assert!(is_data_route_queue_full(RouteResult::QueueFullHigh));
assert!(!is_data_route_queue_full(RouteResult::NoConn));
assert!(!should_close_on_queue_full_streak(1));
assert!(!should_close_on_queue_full_streak(2));
assert!(should_close_on_queue_full_streak(3));
assert!(should_close_on_queue_full_streak(u8::MAX));
}
#[test]
fn ack_queue_full_is_soft_dropped_without_forced_close() {
assert!(!should_close_on_route_result_for_ack(RouteResult::Routed));
assert!(!should_close_on_route_result_for_ack(
RouteResult::QueueFullBase
));
assert!(!should_close_on_route_result_for_ack(
RouteResult::QueueFullHigh
));
assert!(should_close_on_route_result_for_ack(RouteResult::NoConn));
assert!(should_close_on_route_result_for_ack(
RouteResult::ChannelClosed
));
}
#[tokio::test]
async fn route_data_with_retry_returns_routed_when_channel_has_capacity() {
let reg = ConnRegistry::with_route_channel_capacity(1);
let (conn_id, mut rx) = reg.register().await;
let routed = route_data_with_retry(&reg, conn_id, 0, Bytes::from_static(b"a"), 20).await;
assert!(matches!(routed, RouteResult::Routed));
match rx.recv().await {
Some(MeResponse::Data { flags, data }) => {
assert_eq!(flags, 0);
assert_eq!(data, Bytes::from_static(b"a"));
}
other => panic!("expected routed data response, got {other:?}"),
}
}
#[tokio::test]
async fn route_data_with_retry_stops_after_bounded_attempts() {
let reg = ConnRegistry::with_route_channel_capacity(1);
let (conn_id, _rx) = reg.register().await;
assert!(matches!(
reg.route_nowait(conn_id, MeResponse::Ack(1)).await,
RouteResult::Routed
));
let routed = route_data_with_retry(&reg, conn_id, 0, Bytes::from_static(b"a"), 0).await;
assert!(matches!(
routed,
RouteResult::QueueFullBase | RouteResult::QueueFullHigh
));
}
}
async fn send_close_conn(tx: &mpsc::Sender<WriterCommand>, conn_id: u64) {
let mut p = Vec::with_capacity(12);
p.extend_from_slice(&RPC_CLOSE_CONN_U32.to_le_bytes());

View File

@@ -55,6 +55,20 @@ struct RoutingTable {
map: DashMap<u64, mpsc::Sender<MeResponse>>,
}
struct WriterTable {
map: DashMap<u64, mpsc::Sender<WriterCommand>>,
}
#[derive(Clone)]
struct HotConnBinding {
writer_id: u64,
meta: ConnMeta,
}
struct HotBindingTable {
map: DashMap<u64, HotConnBinding>,
}
struct BindingState {
inner: Mutex<BindingInner>,
}
@@ -83,6 +97,8 @@ impl BindingInner {
pub struct ConnRegistry {
routing: RoutingTable,
writers: WriterTable,
hot_binding: HotBindingTable,
binding: BindingState,
next_id: AtomicU64,
route_channel_capacity: usize,
@@ -105,6 +121,12 @@ impl ConnRegistry {
routing: RoutingTable {
map: DashMap::new(),
},
writers: WriterTable {
map: DashMap::new(),
},
hot_binding: HotBindingTable {
map: DashMap::new(),
},
binding: BindingState {
inner: Mutex::new(BindingInner::new()),
},
@@ -149,16 +171,18 @@ impl ConnRegistry {
pub async fn register_writer(&self, writer_id: u64, tx: mpsc::Sender<WriterCommand>) {
let mut binding = self.binding.inner.lock().await;
binding.writers.insert(writer_id, tx);
binding.writers.insert(writer_id, tx.clone());
binding
.conns_for_writer
.entry(writer_id)
.or_insert_with(HashSet::new);
self.writers.map.insert(writer_id, tx);
}
/// Unregister connection, returning associated writer_id if any.
pub async fn unregister(&self, id: u64) -> Option<u64> {
self.routing.map.remove(&id);
self.hot_binding.map.remove(&id);
let mut binding = self.binding.inner.lock().await;
binding.meta.remove(&id);
if let Some(writer_id) = binding.writer_for_conn.remove(&id) {
@@ -325,13 +349,16 @@ impl ConnRegistry {
}
binding.meta.insert(conn_id, meta.clone());
binding.last_meta_for_writer.insert(writer_id, meta);
binding.last_meta_for_writer.insert(writer_id, meta.clone());
binding.writer_idle_since_epoch_secs.remove(&writer_id);
binding
.conns_for_writer
.entry(writer_id)
.or_insert_with(HashSet::new)
.insert(conn_id);
self.hot_binding
.map
.insert(conn_id, HotConnBinding { writer_id, meta });
true
}
@@ -392,39 +419,20 @@ impl ConnRegistry {
}
pub async fn get_writer(&self, conn_id: u64) -> Option<ConnWriter> {
let mut binding = self.binding.inner.lock().await;
// ROUTING IS THE SOURCE OF TRUTH:
// stale bindings are ignored and lazily cleaned when routing no longer
// contains the connection.
if !self.routing.map.contains_key(&conn_id) {
binding.meta.remove(&conn_id);
if let Some(stale_writer_id) = binding.writer_for_conn.remove(&conn_id)
&& let Some(conns) = binding.conns_for_writer.get_mut(&stale_writer_id)
{
conns.remove(&conn_id);
if conns.is_empty() {
binding
.writer_idle_since_epoch_secs
.insert(stale_writer_id, Self::now_epoch_secs());
}
}
return None;
}
let writer_id = binding.writer_for_conn.get(&conn_id).copied()?;
let Some(writer) = binding.writers.get(&writer_id).cloned() else {
binding.writer_for_conn.remove(&conn_id);
binding.meta.remove(&conn_id);
if let Some(conns) = binding.conns_for_writer.get_mut(&writer_id) {
conns.remove(&conn_id);
if conns.is_empty() {
binding
.writer_idle_since_epoch_secs
.insert(writer_id, Self::now_epoch_secs());
}
}
return None;
};
let writer_id = self
.hot_binding
.map
.get(&conn_id)
.map(|entry| entry.writer_id)?;
let writer = self
.writers
.map
.get(&writer_id)
.map(|entry| entry.value().clone())?;
Some(ConnWriter {
writer_id,
tx: writer,
@@ -439,6 +447,7 @@ impl ConnRegistry {
pub async fn writer_lost(&self, writer_id: u64) -> Vec<BoundConn> {
let mut binding = self.binding.inner.lock().await;
binding.writers.remove(&writer_id);
self.writers.map.remove(&writer_id);
binding.last_meta_for_writer.remove(&writer_id);
binding.writer_idle_since_epoch_secs.remove(&writer_id);
let conns = binding
@@ -454,6 +463,15 @@ impl ConnRegistry {
continue;
}
binding.writer_for_conn.remove(&conn_id);
let remove_hot = self
.hot_binding
.map
.get(&conn_id)
.map(|hot| hot.writer_id == writer_id)
.unwrap_or(false);
if remove_hot {
self.hot_binding.map.remove(&conn_id);
}
if let Some(m) = binding.meta.get(&conn_id) {
out.push(BoundConn {
conn_id,
@@ -466,8 +484,10 @@ impl ConnRegistry {
#[allow(dead_code)]
pub async fn get_meta(&self, conn_id: u64) -> Option<ConnMeta> {
let binding = self.binding.inner.lock().await;
binding.meta.get(&conn_id).cloned()
self.hot_binding
.map
.get(&conn_id)
.map(|entry| entry.meta.clone())
}
pub async fn is_writer_empty(&self, writer_id: u64) -> bool {
@@ -491,6 +511,7 @@ impl ConnRegistry {
}
binding.writers.remove(&writer_id);
self.writers.map.remove(&writer_id);
binding.last_meta_for_writer.remove(&writer_id);
binding.writer_idle_since_epoch_secs.remove(&writer_id);
binding.conns_for_writer.remove(&writer_id);

View File

@@ -842,6 +842,7 @@ zabbix_export:
name: 'Prometheus metrics'
type: HTTP_AGENT
key: telemt.prom_metrics
history: '0'
value_type: TEXT
trends: '0'
url: '{$TELEMT_URL}'