Compare commits

..

17 Commits

Author SHA1 Message Date
Alexey 32d5cee01c Bump 2026-04-15 02:18:44 +03:00
Alexey 3a17901e83 Reconnect logic for single-endpoint DC + Handling single-endpoint outages + Windows build + Mask timeouts + BINDTODEVICE + Gray Action for API + Beobachten Path + Server.Listeners + Upstream V4/V6 + Server.Listeners + Upstream V4/V6: merge pull request #705 from telemt/flow
Reconnect logic for single-endpoint DC + Handling single-endpoint outages + Windows build + Mask timeouts + BINDTODEVICE + Gray Action for API + Beobachten Path + Server.Listeners + Upstream V4/V6 + Server.Listeners + Upstream V4/V6
2026-04-15 02:02:51 +03:00
Alexey 902a4e83cf Specific scopes for Connectivity by #699 and #700 2026-04-15 01:56:49 +03:00
Alexey 696316f919 Rustfmt 2026-04-15 01:39:47 +03:00
Alexey d7a0319696 Server.Listeners + Upstream V4/V6
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-15 01:32:49 +03:00
Alexey 9303c7854a Merge pull request #701 from groozchique/main
[FAQ] Updated info + section about Telegram DC interaction
2026-04-14 18:05:47 +03:00
Nick Parfyonov afc07345f5 [docs] fix typo in FAQ.en.md 2026-04-14 15:07:44 +03:00
Nick Parfyonov a965b38bd4 [docs] add section about client interaction with Telegram DCs 2026-04-14 14:59:04 +03:00
Nick Parfyonov f0ebbac338 [docs] update information about TLS fingerprint in FAQ
Updated information about TLS fingerprint issue and notice for users to update their clients
2026-04-14 14:26:12 +03:00
Alexey 286662fc51 Merge pull request #697 from TWRoman/main
[docs] Updated QUICK START GUIDEs and READMEs
2026-04-13 19:39:05 +03:00
TWRoman c5390baaf1 Merge branch 'main' of github.com:TWRoman/telemt_docs 2026-04-13 11:15:49 +03:00
TWRoman 1cd1e96079 Fixed server.listeners and upstreams description 2026-04-13 11:14:02 +03:00
Roman 2b995c31b0 Update README.md
Fixed the link for README.ru
2026-04-13 10:20:25 +03:00
Roman 442320302d Update QUICK_START_GUIDE.ru.md 2026-04-13 10:14:39 +03:00
Roman ac0dde567b Update README.ru.md 2026-04-13 10:07:50 +03:00
TWRoman b2fe9b78d8 [docs] Updated READMEs 2026-04-13 10:05:55 +03:00
TWRoman f039ce1827 [docs] Updated QUICK START GUIDES 2026-04-13 09:56:44 +03:00
43 changed files with 643 additions and 130 deletions
Generated
+1 -1
View File
@@ -2780,7 +2780,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]] [[package]]
name = "telemt" name = "telemt"
version = "3.3.39" version = "3.4.0"
dependencies = [ dependencies = [
"aes", "aes",
"anyhow", "anyhow",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "telemt" name = "telemt"
version = "3.3.39" version = "3.4.0"
edition = "2024" edition = "2024"
[features] [features]
+2
View File
@@ -2,6 +2,8 @@
![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon) ![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social) ![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs) ![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon) ![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social) ![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs)
[🇷🇺 README на русском](https://github.com/telemt/telemt/blob/main/README.ru.md)
***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist*** ***Löst Probleme, bevor andere überhaupt wissen, dass sie existieren*** / ***It solves problems before others even realize they exist***
> [!NOTE] > [!NOTE]
+21
View File
@@ -85,4 +85,25 @@ telemt config.toml
- Безопасность памяти; - Безопасность памяти;
- Асинхронная архитектура Tokio. - Асинхронная архитектура Tokio.
## Поддержать Telemt
Telemt — это бесплатное программное обеспечение с открытым исходным кодом, разработанное в свободное время.
Если оно оказалось вам полезным, вы можете поддержать дальнейшую разработку.
Принимаемые криптовалюты (BTC, ETH, USDT, 350+ и другие):
<p align="center">
<a href="https://nowpayments.io/donation?api_key=2bf1afd2-abc2-49f9-a012-f1e715b37223" target="_blank" rel="noreferrer noopener">
<img src="https://nowpayments.io/images/embeds/donation-button-white.svg" alt="Cryptocurrency & Bitcoin donation button by NOWPayments" height="80">
</a>
</p>
Monero (XMR) напрямую:
```
8Bk4tZEYPQWSypeD2hrUXG2rKbAKF16GqEN942ZdAP5cFdSqW6h4DwkP5cJMAdszzuPeHeHZPTyjWWFwzeFdjuci3ktfMoB
```
Все пожертвования пойдут на инфраструктуру, разработку и исследования.
![telemt_scheme](docs/assets/telemt.png) ![telemt_scheme](docs/assets/telemt.png)
+21 -21
View File
@@ -2106,7 +2106,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
- **Example**: - **Example**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
``` ```
## announce ## announce
@@ -2115,7 +2115,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
- **Example**: - **Example**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
announce = "proxy.example.com" announce = "proxy.example.com"
``` ```
@@ -2125,7 +2125,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
- **Example**: - **Example**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
announce_ip = "203.0.113.10" announce_ip = "203.0.113.10"
``` ```
@@ -2138,7 +2138,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
[server] [server]
proxy_protocol = false proxy_protocol = false
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
proxy_protocol = true proxy_protocol = true
``` ```
@@ -2149,7 +2149,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
- **Example**: - **Example**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
reuse_allow = false reuse_allow = false
``` ```
@@ -2931,7 +2931,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
``` ```
# [upstreams] # [[upstreams]]
| Key | Type | Default | | Key | Type | Default |
@@ -2950,18 +2950,18 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
## type ## type
- **Constraints / validation**: Required field. Must be one of: `"direct"`, `"socks4"`, `"socks5"`, `"shadowsocks"`. - **Constraints / validation**: Required field. Must be one of: `"direct"`, `"socks4"`, `"socks5"`, `"shadowsocks"`.
- **Description**: Selects the upstream transport implementation for this `[upstreams]` entry. - **Description**: Selects the upstream transport implementation for this `[[upstreams]]` entry.
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
[upstreams] [[upstreams]]
type = "shadowsocks" type = "shadowsocks"
url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388" url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388"
``` ```
@@ -2971,7 +2971,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
weight = 10 weight = 10
``` ```
@@ -2981,7 +2981,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
enabled = false enabled = false
@@ -2992,7 +2992,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks4" type = "socks4"
address = "10.0.0.10:1080" address = "10.0.0.10:1080"
scopes = "me, fetch, dc2" scopes = "me, fetch, dc2"
@@ -3006,11 +3006,11 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
interface = "eth0" interface = "eth0"
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "203.0.113.10:1080" address = "203.0.113.10:1080"
interface = "192.0.2.10" # explicit local bind IP interface = "192.0.2.10" # explicit local bind IP
@@ -3023,7 +3023,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
bind_addresses = ["192.0.2.10", "192.0.2.11"] bind_addresses = ["192.0.2.10", "192.0.2.11"]
``` ```
@@ -3039,7 +3039,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
[general] [general]
use_middle_proxy = false use_middle_proxy = false
[upstreams] [[upstreams]]
type = "shadowsocks" type = "shadowsocks"
url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388" url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388"
``` ```
@@ -3049,7 +3049,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
``` ```
@@ -3059,7 +3059,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks4" type = "socks4"
address = "127.0.0.1:1080" address = "127.0.0.1:1080"
user_id = "telemt" user_id = "telemt"
@@ -3070,7 +3070,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
username = "alice" username = "alice"
@@ -3081,7 +3081,7 @@ If your backend or network is very bandwidth-constrained, reduce cap first. If p
- **Example**: - **Example**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
username = "alice" username = "alice"
+21 -21
View File
@@ -2112,7 +2112,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
``` ```
## announce ## announce
@@ -2121,7 +2121,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
announce = "proxy.example.com" announce = "proxy.example.com"
``` ```
@@ -2131,7 +2131,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
announce_ip = "203.0.113.10" announce_ip = "203.0.113.10"
``` ```
@@ -2144,7 +2144,7 @@
[server] [server]
proxy_protocol = false proxy_protocol = false
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
proxy_protocol = true proxy_protocol = true
``` ```
@@ -2155,7 +2155,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[server.listeners] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
reuse_allow = false reuse_allow = false
``` ```
@@ -2912,7 +2912,7 @@
``` ```
# [upstreams] # [[upstreams]]
| Ключ | Тип | По умолчанию | | Ключ | Тип | По умолчанию |
@@ -2931,18 +2931,18 @@
## type ## type
- **Ограничения / валидация**: Обязательный параметр.`"direct"`, `"socks4"`, `"socks5"`, `"shadowsocks"`. - **Ограничения / валидация**: Обязательный параметр.`"direct"`, `"socks4"`, `"socks5"`, `"shadowsocks"`.
- **Описание**: Выбирает реализацию upstream-транспорта для этой записи в `[upstreams]`. - **Описание**: Выбирает реализацию upstream-транспорта для этой записи в `[[upstreams]]`.
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
[upstreams] [[upstreams]]
type = "shadowsocks" type = "shadowsocks"
url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388" url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388"
``` ```
@@ -2952,7 +2952,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
weight = 10 weight = 10
``` ```
@@ -2962,7 +2962,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
enabled = false enabled = false
@@ -2973,7 +2973,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks4" type = "socks4"
address = "10.0.0.10:1080" address = "10.0.0.10:1080"
scopes = "me, fetch, dc2" scopes = "me, fetch, dc2"
@@ -2987,11 +2987,11 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
interface = "eth0" interface = "eth0"
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "203.0.113.10:1080" address = "203.0.113.10:1080"
interface = "192.0.2.10" # explicit local bind IP interface = "192.0.2.10" # explicit local bind IP
@@ -3004,7 +3004,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "direct" type = "direct"
bind_addresses = ["192.0.2.10", "192.0.2.11"] bind_addresses = ["192.0.2.10", "192.0.2.11"]
``` ```
@@ -3020,7 +3020,7 @@
[general] [general]
use_middle_proxy = false use_middle_proxy = false
[upstreams] [[upstreams]]
type = "shadowsocks" type = "shadowsocks"
url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388" url = "ss://2022-blake3-aes-256-gcm:BASE64PASSWORD@127.0.0.1:8388"
``` ```
@@ -3030,7 +3030,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
``` ```
@@ -3040,7 +3040,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks4" type = "socks4"
address = "127.0.0.1:1080" address = "127.0.0.1:1080"
user_id = "telemt" user_id = "telemt"
@@ -3051,7 +3051,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
username = "alice" username = "alice"
@@ -3062,7 +3062,7 @@
- **Пример**: - **Пример**:
```toml ```toml
[upstreams] [[upstreams]]
type = "socks5" type = "socks5"
address = "127.0.0.1:9050" address = "127.0.0.1:9050"
username = "alice" username = "alice"
+25 -3
View File
@@ -36,8 +36,11 @@ hello2 = "ad_tag2"
On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS, On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS,
based on the ECH extension and the ordering of cipher suites, based on the ECH extension and the ordering of cipher suites,
as well as an overall unique JA3/JA4 fingerprint as well as an overall unique JA3/JA4 fingerprint
that does not occur in modern browsers: that does not occur in modern browsers.
we have already submitted initial changes to the Telegram Desktop developers and are working on updates for other clients.
> [!IMPORTANT]
> TLS fingerprint has been fixed in latest version of clients for Desktop / Android / iOS.
> Please update your client for MTProxy Fake-TLS to work correctly.
- We consider this a breakthrough aspect, which has no stable analogues today - We consider this a breakthrough aspect, which has no stable analogues today
- Based on this: if `telemt` configured correctly, **TLS mode is completely identical to real-life handshake + communication** with a specified host - Based on this: if `telemt` configured correctly, **TLS mode is completely identical to real-life handshake + communication** with a specified host
@@ -154,6 +157,24 @@ Keep-Alive: timeout=60
### Why do you need a middle proxy (ME) ### Why do you need a middle proxy (ME)
https://github.com/telemt/telemt/discussions/167 https://github.com/telemt/telemt/discussions/167
## How clients interact with Telegram DCs
When you register a Telegram account, it gets permanently bound to one of Telegram's data centers (DCs).
It is deciced beforehand by Telegram based on the phone number's region.
This DC becomes your **home DC**: all content you upload (photos, videos, files, messages) is stored there.
Your client authenticates on it with every connection.
For example, if your account is registered on **DC2**, your client will always connect to DC2 first.
When you open a chat with another user whose home DC is **DC5**, your client opens an additional connection to DC5 to download their media.
Those cross-DC requests are normal and happen constantly.
> [!WARNING]
> Because every session is anchored to your home DC, an outage there causes other DCs to be unavaliable.
> If your home DC is DC2 and DC2 goes down, you **cannot** reach DC5 even though DC5 itself is perfectly healthy.
> The client has no valid session to route the request through.
This is also why an MTProxy only needs to reach Telegram's DC infrastructure as a whole.
The proxy itself doesn't care which DC your account lives on. The client negotiates the correct DC through the proxy after connecting.
### How many people can use one link ### How many people can use one link
By default, an unlimited number of people can use a single link. By default, an unlimited number of people can use a single link.
However, you can limit the number of unique IP addresses for each user: However, you can limit the number of unique IP addresses for each user:
@@ -161,7 +182,8 @@ However, you can limit the number of unique IP addresses for each user:
[access.user_max_unique_ips] [access.user_max_unique_ips]
hello = 1 hello = 1
``` ```
This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect. At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network). This parameter sets the maximum number of unique IP addresses from which a single link can be used simultaneously. If the first user disconnects, a second one can connect.
At the same time, multiple users can connect from a single IP address simultaneously (for example, devices on the same Wi-Fi network).
### How to create multiple different links ### How to create multiple different links
1. Generate the required number of secrets using the command: `openssl rand -hex 16`. 1. Generate the required number of secrets using the command: `openssl rand -hex 16`.
+22 -2
View File
@@ -33,9 +33,12 @@ hello = "ad_tag"
hello2 = "ad_tag2" hello2 = "ad_tag2"
``` ```
## Распознаваемость для DPI и сканеров ## Распознаваемость для DPI и сканеров
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах.
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров, > [!IMPORTANT]
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах: мы уже отправили первоначальные изменения разработчикам Telegram Desktop и работаем над обновлениями для других клиентов. > Проблема с TLS отпечатком исправлена в последних версиях клиентов Telegram для Desktop / Android / iOS.
> Обновите свой клиент для корректной работы с MTProxy Fake-TLS!
- Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов; - Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов;
- Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом; - Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом;
@@ -152,6 +155,23 @@ Keep-Alive: timeout=60
## Зачем нужен middle proxy (ME) ## Зачем нужен middle proxy (ME)
https://github.com/telemt/telemt/discussions/167 https://github.com/telemt/telemt/discussions/167
## Как клиенты взаимодействуют с дата-центрами Telegram
При регистрации аккаунта Telegram он навсегда привязывается к одному из дата-центров (DC).
Telegram заранее определяет к какому DC привязать аккаунт исходя из региона, к которому относиться номер телефона.
Этот DC становится вашим **домашним**: именно там хранится весь контент, который вы загружаете (фото, видео, файлы, сообщения).
И именно на нем клиент авторизуется при каждом подключении.
Например, если ваш аккаунт зарегистрирован на **DC2**, клиент всегда будет подключаться в первую очередь к DC2.
Когда вы открываете переписку с пользователем, чей домашний DC — **DC5**, клиент устанавливает доп. соединение с DC5, чтобы загрузить его контент.
Такие кросс-запросы к DC — это нормальная часть работы Telegram.
> [!WARNING]
> Поскольку аккаунт всегда привязан к домашнему DC, при его падении контент с других DC будет недоступен.
> Если ваш домашний DC — DC2, и DC2 лежит, вы **не сможете** достучаться и до DC5, даже если сам DC5 полностью исправен.
> У клиента просто нет валидной сессии, через которую можно было бы направить запрос.
По той же причине MTProxy достаточно иметь доступ к инфраструктуре Telegram в целом.
Cамому MTProxy всё равно, на каком DC живёт ваш аккаунт. Клиент cам договаривается о нужном DC через прокси уже после подключения.
## Что такое dd и ee в контексте MTProxy? ## Что такое dd и ee в контексте MTProxy?
+27
View File
@@ -1,9 +1,36 @@
# Installation Options
There are three options for installing Telemt:
- [Automated installation using a script](#very-quick-start).
- [Manual installation of Telemt as a service](#telemt-via-systemd).
- [Installation using Docker Compose](#telemt-via-docker-compose).
# Very quick start # Very quick start
### One-command installation / update on re-run ### One-command installation / update on re-run
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
``` ```
After starting, the script will prompt for:
- Your language (1 - English, 2 - Russian);
- Your TLS domain (press Enter for petrovich.ru).
The script checks if the port (default **443**) is free. If the port is already in use, installation will fail. You need to free up the port or use the **-p** flag with a different port to retry the installation.
To modify the scripts startup parameters, you can use the following flags:
- **-d, --domain** - TLS domain;
- **-p, --port** - server port (165535);
- **-s, --secret** - 32 hex secret;
- **-a, --ad-tag** - ad_tag;
- **-l, --lan**g - language (1/en or 2/ru);
Providing all options skips interactive prompts.
After completion, the script will provide a link for client connections:
```bash
tg://proxy?server=IP&port=PORT&secret=SECRET
```
### Installing a specific version ### Installing a specific version
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39 curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
+26
View File
@@ -1,9 +1,35 @@
# Варианты установки
Имеется три варианта установки Telemt:
- [Автоматизированная установка с помощью скрипта](#очень-быстрый-старт).
- [Ручная установка Telemt в качестве службы](#telemt-через-systemd-вручную).
- [Установка через Docker Compose](#telemt-через-docker-compose).
# Очень быстрый старт # Очень быстрый старт
### Установка одной командой / обновление при повторном запуске ### Установка одной командой / обновление при повторном запуске
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh
``` ```
После запуска скрипт запросит:
- ваш язык (1 - English, 2 - Русский);
- ваш TLS-домен (нажмите Enter для petrovich.ru).
Во время установки скрипт проверяет, свободен ли порт (по умолчанию **443**). Если порт занят другим процессом - установка завершится с ошибкой. Для повторной установки необходимо освободить порт или указать другой через флаг **-p**.
Для изменения параметров запуска скрипта можно использовать следующие флаги:
- **-d, --domain** - TLS-домен;
- **-p, --port** - порт (165535);
- **-s, --secret** - секрет (32 hex символа);
- **-a, --ad-tag** - ad_tag;
- **-l, --lang** - язык (1/en или 2/ru).
Если заданы флаги для языка и домена, интерактивных вопросов не будет.
После завершения установки скрипт выдаст ссылку для подключения клиентов:
```bash
tg://proxy?server=IP&port=PORT&secret=SECRET
```
### Установка нужной версии ### Установка нужной версии
```bash ```bash
curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39 curl -fsSL https://raw.githubusercontent.com/telemt/telemt/main/install.sh | sh -s -- 3.3.39
+18 -9
View File
@@ -224,13 +224,11 @@ async fn handle(
"Source IP is not allowed", "Source IP is not allowed",
), ),
)), )),
ApiGrayAction::Ok200 => Ok( ApiGrayAction::Ok200 => Ok(Response::builder()
Response::builder() .status(StatusCode::OK)
.status(StatusCode::OK) .header("content-type", "text/html; charset=utf-8")
.header("content-type", "text/html; charset=utf-8") .body(Full::new(Bytes::new()))
.body(Full::new(Bytes::new())) .unwrap()),
.unwrap(),
),
ApiGrayAction::Drop => Err(IoError::new( ApiGrayAction::Drop => Err(IoError::new(
ErrorKind::ConnectionAborted, ErrorKind::ConnectionAborted,
"api request dropped by gray_action=drop", "api request dropped by gray_action=drop",
@@ -259,11 +257,16 @@ async fn handle(
let method = req.method().clone(); let method = req.method().clone();
let path = req.uri().path().to_string(); let path = req.uri().path().to_string();
let normalized_path = if path.len() > 1 {
path.trim_end_matches('/')
} else {
path.as_str()
};
let query = req.uri().query().map(str::to_string); let query = req.uri().query().map(str::to_string);
let body_limit = api_cfg.request_body_limit_bytes; let body_limit = api_cfg.request_body_limit_bytes;
let result: Result<Response<Full<Bytes>>, ApiFailure> = async { let result: Result<Response<Full<Bytes>>, ApiFailure> = async {
match (method.as_str(), path.as_str()) { match (method.as_str(), normalized_path) {
("GET", "/v1/health") => { ("GET", "/v1/health") => {
let revision = current_revision(&shared.config_path).await?; let revision = current_revision(&shared.config_path).await?;
let data = HealthData { let data = HealthData {
@@ -446,7 +449,7 @@ async fn handle(
Ok(success_response(status, data, revision)) Ok(success_response(status, data, revision))
} }
_ => { _ => {
if let Some(user) = path.strip_prefix("/v1/users/") if let Some(user) = normalized_path.strip_prefix("/v1/users/")
&& !user.is_empty() && !user.is_empty()
&& !user.contains('/') && !user.contains('/')
{ {
@@ -615,6 +618,12 @@ async fn handle(
), ),
)); ));
} }
debug!(
method = method.as_str(),
path = %path,
normalized_path = %normalized_path,
"API route not found"
);
Ok(error_response( Ok(error_response(
request_id, request_id,
ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"), ApiFailure::new(StatusCode::NOT_FOUND, "not_found", "Route not found"),
+13 -1
View File
@@ -452,7 +452,11 @@ fn build_user_links(
startup_detected_ip_v6: Option<IpAddr>, startup_detected_ip_v6: Option<IpAddr>,
) -> UserLinks { ) -> UserLinks {
let hosts = resolve_link_hosts(cfg, startup_detected_ip_v4, startup_detected_ip_v6); let hosts = resolve_link_hosts(cfg, startup_detected_ip_v4, startup_detected_ip_v6);
let port = cfg.general.links.public_port.unwrap_or(cfg.server.port); let port = cfg
.general
.links
.public_port
.unwrap_or(resolve_default_link_port(cfg));
let tls_domains = resolve_tls_domains(cfg); let tls_domains = resolve_tls_domains(cfg);
let mut classic = Vec::new(); let mut classic = Vec::new();
@@ -490,6 +494,14 @@ fn build_user_links(
} }
} }
fn resolve_default_link_port(cfg: &ProxyConfig) -> u16 {
cfg.server
.listeners
.first()
.and_then(|listener| listener.port)
.unwrap_or(cfg.server.port)
}
fn resolve_link_hosts( fn resolve_link_hosts(
cfg: &ProxyConfig, cfg: &ProxyConfig,
startup_detected_ip_v4: Option<IpAddr>, startup_detected_ip_v4: Option<IpAddr>,
+2 -1
View File
@@ -598,16 +598,17 @@ secure = false
tls = true tls = true
[server] [server]
port = {port}
listen_addr_ipv4 = "0.0.0.0" listen_addr_ipv4 = "0.0.0.0"
listen_addr_ipv6 = "::" listen_addr_ipv6 = "::"
[[server.listeners]] [[server.listeners]]
ip = "0.0.0.0" ip = "0.0.0.0"
port = {port}
# reuse_allow = false # Set true only when intentionally running multiple telemt instances on same port # reuse_allow = false # Set true only when intentionally running multiple telemt instances on same port
[[server.listeners]] [[server.listeners]]
ip = "::" ip = "::"
port = {port}
[timeouts] [timeouts]
client_first_byte_idle_secs = 300 client_first_byte_idle_secs = 300
+13 -3
View File
@@ -17,8 +17,9 @@
//! | `network` | `dns_overrides` | Applied immediately | //! | `network` | `dns_overrides` | Applied immediately |
//! | `access` | All user/quota fields | Effective immediately | //! | `access` | All user/quota fields | Effective immediately |
//! //!
//! Fields that require re-binding sockets (`server.port`, `censorship.*`, //! Fields that require re-binding sockets (`server.listeners`, legacy
//! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted. //! `server.port`, `censorship.*`, `network.*`, `use_middle_proxy`) are **not**
//! applied; a warning is emitted.
//! Non-hot changes are never mixed into the runtime config snapshot. //! Non-hot changes are never mixed into the runtime config snapshot.
use std::collections::BTreeSet; use std::collections::BTreeSet;
@@ -299,6 +300,7 @@ fn listeners_equal(
} }
lhs.iter().zip(rhs.iter()).all(|(a, b)| { lhs.iter().zip(rhs.iter()).all(|(a, b)| {
a.ip == b.ip a.ip == b.ip
&& a.port == b.port
&& a.announce == b.announce && a.announce == b.announce
&& a.announce_ip == b.announce_ip && a.announce_ip == b.announce_ip
&& a.proxy_protocol == b.proxy_protocol && a.proxy_protocol == b.proxy_protocol
@@ -306,6 +308,14 @@ fn listeners_equal(
}) })
} }
fn resolve_default_link_port(cfg: &ProxyConfig) -> u16 {
cfg.server
.listeners
.first()
.and_then(|listener| listener.port)
.unwrap_or(cfg.server.port)
}
#[derive(Debug, Clone, Default, PartialEq, Eq)] #[derive(Debug, Clone, Default, PartialEq, Eq)]
struct WatchManifest { struct WatchManifest {
files: BTreeSet<PathBuf>, files: BTreeSet<PathBuf>,
@@ -1120,7 +1130,7 @@ fn log_changes(
.general .general
.links .links
.public_port .public_port
.unwrap_or(new_cfg.server.port); .unwrap_or(resolve_default_link_port(new_cfg));
for user in &added { for user in &added {
if let Some(secret) = new_hot.users.get(*user) { if let Some(secret) = new_hot.users.get(*user) {
print_user_links(user, secret, &host, port, new_cfg); print_user_links(user, secret, &host, port, new_cfg);
+19 -6
View File
@@ -253,6 +253,12 @@ fn validate_upstreams(config: &ProxyConfig) -> Result<()> {
} }
for upstream in &config.upstreams { for upstream in &config.upstreams {
if matches!(upstream.ipv4, Some(false)) && matches!(upstream.ipv6, Some(false)) {
return Err(ProxyError::Config(
"upstream.ipv4 and upstream.ipv6 cannot both be false".to_string(),
));
}
if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type { if let UpstreamType::Shadowsocks { url, .. } = &upstream.upstream_type {
let parsed = ShadowsocksServerConfig::from_url(url) let parsed = ShadowsocksServerConfig::from_url(url)
.map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?; .map_err(|error| ProxyError::Config(format!("invalid shadowsocks url: {error}")))?;
@@ -378,9 +384,7 @@ impl ProxyConfig {
// Backward compatibility: legacy top-level beobachten* keys. // Backward compatibility: legacy top-level beobachten* keys.
// Prefer `[general].*` when both are present. // Prefer `[general].*` when both are present.
let mut legacy_beobachten_applied = false; let mut legacy_beobachten_applied = false;
if !beobachten_is_explicit if !beobachten_is_explicit && let Some(value) = legacy_top_level_beobachten.as_ref() {
&& let Some(value) = legacy_top_level_beobachten.as_ref()
{
let parsed = value.as_bool().ok_or_else(|| { let parsed = value.as_bool().ok_or_else(|| {
ProxyError::Config("beobachten (top-level) must be a boolean".to_string()) ProxyError::Config("beobachten (top-level) must be a boolean".to_string())
})?; })?;
@@ -427,9 +431,7 @@ impl ProxyConfig {
legacy_beobachten_applied = true; legacy_beobachten_applied = true;
} }
if legacy_beobachten_applied { if legacy_beobachten_applied {
warn!( warn!("top-level beobachten* keys are deprecated; use general.beobachten* instead");
"top-level beobachten* keys are deprecated; use general.beobachten* instead"
);
} }
let legacy_nat_stun = config.general.middle_proxy_nat_stun.take(); let legacy_nat_stun = config.general.middle_proxy_nat_stun.take();
@@ -1324,6 +1326,7 @@ impl ProxyConfig {
if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() { if let Ok(ipv4) = ipv4_str.parse::<IpAddr>() {
config.server.listeners.push(ListenerConfig { config.server.listeners.push(ListenerConfig {
ip: ipv4, ip: ipv4,
port: Some(config.server.port),
announce: None, announce: None,
announce_ip: None, announce_ip: None,
proxy_protocol: None, proxy_protocol: None,
@@ -1335,6 +1338,7 @@ impl ProxyConfig {
{ {
config.server.listeners.push(ListenerConfig { config.server.listeners.push(ListenerConfig {
ip: ipv6, ip: ipv6,
port: Some(config.server.port),
announce: None, announce: None,
announce_ip: None, announce_ip: None,
proxy_protocol: None, proxy_protocol: None,
@@ -1343,6 +1347,13 @@ impl ProxyConfig {
} }
} }
// Migration: listeners[].port fallback to legacy server.port.
for listener in &mut config.server.listeners {
if listener.port.is_none() {
listener.port = Some(config.server.port);
}
}
// Migration: announce_ip → announce for each listener. // Migration: announce_ip → announce for each listener.
for listener in &mut config.server.listeners { for listener in &mut config.server.listeners {
if listener.announce.is_none() if listener.announce.is_none()
@@ -1369,6 +1380,8 @@ impl ProxyConfig {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}); });
} }
+15 -1
View File
@@ -1153,7 +1153,8 @@ pub struct LinksConfig {
#[serde(default)] #[serde(default)]
pub public_host: Option<String>, pub public_host: Option<String>,
/// Public port for tg:// link generation (overrides server.port). /// Public port for tg:// link generation.
/// Overrides listener ports and legacy `server.port`.
#[serde(default)] #[serde(default)]
pub public_port: Option<u16>, pub public_port: Option<u16>,
} }
@@ -1375,6 +1376,8 @@ impl Default for ConntrackControlConfig {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig { pub struct ServerConfig {
/// Legacy listener port used for backward compatibility.
/// For new configs prefer `[[server.listeners]].port`.
#[serde(default = "default_port")] #[serde(default = "default_port")]
pub port: u16, pub port: u16,
@@ -1917,11 +1920,22 @@ pub struct UpstreamConfig {
pub scopes: String, pub scopes: String,
#[serde(skip)] #[serde(skip)]
pub selected_scope: String, pub selected_scope: String,
/// Allow IPv4 DC targets for this upstream.
/// `None` means auto-detect from runtime connectivity state.
#[serde(default)]
pub ipv4: Option<bool>,
/// Allow IPv6 DC targets for this upstream.
/// `None` means auto-detect from runtime connectivity state.
#[serde(default)]
pub ipv6: Option<bool>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListenerConfig { pub struct ListenerConfig {
pub ip: IpAddr, pub ip: IpAddr,
/// Per-listener TCP port. If omitted, falls back to legacy `server.port`.
#[serde(default)]
pub port: Option<u16>,
/// IP address or hostname to announce in proxy links. /// IP address or hostname to announce in proxy links.
/// Takes precedence over `announce_ip` if both are set. /// Takes precedence over `announce_ip` if both are set.
#[serde(default)] #[serde(default)]
+40 -21
View File
@@ -343,15 +343,28 @@ fn command_exists(binary: &str) -> bool {
}) })
} }
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr>>) { fn listener_port_set(cfg: &ProxyConfig) -> Vec<u16> {
let mut ports: BTreeSet<u16> = BTreeSet::new();
if cfg.server.listeners.is_empty() {
ports.insert(cfg.server.port);
} else {
for listener in &cfg.server.listeners {
ports.insert(listener.port.unwrap_or(cfg.server.port));
}
}
ports.into_iter().collect()
}
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<(Option<IpAddr>, u16)>, Vec<(Option<IpAddr>, u16)>) {
let mode = cfg.server.conntrack_control.mode; let mode = cfg.server.conntrack_control.mode;
let mut v4_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new(); let mut v4_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
let mut v6_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new(); let mut v6_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
match mode { match mode {
ConntrackMode::Tracked => {} ConntrackMode::Tracked => {}
ConntrackMode::Notrack => { ConntrackMode::Notrack => {
if cfg.server.listeners.is_empty() { if cfg.server.listeners.is_empty() {
let port = cfg.server.port;
if let Some(ipv4) = cfg if let Some(ipv4) = cfg
.server .server
.listen_addr_ipv4 .listen_addr_ipv4
@@ -359,9 +372,9 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr
.and_then(|s| s.parse::<IpAddr>().ok()) .and_then(|s| s.parse::<IpAddr>().ok())
{ {
if ipv4.is_unspecified() { if ipv4.is_unspecified() {
v4_targets.insert(None); v4_targets.insert((None, port));
} else { } else {
v4_targets.insert(Some(ipv4)); v4_targets.insert((Some(ipv4), port));
} }
} }
if let Some(ipv6) = cfg if let Some(ipv6) = cfg
@@ -371,33 +384,39 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr
.and_then(|s| s.parse::<IpAddr>().ok()) .and_then(|s| s.parse::<IpAddr>().ok())
{ {
if ipv6.is_unspecified() { if ipv6.is_unspecified() {
v6_targets.insert(None); v6_targets.insert((None, port));
} else { } else {
v6_targets.insert(Some(ipv6)); v6_targets.insert((Some(ipv6), port));
} }
} }
} else { } else {
for listener in &cfg.server.listeners { for listener in &cfg.server.listeners {
let port = listener.port.unwrap_or(cfg.server.port);
if listener.ip.is_ipv4() { if listener.ip.is_ipv4() {
if listener.ip.is_unspecified() { if listener.ip.is_unspecified() {
v4_targets.insert(None); v4_targets.insert((None, port));
} else { } else {
v4_targets.insert(Some(listener.ip)); v4_targets.insert((Some(listener.ip), port));
} }
} else if listener.ip.is_unspecified() { } else if listener.ip.is_unspecified() {
v6_targets.insert(None); v6_targets.insert((None, port));
} else { } else {
v6_targets.insert(Some(listener.ip)); v6_targets.insert((Some(listener.ip), port));
} }
} }
} }
} }
ConntrackMode::Hybrid => { ConntrackMode::Hybrid => {
let ports = listener_port_set(cfg);
for ip in &cfg.server.conntrack_control.hybrid_listener_ips { for ip in &cfg.server.conntrack_control.hybrid_listener_ips {
if ip.is_ipv4() { if ip.is_ipv4() {
v4_targets.insert(Some(*ip)); for port in &ports {
v4_targets.insert((Some(*ip), *port));
}
} else { } else {
v6_targets.insert(Some(*ip)); for port in &ports {
v6_targets.insert((Some(*ip), *port));
}
} }
} }
} }
@@ -422,19 +441,19 @@ async fn apply_nft_rules(cfg: &ProxyConfig) -> Result<(), String> {
let (v4_targets, v6_targets) = notrack_targets(cfg); let (v4_targets, v6_targets) = notrack_targets(cfg);
let mut rules = Vec::new(); let mut rules = Vec::new();
for ip in v4_targets { for (ip, port) in v4_targets {
let rule = if let Some(ip) = ip { let rule = if let Some(ip) = ip {
format!("tcp dport {} ip daddr {} notrack", cfg.server.port, ip) format!("tcp dport {} ip daddr {} notrack", port, ip)
} else { } else {
format!("tcp dport {} notrack", cfg.server.port) format!("tcp dport {} notrack", port)
}; };
rules.push(rule); rules.push(rule);
} }
for ip in v6_targets { for (ip, port) in v6_targets {
let rule = if let Some(ip) = ip { let rule = if let Some(ip) = ip {
format!("tcp dport {} ip6 daddr {} notrack", cfg.server.port, ip) format!("tcp dport {} ip6 daddr {} notrack", port, ip)
} else { } else {
format!("tcp dport {} notrack", cfg.server.port) format!("tcp dport {} notrack", port)
}; };
rules.push(rule); rules.push(rule);
} }
@@ -498,7 +517,7 @@ async fn apply_iptables_rules_for_binary(
let (v4_targets, v6_targets) = notrack_targets(cfg); let (v4_targets, v6_targets) = notrack_targets(cfg);
let selected = if ipv4 { v4_targets } else { v6_targets }; let selected = if ipv4 { v4_targets } else { v6_targets };
for ip in selected { for (ip, port) in selected {
let mut args = vec![ let mut args = vec![
"-t".to_string(), "-t".to_string(),
"raw".to_string(), "raw".to_string(),
@@ -507,7 +526,7 @@ async fn apply_iptables_rules_for_binary(
"-p".to_string(), "-p".to_string(),
"tcp".to_string(), "tcp".to_string(),
"--dport".to_string(), "--dport".to_string(),
cfg.server.port.to_string(), port.to_string(),
]; ];
if let Some(ip) = ip { if let Some(ip) = ip {
args.push("-d".to_string()); args.push("-d".to_string());
+18 -8
View File
@@ -31,6 +31,19 @@ pub(crate) struct BoundListeners {
pub(crate) has_unix_listener: bool, pub(crate) has_unix_listener: bool,
} }
fn listener_port_or_legacy(listener: &crate::config::ListenerConfig, config: &ProxyConfig) -> u16 {
listener.port.unwrap_or(config.server.port)
}
fn default_link_port(config: &ProxyConfig) -> u16 {
config
.server
.listeners
.first()
.and_then(|listener| listener.port)
.unwrap_or(config.server.port)
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) async fn bind_listeners( pub(crate) async fn bind_listeners(
config: &Arc<ProxyConfig>, config: &Arc<ProxyConfig>,
@@ -63,7 +76,8 @@ pub(crate) async fn bind_listeners(
let mut listeners = Vec::new(); let mut listeners = Vec::new();
for listener_conf in &config.server.listeners { for listener_conf in &config.server.listeners {
let addr = SocketAddr::new(listener_conf.ip, config.server.port); let listener_port = listener_port_or_legacy(listener_conf, config);
let addr = SocketAddr::new(listener_conf.ip, listener_port);
if addr.is_ipv4() && !decision_ipv4_dc { if addr.is_ipv4() && !decision_ipv4_dc {
warn!(%addr, "Skipping IPv4 listener: IPv4 disabled by [network]"); warn!(%addr, "Skipping IPv4 listener: IPv4 disabled by [network]");
continue; continue;
@@ -106,11 +120,7 @@ pub(crate) async fn bind_listeners(
if config.general.links.public_host.is_none() if config.general.links.public_host.is_none()
&& !config.general.links.show.is_empty() && !config.general.links.show.is_empty()
{ {
let link_port = config let link_port = config.general.links.public_port.unwrap_or(listener_port);
.general
.links
.public_port
.unwrap_or(config.server.port);
print_proxy_links(&public_host, link_port, config); print_proxy_links(&public_host, link_port, config);
} }
@@ -158,7 +168,7 @@ pub(crate) async fn bind_listeners(
.general .general
.links .links
.public_port .public_port
.unwrap_or(config.server.port), .unwrap_or(default_link_port(config)),
) )
} else { } else {
let ip = detected_ip_v4.or(detected_ip_v6).map(|ip| ip.to_string()); let ip = detected_ip_v4.or(detected_ip_v6).map(|ip| ip.to_string());
@@ -173,7 +183,7 @@ pub(crate) async fn bind_listeners(
.general .general
.links .links
.public_port .public_port
.unwrap_or(config.server.port), .unwrap_or(default_link_port(config)),
) )
}; };
+4 -6
View File
@@ -83,7 +83,9 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
// Shared maestro startup and main loop. `drop_after_bind` runs on Unix after listeners are bound // Shared maestro startup and main loop. `drop_after_bind` runs on Unix after listeners are bound
// (for privilege drop); it is a no-op on other platforms. // (for privilege drop); it is a no-op on other platforms.
async fn run_telemt_core(drop_after_bind: impl FnOnce()) -> std::result::Result<(), Box<dyn std::error::Error>> { async fn run_telemt_core(
drop_after_bind: impl FnOnce(),
) -> std::result::Result<(), Box<dyn std::error::Error>> {
let process_started_at = Instant::now(); let process_started_at = Instant::now();
let process_started_at_epoch_secs = SystemTime::now() let process_started_at_epoch_secs = SystemTime::now()
.duration_since(UNIX_EPOCH) .duration_since(UNIX_EPOCH)
@@ -819,11 +821,7 @@ async fn run_inner(
run_telemt_core(|| { run_telemt_core(|| {
if user.is_some() || group.is_some() { if user.is_some() || group.is_some() {
if let Err(e) = drop_privileges( if let Err(e) = drop_privileges(user.as_deref(), group.as_deref(), _pid_file.as_ref()) {
user.as_deref(),
group.as_deref(),
_pid_file.as_ref(),
) {
error!(error = %e, "Failed to drop privileges"); error!(error = %e, "Failed to drop privileges");
std::process::exit(1); std::process::exit(1);
} }
@@ -37,6 +37,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -33,6 +33,8 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -31,6 +31,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -44,6 +44,8 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -22,6 +22,8 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -45,6 +45,8 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -236,6 +238,8 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -478,6 +482,8 @@ async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sen
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -553,6 +559,8 @@ async fn capture_forwarded_probe_len(tls_len: u16, body_sent: usize) -> usize {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -19,6 +19,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -17,6 +17,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -31,6 +31,8 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
+54
View File
@@ -338,6 +338,8 @@ async fn relay_task_abort_releases_user_gate_and_ip_reservation() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -453,6 +455,8 @@ async fn relay_cutover_releases_user_gate_and_ip_reservation() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -578,6 +582,8 @@ async fn integration_route_cutover_and_quota_overlap_fails_closed_and_releases_s
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -749,6 +755,8 @@ async fn proxy_protocol_header_is_rejected_when_trust_list_is_empty() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -827,6 +835,8 @@ async fn proxy_protocol_header_from_untrusted_peer_range_is_rejected_under_load(
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -988,6 +998,8 @@ async fn short_tls_probe_is_masked_through_client_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1077,6 +1089,8 @@ async fn tls12_record_probe_is_masked_through_client_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1164,6 +1178,8 @@ async fn handle_client_stream_increments_connects_all_exactly_once() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1258,6 +1274,8 @@ async fn running_client_handler_increments_connects_all_exactly_once() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1349,6 +1367,8 @@ async fn idle_pooled_connection_closes_cleanly_in_generic_stream_path() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1421,6 +1441,8 @@ async fn idle_pooled_connection_closes_cleanly_in_client_handler_path() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1508,6 +1530,8 @@ async fn partial_tls_header_stall_triggers_handshake_timeout() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1834,6 +1858,8 @@ async fn valid_tls_path_does_not_fall_back_to_mask_backend() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1944,6 +1970,8 @@ async fn valid_tls_with_invalid_mtproto_falls_back_to_mask_backend() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2052,6 +2080,8 @@ async fn client_handler_tls_bad_mtproto_is_forwarded_to_mask_backend() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2175,6 +2205,8 @@ async fn alpn_mismatch_tls_probe_is_masked_through_client_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2269,6 +2301,8 @@ async fn invalid_hmac_tls_probe_is_masked_through_client_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -2369,6 +2403,8 @@ async fn burst_invalid_tls_probes_are_masked_verbatim() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -3275,6 +3311,8 @@ async fn relay_connect_error_releases_user_and_ip_before_return() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -3837,6 +3875,8 @@ async fn untrusted_proxy_header_source_is_rejected() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -3908,6 +3948,8 @@ async fn empty_proxy_trusted_cidrs_rejects_proxy_header_by_default() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4006,6 +4048,8 @@ async fn oversized_tls_record_is_masked_in_generic_stream_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4110,6 +4154,8 @@ async fn oversized_tls_record_is_masked_in_client_handler_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4228,6 +4274,8 @@ async fn tls_record_len_min_minus_1_is_rejected_in_generic_stream_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4332,6 +4380,8 @@ async fn tls_record_len_min_minus_1_is_rejected_in_client_handler_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4439,6 +4489,8 @@ async fn tls_record_len_16384_is_accepted_in_generic_stream_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -4541,6 +4593,8 @@ async fn tls_record_len_16384_is_accepted_in_client_handler_pipeline() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -30,6 +30,8 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -32,6 +32,8 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -33,6 +33,8 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -47,6 +47,8 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1299,6 +1299,8 @@ async fn direct_relay_abort_midflight_releases_route_gauge() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1407,6 +1409,8 @@ async fn direct_relay_cutover_midflight_releases_route_gauge() {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1530,6 +1534,8 @@ async fn direct_relay_cutover_storm_multi_session_keeps_generic_errors_and_relea
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
@@ -1764,6 +1770,8 @@ async fn negative_direct_relay_dc_connection_refused_fails_fast() {
bindtodevice: None, bindtodevice: None,
}, },
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
100, 100,
@@ -1856,6 +1864,8 @@ async fn adversarial_direct_relay_cutover_integrity() {
bindtodevice: None, bindtodevice: None,
}, },
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
100, 100,
@@ -59,6 +59,8 @@ fn new_client_harness() -> ClientHarness {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
1, 1,
+222 -25
View File
@@ -329,6 +329,17 @@ pub struct UpstreamManager {
} }
impl UpstreamManager { impl UpstreamManager {
fn is_unscoped_upstream(upstream: &UpstreamConfig) -> bool {
upstream.scopes.is_empty()
}
fn should_check_in_default_dc_connectivity(
has_unscoped: bool,
upstream: &UpstreamConfig,
) -> bool {
!has_unscoped || Self::is_unscoped_upstream(upstream)
}
pub fn new( pub fn new(
configs: Vec<UpstreamConfig>, configs: Vec<UpstreamConfig>,
connect_retry_attempts: u32, connect_retry_attempts: u32,
@@ -455,6 +466,87 @@ impl UpstreamManager {
} }
} }
fn resolve_probe_dc_families(
upstream: &UpstreamConfig,
ipv4_available: bool,
ipv6_available: bool,
) -> (bool, bool) {
(
upstream.ipv4.unwrap_or(ipv4_available),
upstream.ipv6.unwrap_or(ipv6_available),
)
}
fn resolve_runtime_dc_families(
upstream: &UpstreamConfig,
dc_preference: IpPreference,
) -> (bool, bool) {
let (auto_ipv4, auto_ipv6) = match dc_preference {
IpPreference::PreferV4 => (true, false),
IpPreference::PreferV6 => (false, true),
IpPreference::BothWork | IpPreference::Unknown | IpPreference::Unavailable => {
(true, true)
}
};
(
upstream.ipv4.unwrap_or(auto_ipv4),
upstream.ipv6.unwrap_or(auto_ipv6),
)
}
fn dc_table_addr(dc_idx: i16, ipv6: bool, port: u16) -> Option<SocketAddr> {
let arr_idx = UpstreamState::dc_array_idx(dc_idx)?;
let ip = if ipv6 {
TG_DATACENTERS_V6[arr_idx]
} else {
TG_DATACENTERS_V4[arr_idx]
};
Some(SocketAddr::new(ip, port))
}
fn resolve_runtime_dc_target(
target: SocketAddr,
dc_idx: Option<i16>,
upstream: &UpstreamConfig,
dc_preference: IpPreference,
) -> Result<SocketAddr> {
let (allow_ipv4, allow_ipv6) = Self::resolve_runtime_dc_families(upstream, dc_preference);
if (target.is_ipv4() && allow_ipv4) || (target.is_ipv6() && allow_ipv6) {
return Ok(target);
}
if !allow_ipv4 && !allow_ipv6 {
return Err(ProxyError::Config(format!(
"Upstream DC family policy blocks all families for target {target}"
)));
}
let Some(dc_idx) = dc_idx else {
return Err(ProxyError::Config(format!(
"Upstream DC family policy cannot remap target {target} without dc_idx"
)));
};
let remapped = if target.is_ipv4() {
if allow_ipv6 {
Self::dc_table_addr(dc_idx, true, target.port())
} else {
None
}
} else if allow_ipv4 {
Self::dc_table_addr(dc_idx, false, target.port())
} else {
None
};
remapped.ok_or_else(|| {
ProxyError::Config(format!(
"Upstream DC family policy rejected target {target} (dc_idx={dc_idx})"
))
})
}
#[cfg(unix)] #[cfg(unix)]
fn resolve_interface_addrs(name: &str, want_ipv6: bool) -> Vec<IpAddr> { fn resolve_interface_addrs(name: &str, want_ipv6: bool) -> Vec<IpAddr> {
use nix::ifaddrs::getifaddrs; use nix::ifaddrs::getifaddrs;
@@ -728,18 +820,28 @@ impl UpstreamManager {
.await .await
.ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?; .ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?;
let mut upstream = { let (mut upstream, bind_rr, dc_preference) = {
let guard = self.upstreams.read().await; let guard = self.upstreams.read().await;
guard[idx].config.clone() let state = &guard[idx];
let dc_preference = dc_idx
.and_then(UpstreamState::dc_array_idx)
.map(|dc_array_idx| state.dc_ip_pref[dc_array_idx])
.unwrap_or(IpPreference::Unknown);
(
state.config.clone(),
Some(state.bind_rr.clone()),
dc_preference,
)
}; };
if let Some(s) = scope { if let Some(s) = scope {
upstream.selected_scope = s.to_string(); upstream.selected_scope = s.to_string();
} }
let bind_rr = { let target = if dc_idx.is_some() {
let guard = self.upstreams.read().await; Self::resolve_runtime_dc_target(target, dc_idx, &upstream, dc_preference)?
guard.get(idx).map(|u| u.bind_rr.clone()) } else {
target
}; };
let (stream, _) = self let (stream, _) = self
@@ -760,9 +862,18 @@ impl UpstreamManager {
.await .await
.ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?; .ok_or_else(|| ProxyError::Config("No upstreams available".to_string()))?;
let mut upstream = { let (mut upstream, bind_rr, dc_preference) = {
let guard = self.upstreams.read().await; let guard = self.upstreams.read().await;
guard[idx].config.clone() let state = &guard[idx];
let dc_preference = dc_idx
.and_then(UpstreamState::dc_array_idx)
.map(|dc_array_idx| state.dc_ip_pref[dc_array_idx])
.unwrap_or(IpPreference::Unknown);
(
state.config.clone(),
Some(state.bind_rr.clone()),
dc_preference,
)
}; };
// Set scope for configuration copy // Set scope for configuration copy
@@ -770,9 +881,10 @@ impl UpstreamManager {
upstream.selected_scope = s.to_string(); upstream.selected_scope = s.to_string();
} }
let bind_rr = { let target = if dc_idx.is_some() {
let guard = self.upstreams.read().await; Self::resolve_runtime_dc_target(target, dc_idx, &upstream, dc_preference)?
guard.get(idx).map(|u| u.bind_rr.clone()) } else {
target
}; };
let (stream, egress) = self let (stream, egress) = self
@@ -1208,10 +1320,21 @@ impl UpstreamManager {
.map(|(i, u)| (i, u.config.clone(), u.bind_rr.clone())) .map(|(i, u)| (i, u.config.clone(), u.bind_rr.clone()))
.collect() .collect()
}; };
let has_unscoped = upstreams
.iter()
.any(|(_, cfg, _)| Self::is_unscoped_upstream(cfg));
let mut all_results = Vec::new(); let mut all_results = Vec::new();
for (upstream_idx, upstream_config, bind_rr) in &upstreams { for (upstream_idx, upstream_config, bind_rr) in &upstreams {
// DC connectivity checks should follow the default routing path.
// Scoped upstreams are included only when no unscoped upstream exists.
if !Self::should_check_in_default_dc_connectivity(has_unscoped, upstream_config) {
continue;
}
let (upstream_ipv4_enabled, upstream_ipv6_enabled) =
Self::resolve_probe_dc_families(upstream_config, ipv4_enabled, ipv6_enabled);
let upstream_name = match &upstream_config.upstream_type { let upstream_name = match &upstream_config.upstream_type {
UpstreamType::Direct { UpstreamType::Direct {
interface, interface,
@@ -1244,7 +1367,7 @@ impl UpstreamManager {
}; };
let mut v6_results = Vec::with_capacity(NUM_DCS); let mut v6_results = Vec::with_capacity(NUM_DCS);
if ipv6_enabled { if upstream_ipv6_enabled {
for dc_zero_idx in 0..NUM_DCS { for dc_zero_idx in 0..NUM_DCS {
let dc_v6 = TG_DATACENTERS_V6[dc_zero_idx]; let dc_v6 = TG_DATACENTERS_V6[dc_zero_idx];
let addr_v6 = SocketAddr::new(dc_v6, TG_DATACENTER_PORT); let addr_v6 = SocketAddr::new(dc_v6, TG_DATACENTER_PORT);
@@ -1295,13 +1418,17 @@ impl UpstreamManager {
dc_idx: dc_zero_idx + 1, dc_idx: dc_zero_idx + 1,
dc_addr: SocketAddr::new(dc_v6, TG_DATACENTER_PORT), dc_addr: SocketAddr::new(dc_v6, TG_DATACENTER_PORT),
rtt_ms: None, rtt_ms: None,
error: Some("ipv6 disabled".to_string()), error: Some(if ipv6_enabled {
"ipv6 disabled by upstream policy".to_string()
} else {
"ipv6 disabled".to_string()
}),
}); });
} }
} }
let mut v4_results = Vec::with_capacity(NUM_DCS); let mut v4_results = Vec::with_capacity(NUM_DCS);
if ipv4_enabled { if upstream_ipv4_enabled {
for dc_zero_idx in 0..NUM_DCS { for dc_zero_idx in 0..NUM_DCS {
let dc_v4 = TG_DATACENTERS_V4[dc_zero_idx]; let dc_v4 = TG_DATACENTERS_V4[dc_zero_idx];
let addr_v4 = SocketAddr::new(dc_v4, TG_DATACENTER_PORT); let addr_v4 = SocketAddr::new(dc_v4, TG_DATACENTER_PORT);
@@ -1352,7 +1479,11 @@ impl UpstreamManager {
dc_idx: dc_zero_idx + 1, dc_idx: dc_zero_idx + 1,
dc_addr: SocketAddr::new(dc_v4, TG_DATACENTER_PORT), dc_addr: SocketAddr::new(dc_v4, TG_DATACENTER_PORT),
rtt_ms: None, rtt_ms: None,
error: Some("ipv4 disabled".to_string()), error: Some(if ipv4_enabled {
"ipv4 disabled by upstream policy".to_string()
} else {
"ipv4 disabled".to_string()
}),
}); });
} }
} }
@@ -1372,7 +1503,9 @@ impl UpstreamManager {
match addr_str.parse::<SocketAddr>() { match addr_str.parse::<SocketAddr>() {
Ok(addr) => { Ok(addr) => {
let is_v6 = addr.is_ipv6(); let is_v6 = addr.is_ipv6();
if (is_v6 && !ipv6_enabled) || (!is_v6 && !ipv4_enabled) { if (is_v6 && !upstream_ipv6_enabled)
|| (!is_v6 && !upstream_ipv4_enabled)
{
continue; continue;
} }
let result = tokio::time::timeout( let result = tokio::time::timeout(
@@ -1607,13 +1740,32 @@ impl UpstreamManager {
continue; continue;
} }
let count = self.upstreams.read().await.len(); let target_upstreams: Vec<usize> = {
for i in 0..count { let guard = self.upstreams.read().await;
let has_unscoped = guard
.iter()
.any(|upstream| Self::is_unscoped_upstream(&upstream.config));
guard
.iter()
.enumerate()
.filter(|(_, upstream)| {
Self::should_check_in_default_dc_connectivity(
has_unscoped,
&upstream.config,
)
})
.map(|(idx, _)| idx)
.collect()
};
for i in target_upstreams {
let (config, bind_rr) = { let (config, bind_rr) = {
let guard = self.upstreams.read().await; let guard = self.upstreams.read().await;
let u = &guard[i]; let u = &guard[i];
(u.config.clone(), u.bind_rr.clone()) (u.config.clone(), u.bind_rr.clone())
}; };
let (upstream_ipv4_enabled, upstream_ipv6_enabled) =
Self::resolve_probe_dc_families(&config, ipv4_enabled, ipv6_enabled);
let mut healthy_groups = 0usize; let mut healthy_groups = 0usize;
let mut latency_updates: Vec<(usize, f64)> = Vec::new(); let mut latency_updates: Vec<(usize, f64)> = Vec::new();
@@ -1629,14 +1781,30 @@ impl UpstreamManager {
continue; continue;
} }
let rotation_key = (i, group.dc_idx, is_primary); let filtered_endpoints: Vec<SocketAddr> = endpoints
let start_idx = .iter()
*endpoint_rotation.entry(rotation_key).or_insert(0) % endpoints.len(); .copied()
let mut next_idx = (start_idx + 1) % endpoints.len(); .filter(|endpoint| {
if endpoint.is_ipv4() {
upstream_ipv4_enabled
} else {
upstream_ipv6_enabled
}
})
.collect();
for step in 0..endpoints.len() { if filtered_endpoints.is_empty() {
let endpoint_idx = (start_idx + step) % endpoints.len(); continue;
let endpoint = endpoints[endpoint_idx]; }
let rotation_key = (i, group.dc_idx, is_primary);
let start_idx = *endpoint_rotation.entry(rotation_key).or_insert(0)
% filtered_endpoints.len();
let mut next_idx = (start_idx + 1) % filtered_endpoints.len();
for step in 0..filtered_endpoints.len() {
let endpoint_idx = (start_idx + step) % filtered_endpoints.len();
let endpoint = filtered_endpoints[endpoint_idx];
let start = Instant::now(); let start = Instant::now();
let result = tokio::time::timeout( let result = tokio::time::timeout(
@@ -1655,7 +1823,7 @@ impl UpstreamManager {
Ok(Ok(_stream)) => { Ok(Ok(_stream)) => {
group_ok = true; group_ok = true;
group_rtt_ms = Some(start.elapsed().as_secs_f64() * 1000.0); group_rtt_ms = Some(start.elapsed().as_secs_f64() * 1000.0);
next_idx = (endpoint_idx + 1) % endpoints.len(); next_idx = (endpoint_idx + 1) % filtered_endpoints.len();
break; break;
} }
Ok(Err(e)) => { Ok(Err(e)) => {
@@ -1870,6 +2038,33 @@ mod tests {
assert!(!UpstreamManager::is_hard_connect_error(&error)); assert!(!UpstreamManager::is_hard_connect_error(&error));
} }
#[test]
fn unscoped_selection_detects_default_route_upstream() {
let mut upstream = UpstreamConfig {
upstream_type: UpstreamType::Direct {
interface: None,
bind_addresses: None,
bindtodevice: None,
},
weight: 1,
enabled: true,
scopes: String::new(),
selected_scope: String::new(),
ipv4: None,
ipv6: None,
};
assert!(UpstreamManager::is_unscoped_upstream(&upstream));
upstream.scopes = "local".to_string();
assert!(!UpstreamManager::is_unscoped_upstream(&upstream));
assert!(!UpstreamManager::should_check_in_default_dc_connectivity(
true, &upstream
));
assert!(UpstreamManager::should_check_in_default_dc_connectivity(
false, &upstream
));
}
#[test] #[test]
fn resolve_bind_address_prefers_explicit_bind_ip() { fn resolve_bind_address_prefers_explicit_bind_ip() {
let target = "203.0.113.10:443".parse::<SocketAddr>().unwrap(); let target = "203.0.113.10:443".parse::<SocketAddr>().unwrap();
@@ -1910,6 +2105,8 @@ mod tests {
enabled: true, enabled: true,
scopes: String::new(), scopes: String::new(),
selected_scope: String::new(), selected_scope: String::new(),
ipv4: None,
ipv6: None,
}], }],
1, 1,
100, 100,