mirror of
https://github.com/telemt/telemt.git
synced 2026-06-28 22:01:11 +03:00
Compare commits
3 Commits
3.4.0
..
6c9b5932d8
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c9b5932d8 | |||
| bac9cc01f3 | |||
| d3b0dbd541 |
Generated
+1
-1
@@ -2780,7 +2780,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.4.0"
|
version = "3.3.39"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.4.0"
|
version = "3.3.39"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
|
|
||||||
   [](https://t.me/telemtrs)
|
   [](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]
|
||||||
|
|||||||
@@ -85,25 +85,4 @@ 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
|
|
||||||
```
|
|
||||||
|
|
||||||
Все пожертвования пойдут на инфраструктуру, разработку и исследования.
|
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -1807,8 +1807,7 @@ This document lists all configuration keys accepted by `config.toml`.
|
|||||||
```
|
```
|
||||||
## proxy_protocol_trusted_cidrs
|
## proxy_protocol_trusted_cidrs
|
||||||
- **Constraints / validation**: `IpNetwork[]`.
|
- **Constraints / validation**: `IpNetwork[]`.
|
||||||
- If omitted, defaults to trust-all CIDRs (`0.0.0.0/0` and `::/0`).
|
- If omitted, defaults to an empty list and incoming PROXY headers are rejected.
|
||||||
> In production behind HAProxy/nginx, prefer setting explicit trusted CIDRs instead of relying on this fallback.
|
|
||||||
- If explicitly set to an empty array, all PROXY headers are rejected.
|
- If explicitly set to an empty array, all PROXY headers are rejected.
|
||||||
- **Description**: Trusted source CIDRs allowed to provide PROXY protocol headers (security control).
|
- **Description**: Trusted source CIDRs allowed to provide PROXY protocol headers (security control).
|
||||||
- **Example**:
|
- **Example**:
|
||||||
@@ -2106,7 +2105,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 +2114,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 +2124,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 +2137,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 +2148,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
|
||||||
```
|
```
|
||||||
@@ -2268,40 +2267,37 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
|
|||||||
|
|
||||||
| Key | Type | Default |
|
| Key | Type | Default |
|
||||||
| --- | ---- | ------- |
|
| --- | ---- | ------- |
|
||||||
| [`tls_domain`](#cfg-censorship-tls_domain) | `String` | `"petrovich.ru"` |
|
| [`tls_domain`](#tls_domain) | `String` | `"petrovich.ru"` |
|
||||||
| [`tls_domains`](#cfg-censorship-tls_domains) | `String[]` | `[]` |
|
| [`tls_domains`](#tls_domains) | `String[]` | `[]` |
|
||||||
| [`unknown_sni_action`](#cfg-censorship-unknown_sni_action) | `"drop"`, `"mask"`, `"accept"` | `"drop"` |
|
| [`unknown_sni_action`](#unknown_sni_action) | `"drop"`, `"mask"`, `"accept"` | `"drop"` |
|
||||||
| [`tls_fetch_scope`](#cfg-censorship-tls_fetch_scope) | `String` | `""` |
|
| [`tls_fetch_scope`](#tls_fetch_scope) | `String` | `""` |
|
||||||
| [`tls_fetch`](#cfg-censorship-tls_fetch) | `Table` | built-in defaults |
|
| [`tls_fetch`](#tls_fetch) | `Table` | built-in defaults |
|
||||||
| [`mask`](#cfg-censorship-mask) | `bool` | `true` |
|
| [`mask`](#mask) | `bool` | `true` |
|
||||||
| [`mask_host`](#cfg-censorship-mask_host) | `String` | — |
|
| [`mask_host`](#mask_host) | `String` | — |
|
||||||
| [`mask_port`](#cfg-censorship-mask_port) | `u16` | `443` |
|
| [`mask_port`](#mask_port) | `u16` | `443` |
|
||||||
| [`mask_unix_sock`](#cfg-censorship-mask_unix_sock) | `String` | — |
|
| [`mask_unix_sock`](#mask_unix_sock) | `String` | — |
|
||||||
| [`fake_cert_len`](#cfg-censorship-fake_cert_len) | `usize` | `2048` |
|
| [`fake_cert_len`](#fake_cert_len) | `usize` | `2048` |
|
||||||
| [`tls_emulation`](#cfg-censorship-tls_emulation) | `bool` | `true` |
|
| [`tls_emulation`](#tls_emulation) | `bool` | `true` |
|
||||||
| [`tls_front_dir`](#cfg-censorship-tls_front_dir) | `String` | `"tlsfront"` |
|
| [`tls_front_dir`](#tls_front_dir) | `String` | `"tlsfront"` |
|
||||||
| [`server_hello_delay_min_ms`](#cfg-censorship-server_hello_delay_min_ms) | `u64` | `0` |
|
| [`server_hello_delay_min_ms`](#server_hello_delay_min_ms) | `u64` | `0` |
|
||||||
| [`server_hello_delay_max_ms`](#cfg-censorship-server_hello_delay_max_ms) | `u64` | `0` |
|
| [`server_hello_delay_max_ms`](#server_hello_delay_max_ms) | `u64` | `0` |
|
||||||
| [`tls_new_session_tickets`](#cfg-censorship-tls_new_session_tickets) | `u8` | `0` |
|
| [`tls_new_session_tickets`](#tls_new_session_tickets) | `u8` | `0` |
|
||||||
| [`tls_full_cert_ttl_secs`](#cfg-censorship-tls_full_cert_ttl_secs) | `u64` | `90` |
|
| [`tls_full_cert_ttl_secs`](#tls_full_cert_ttl_secs) | `u64` | `90` |
|
||||||
| [`alpn_enforce`](#cfg-censorship-alpn_enforce) | `bool` | `true` |
|
| [`alpn_enforce`](#alpn_enforce) | `bool` | `true` |
|
||||||
| [`mask_proxy_protocol`](#cfg-censorship-mask_proxy_protocol) | `u8` | `0` |
|
| [`mask_proxy_protocol`](#mask_proxy_protocol) | `u8` | `0` |
|
||||||
| [`mask_shape_hardening`](#cfg-censorship-mask_shape_hardening) | `bool` | `true` |
|
| [`mask_shape_hardening`](#mask_shape_hardening) | `bool` | `true` |
|
||||||
| [`mask_shape_hardening_aggressive_mode`](#cfg-censorship-mask_shape_hardening_aggressive_mode) | `bool` | `false` |
|
| [`mask_shape_hardening_aggressive_mode`](#mask_shape_hardening_aggressive_mode) | `bool` | `false` |
|
||||||
| [`mask_shape_bucket_floor_bytes`](#cfg-censorship-mask_shape_bucket_floor_bytes) | `usize` | `512` |
|
| [`mask_shape_bucket_floor_bytes`](#mask_shape_bucket_floor_bytes) | `usize` | `512` |
|
||||||
| [`mask_shape_bucket_cap_bytes`](#cfg-censorship-mask_shape_bucket_cap_bytes) | `usize` | `4096` |
|
| [`mask_shape_bucket_cap_bytes`](#mask_shape_bucket_cap_bytes) | `usize` | `4096` |
|
||||||
| [`mask_shape_above_cap_blur`](#cfg-censorship-mask_shape_above_cap_blur) | `bool` | `false` |
|
| [`mask_shape_above_cap_blur`](#mask_shape_above_cap_blur) | `bool` | `false` |
|
||||||
| [`mask_shape_above_cap_blur_max_bytes`](#cfg-censorship-mask_shape_above_cap_blur_max_bytes) | `usize` | `512` |
|
| [`mask_shape_above_cap_blur_max_bytes`](#mask_shape_above_cap_blur_max_bytes) | `usize` | `512` |
|
||||||
| [`mask_relay_max_bytes`](#cfg-censorship-mask_relay_max_bytes) | `usize` | `5242880` |
|
| [`mask_relay_max_bytes`](#mask_relay_max_bytes) | `usize` | `5242880` |
|
||||||
| [`mask_relay_timeout_ms`](#cfg-censorship-mask_relay_timeout_ms) | `u64` | `60_000` |
|
| [`mask_classifier_prefetch_timeout_ms`](#mask_classifier_prefetch_timeout_ms) | `u64` | `5` |
|
||||||
| [`mask_relay_idle_timeout_ms`](#cfg-censorship-mask_relay_idle_timeout_ms) | `u64` | `5_000` |
|
| [`mask_timing_normalization_enabled`](#mask_timing_normalization_enabled) | `bool` | `false` |
|
||||||
| [`mask_classifier_prefetch_timeout_ms`](#cfg-censorship-mask_classifier_prefetch_timeout_ms) | `u64` | `5` |
|
| [`mask_timing_normalization_floor_ms`](#mask_timing_normalization_floor_ms) | `u64` | `0` |
|
||||||
| [`mask_timing_normalization_enabled`](#cfg-censorship-mask_timing_normalization_enabled) | `bool` | `false` |
|
| [`mask_timing_normalization_ceiling_ms`](#mask_timing_normalization_ceiling_ms) | `u64` | `0` |
|
||||||
| [`mask_timing_normalization_floor_ms`](#cfg-censorship-mask_timing_normalization_floor_ms) | `u64` | `0` |
|
|
||||||
| [`mask_timing_normalization_ceiling_ms`](#cfg-censorship-mask_timing_normalization_ceiling_ms) | `u64` | `0` |
|
|
||||||
|
|
||||||
## "cfg-censorship-tls_domain"
|
## tls_domain
|
||||||
- `tls_domain`
|
|
||||||
- **Constraints / validation**: Must be a non-empty domain name. Must not contain spaces or `/`.
|
- **Constraints / validation**: Must be a non-empty domain name. Must not contain spaces or `/`.
|
||||||
- **Description**: Primary domain used for Fake-TLS masking / fronting profile and as the default SNI domain presented to clients.
|
- **Description**: Primary domain used for Fake-TLS masking / fronting profile and as the default SNI domain presented to clients.
|
||||||
This value becomes part of generated `ee` links, and changing it invalidates previously generated links.
|
This value becomes part of generated `ee` links, and changing it invalidates previously generated links.
|
||||||
@@ -2542,28 +2538,7 @@ Note: This section also accepts the legacy alias `[server.admin_api]` (same sche
|
|||||||
[censorship]
|
[censorship]
|
||||||
mask_relay_max_bytes = 5242880
|
mask_relay_max_bytes = 5242880
|
||||||
```
|
```
|
||||||
## "cfg-censorship-mask_relay_timeout_ms"
|
## mask_classifier_prefetch_timeout_ms
|
||||||
- `mask_relay_timeout_ms`
|
|
||||||
- **Constraints / validation**: Should be `>= mask_relay_idle_timeout_ms`.
|
|
||||||
- **Description**: Wall-clock cap for the full masking relay on non-MTProto fallback paths. Raise when the mask target is a long-lived service (e.g. WebSocket). Default: 60 000 ms (1 minute).
|
|
||||||
- **Example**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[censorship]
|
|
||||||
mask_relay_timeout_ms = 60000
|
|
||||||
```
|
|
||||||
## "cfg-censorship-mask_relay_idle_timeout_ms"
|
|
||||||
- `mask_relay_idle_timeout_ms`
|
|
||||||
- **Constraints / validation**: Should be `<= mask_relay_timeout_ms`.
|
|
||||||
- **Description**: Per-read idle timeout on masking relay and drain paths. Limits resource consumption by slow-loris attacks and port scanners. A read call stalling beyond this value is treated as an abandoned connection. Default: 5 000 ms (5 s).
|
|
||||||
- **Example**:
|
|
||||||
|
|
||||||
```toml
|
|
||||||
[censorship]
|
|
||||||
mask_relay_idle_timeout_ms = 5000
|
|
||||||
```
|
|
||||||
## "cfg-censorship-mask_classifier_prefetch_timeout_ms"
|
|
||||||
- `mask_classifier_prefetch_timeout_ms`
|
|
||||||
- **Constraints / validation**: Must be within `[5, 50]` (milliseconds).
|
- **Constraints / validation**: Must be within `[5, 50]` (milliseconds).
|
||||||
- **Description**: Timeout budget (ms) for extending fragmented initial classifier window on masking fallback.
|
- **Description**: Timeout budget (ms) for extending fragmented initial classifier window on masking fallback.
|
||||||
- **Example**:
|
- **Example**:
|
||||||
@@ -2931,7 +2906,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 +2925,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 +2946,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 +2956,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 +2967,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 +2981,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 +2998,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 +3014,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 +3024,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 +3034,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 +3045,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,11 +3056,9 @@ 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"
|
||||||
password = "secret"
|
password = "secret"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+3
-25
@@ -36,11 +36,8 @@ 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
|
||||||
@@ -157,24 +154,6 @@ 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:
|
||||||
@@ -182,8 +161,7 @@ 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.
|
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).
|
||||||
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`.
|
||||||
|
|||||||
+2
-22
@@ -33,12 +33,9 @@ hello = "ad_tag"
|
|||||||
hello2 = "ad_tag2"
|
hello2 = "ad_tag2"
|
||||||
```
|
```
|
||||||
## Распознаваемость для DPI и сканеров
|
## Распознаваемость для DPI и сканеров
|
||||||
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
|
|
||||||
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах.
|
|
||||||
|
|
||||||
> [!IMPORTANT]
|
1 апреля 2026 года нам стало известно о методе обнаружения MTProxy Fake-TLS, основанном на расширении ECH и порядке набора шифров,
|
||||||
> Проблема с TLS отпечатком исправлена в последних версиях клиентов Telegram для Desktop / Android / iOS.
|
а также об общем уникальном отпечатке JA3/JA4, который не встречается в современных браузерах: мы уже отправили первоначальные изменения разработчикам Telegram Desktop и работаем над обновлениями для других клиентов.
|
||||||
> Обновите свой клиент для корректной работы с MTProxy Fake-TLS!
|
|
||||||
|
|
||||||
- Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов;
|
- Мы считаем это прорывом, которому на сегодняшний день нет стабильных аналогов;
|
||||||
- Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом;
|
- Исходя из этого: если `telemt` настроен правильно, **режим TLS полностью идентичен реальному «рукопожатию» + обмену данными** с указанным хостом;
|
||||||
@@ -155,23 +152,6 @@ 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?
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,9 @@
|
|||||||
# Installation Options
|
|
||||||
There are three options for installing Telemt:
|
|
||||||
- [Automated installation using a script](#very-quick-start).
|
|
||||||
- [Manual installation of Telemt as a service](#telemt-via-systemd).
|
|
||||||
- [Installation using Docker Compose](#telemt-via-docker-compose).
|
|
||||||
|
|
||||||
# Very quick start
|
# 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 script’s startup parameters, you can use the following flags:
|
|
||||||
- **-d, --domain** - TLS domain;
|
|
||||||
- **-p, --port** - server port (1–65535);
|
|
||||||
- **-s, --secret** - 32 hex secret;
|
|
||||||
- **-a, --ad-tag** - ad_tag;
|
|
||||||
- **-l, --lan**g - language (1/en or 2/ru);
|
|
||||||
|
|
||||||
Providing all options skips interactive prompts.
|
|
||||||
|
|
||||||
After completion, the script will provide a link for client connections:
|
|
||||||
```bash
|
|
||||||
tg://proxy?server=IP&port=PORT&secret=SECRET
|
|
||||||
```
|
|
||||||
|
|
||||||
### Installing a specific version
|
### 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
|
||||||
|
|||||||
@@ -1,35 +1,9 @@
|
|||||||
# Варианты установки
|
|
||||||
Имеется три варианта установки 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** - порт (1–65535);
|
|
||||||
- **-s, --secret** - секрет (32 hex символа);
|
|
||||||
- **-a, --ad-tag** - ad_tag;
|
|
||||||
- **-l, --lang** - язык (1/en или 2/ru).
|
|
||||||
|
|
||||||
Если заданы флаги для языка и домена, интерактивных вопросов не будет.
|
|
||||||
|
|
||||||
После завершения установки скрипт выдаст ссылку для подключения клиентов:
|
|
||||||
```bash
|
|
||||||
tg://proxy?server=IP&port=PORT&secret=SECRET
|
|
||||||
```
|
|
||||||
|
|
||||||
### Установка нужной версии
|
### Установка нужной версии
|
||||||
```bash
|
```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
|
||||||
|
|||||||
+14
-38
@@ -1,6 +1,6 @@
|
|||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use std::io::{Error as IoError, ErrorKind};
|
use std::convert::Infallible;
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -16,7 +16,7 @@ use tokio::net::TcpListener;
|
|||||||
use tokio::sync::{Mutex, RwLock, watch};
|
use tokio::sync::{Mutex, RwLock, watch};
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use crate::config::{ApiGrayAction, ProxyConfig};
|
use crate::config::ProxyConfig;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::proxy::route_mode::RouteRuntimeController;
|
use crate::proxy::route_mode::RouteRuntimeController;
|
||||||
use crate::startup::StartupTracker;
|
use crate::startup::StartupTracker;
|
||||||
@@ -184,9 +184,7 @@ pub async fn serve(
|
|||||||
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
|
.serve_connection(hyper_util::rt::TokioIo::new(stream), svc)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
if !error.is_user() {
|
debug!(error = %error, "API connection error");
|
||||||
debug!(error = %error, "API connection error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -197,7 +195,7 @@ async fn handle(
|
|||||||
peer: SocketAddr,
|
peer: SocketAddr,
|
||||||
shared: Arc<ApiShared>,
|
shared: Arc<ApiShared>,
|
||||||
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
) -> Result<Response<Full<Bytes>>, IoError> {
|
) -> Result<Response<Full<Bytes>>, Infallible> {
|
||||||
let request_id = shared.next_request_id();
|
let request_id = shared.next_request_id();
|
||||||
let cfg = config_rx.borrow().clone();
|
let cfg = config_rx.borrow().clone();
|
||||||
let api_cfg = &cfg.server.api;
|
let api_cfg = &cfg.server.api;
|
||||||
@@ -215,25 +213,14 @@ async fn handle(
|
|||||||
|
|
||||||
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
|
if !api_cfg.whitelist.is_empty() && !api_cfg.whitelist.iter().any(|net| net.contains(peer.ip()))
|
||||||
{
|
{
|
||||||
return match api_cfg.gray_action {
|
return Ok(error_response(
|
||||||
ApiGrayAction::Api => Ok(error_response(
|
request_id,
|
||||||
request_id,
|
ApiFailure::new(
|
||||||
ApiFailure::new(
|
StatusCode::FORBIDDEN,
|
||||||
StatusCode::FORBIDDEN,
|
"forbidden",
|
||||||
"forbidden",
|
"Source IP is not allowed",
|
||||||
"Source IP is not allowed",
|
),
|
||||||
),
|
));
|
||||||
)),
|
|
||||||
ApiGrayAction::Ok200 => Ok(Response::builder()
|
|
||||||
.status(StatusCode::OK)
|
|
||||||
.header("content-type", "text/html; charset=utf-8")
|
|
||||||
.body(Full::new(Bytes::new()))
|
|
||||||
.unwrap()),
|
|
||||||
ApiGrayAction::Drop => Err(IoError::new(
|
|
||||||
ErrorKind::ConnectionAborted,
|
|
||||||
"api request dropped by gray_action=drop",
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !api_cfg.auth_header.is_empty() {
|
if !api_cfg.auth_header.is_empty() {
|
||||||
@@ -257,16 +244,11 @@ 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(), normalized_path) {
|
match (method.as_str(), path.as_str()) {
|
||||||
("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 {
|
||||||
@@ -449,7 +431,7 @@ async fn handle(
|
|||||||
Ok(success_response(status, data, revision))
|
Ok(success_response(status, data, revision))
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if let Some(user) = normalized_path.strip_prefix("/v1/users/")
|
if let Some(user) = path.strip_prefix("/v1/users/")
|
||||||
&& !user.is_empty()
|
&& !user.is_empty()
|
||||||
&& !user.contains('/')
|
&& !user.contains('/')
|
||||||
{
|
{
|
||||||
@@ -618,12 +600,6 @@ 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"),
|
||||||
|
|||||||
+1
-13
@@ -452,11 +452,7 @@ 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
|
let port = cfg.general.links.public_port.unwrap_or(cfg.server.port);
|
||||||
.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();
|
||||||
@@ -494,14 +490,6 @@ fn build_user_links(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_default_link_port(cfg: &ProxyConfig) -> u16 {
|
|
||||||
cfg.server
|
|
||||||
.listeners
|
|
||||||
.first()
|
|
||||||
.and_then(|listener| listener.port)
|
|
||||||
.unwrap_or(cfg.server.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_link_hosts(
|
fn resolve_link_hosts(
|
||||||
cfg: &ProxyConfig,
|
cfg: &ProxyConfig,
|
||||||
startup_detected_ip_v4: Option<IpAddr>,
|
startup_detected_ip_v4: Option<IpAddr>,
|
||||||
|
|||||||
+1
-2
@@ -598,17 +598,16 @@ 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
|
||||||
|
|||||||
+1
-21
@@ -210,7 +210,7 @@ pub(crate) fn default_proxy_protocol_header_timeout_ms() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_proxy_protocol_trusted_cidrs() -> Vec<IpNetwork> {
|
pub(crate) fn default_proxy_protocol_trusted_cidrs() -> Vec<IpNetwork> {
|
||||||
vec!["0.0.0.0/0".parse().unwrap(), "::/0".parse().unwrap()]
|
Vec::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_server_max_connections() -> u32 {
|
pub(crate) fn default_server_max_connections() -> u32 {
|
||||||
@@ -615,26 +615,6 @@ pub(crate) fn default_mask_relay_max_bytes() -> usize {
|
|||||||
32 * 1024
|
32 * 1024
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
pub(crate) fn default_mask_relay_timeout_ms() -> u64 {
|
|
||||||
60_000
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn default_mask_relay_timeout_ms() -> u64 {
|
|
||||||
200
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(test))]
|
|
||||||
pub(crate) fn default_mask_relay_idle_timeout_ms() -> u64 {
|
|
||||||
5_000
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub(crate) fn default_mask_relay_idle_timeout_ms() -> u64 {
|
|
||||||
100
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn default_mask_classifier_prefetch_timeout_ms() -> u64 {
|
pub(crate) fn default_mask_classifier_prefetch_timeout_ms() -> u64 {
|
||||||
5
|
5
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,8 @@
|
|||||||
//! | `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.listeners`, legacy
|
//! Fields that require re-binding sockets (`server.port`, `censorship.*`,
|
||||||
//! `server.port`, `censorship.*`, `network.*`, `use_middle_proxy`) are **not**
|
//! `network.*`, `use_middle_proxy`) are **not** applied; a warning is emitted.
|
||||||
//! 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;
|
||||||
@@ -300,7 +299,6 @@ 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
|
||||||
@@ -308,14 +306,6 @@ fn listeners_equal(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_default_link_port(cfg: &ProxyConfig) -> u16 {
|
|
||||||
cfg.server
|
|
||||||
.listeners
|
|
||||||
.first()
|
|
||||||
.and_then(|listener| listener.port)
|
|
||||||
.unwrap_or(cfg.server.port)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
struct WatchManifest {
|
struct WatchManifest {
|
||||||
files: BTreeSet<PathBuf>,
|
files: BTreeSet<PathBuf>,
|
||||||
@@ -570,7 +560,6 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
if old.server.api.enabled != new.server.api.enabled
|
if old.server.api.enabled != new.server.api.enabled
|
||||||
|| old.server.api.listen != new.server.api.listen
|
|| old.server.api.listen != new.server.api.listen
|
||||||
|| old.server.api.whitelist != new.server.api.whitelist
|
|| old.server.api.whitelist != new.server.api.whitelist
|
||||||
|| old.server.api.gray_action != new.server.api.gray_action
|
|
||||||
|| old.server.api.auth_header != new.server.api.auth_header
|
|| old.server.api.auth_header != new.server.api.auth_header
|
||||||
|| old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes
|
|| old.server.api.request_body_limit_bytes != new.server.api.request_body_limit_bytes
|
||||||
|| old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
|
|| old.server.api.minimal_runtime_enabled != new.server.api.minimal_runtime_enabled
|
||||||
@@ -622,8 +611,6 @@ fn warn_non_hot_changes(old: &ProxyConfig, new: &ProxyConfig, non_hot_changed: b
|
|||||||
|| old.censorship.mask_shape_above_cap_blur_max_bytes
|
|| old.censorship.mask_shape_above_cap_blur_max_bytes
|
||||||
!= new.censorship.mask_shape_above_cap_blur_max_bytes
|
!= new.censorship.mask_shape_above_cap_blur_max_bytes
|
||||||
|| old.censorship.mask_relay_max_bytes != new.censorship.mask_relay_max_bytes
|
|| old.censorship.mask_relay_max_bytes != new.censorship.mask_relay_max_bytes
|
||||||
|| old.censorship.mask_relay_timeout_ms != new.censorship.mask_relay_timeout_ms
|
|
||||||
|| old.censorship.mask_relay_idle_timeout_ms != new.censorship.mask_relay_idle_timeout_ms
|
|
||||||
|| old.censorship.mask_classifier_prefetch_timeout_ms
|
|| old.censorship.mask_classifier_prefetch_timeout_ms
|
||||||
!= new.censorship.mask_classifier_prefetch_timeout_ms
|
!= new.censorship.mask_classifier_prefetch_timeout_ms
|
||||||
|| old.censorship.mask_timing_normalization_enabled
|
|| old.censorship.mask_timing_normalization_enabled
|
||||||
@@ -1130,7 +1117,7 @@ fn log_changes(
|
|||||||
.general
|
.general
|
||||||
.links
|
.links
|
||||||
.public_port
|
.public_port
|
||||||
.unwrap_or(resolve_default_link_port(new_cfg));
|
.unwrap_or(new_cfg.server.port);
|
||||||
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);
|
||||||
|
|||||||
+49
-214
@@ -47,12 +47,18 @@ pub(crate) struct UserAuthEntry {
|
|||||||
|
|
||||||
impl UserAuthSnapshot {
|
impl UserAuthSnapshot {
|
||||||
fn from_users(users: &HashMap<String, String>) -> Result<Self> {
|
fn from_users(users: &HashMap<String, String>) -> Result<Self> {
|
||||||
|
// Keep runtime user ids stable across reloads so overload scans and
|
||||||
|
// sticky hints do not depend on HashMap iteration order.
|
||||||
|
let mut sorted_users: Vec<_> = users.iter().collect();
|
||||||
|
sorted_users
|
||||||
|
.sort_unstable_by(|(left, _), (right, _)| left.as_bytes().cmp(right.as_bytes()));
|
||||||
|
|
||||||
let mut entries = Vec::with_capacity(users.len());
|
let mut entries = Vec::with_capacity(users.len());
|
||||||
let mut by_name = HashMap::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_index = HashMap::with_capacity(users.len());
|
||||||
let mut sni_initial_index = HashMap::with_capacity(users.len());
|
let mut sni_initial_index = HashMap::with_capacity(users.len());
|
||||||
|
|
||||||
for (user, secret_hex) in users {
|
for (user, secret_hex) in sorted_users {
|
||||||
let decoded = hex::decode(secret_hex).map_err(|_| ProxyError::InvalidSecret {
|
let decoded = hex::decode(secret_hex).map_err(|_| ProxyError::InvalidSecret {
|
||||||
user: user.clone(),
|
user: user.clone(),
|
||||||
reason: "Must be 32 hex characters".to_string(),
|
reason: "Must be 32 hex characters".to_string(),
|
||||||
@@ -253,12 +259,6 @@ 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}")))?;
|
||||||
@@ -346,29 +346,12 @@ impl ProxyConfig {
|
|||||||
let update_every_is_explicit = general_table
|
let update_every_is_explicit = general_table
|
||||||
.map(|table| table.contains_key("update_every"))
|
.map(|table| table.contains_key("update_every"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let beobachten_is_explicit = general_table
|
|
||||||
.map(|table| table.contains_key("beobachten"))
|
|
||||||
.unwrap_or(false);
|
|
||||||
let beobachten_minutes_is_explicit = general_table
|
|
||||||
.map(|table| table.contains_key("beobachten_minutes"))
|
|
||||||
.unwrap_or(false);
|
|
||||||
let beobachten_flush_secs_is_explicit = general_table
|
|
||||||
.map(|table| table.contains_key("beobachten_flush_secs"))
|
|
||||||
.unwrap_or(false);
|
|
||||||
let beobachten_file_is_explicit = general_table
|
|
||||||
.map(|table| table.contains_key("beobachten_file"))
|
|
||||||
.unwrap_or(false);
|
|
||||||
let legacy_secret_is_explicit = general_table
|
let legacy_secret_is_explicit = general_table
|
||||||
.map(|table| table.contains_key("proxy_secret_auto_reload_secs"))
|
.map(|table| table.contains_key("proxy_secret_auto_reload_secs"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let legacy_config_is_explicit = general_table
|
let legacy_config_is_explicit = general_table
|
||||||
.map(|table| table.contains_key("proxy_config_auto_reload_secs"))
|
.map(|table| table.contains_key("proxy_config_auto_reload_secs"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let legacy_top_level_beobachten = parsed_toml.get("beobachten").cloned();
|
|
||||||
let legacy_top_level_beobachten_minutes = parsed_toml.get("beobachten_minutes").cloned();
|
|
||||||
let legacy_top_level_beobachten_flush_secs =
|
|
||||||
parsed_toml.get("beobachten_flush_secs").cloned();
|
|
||||||
let legacy_top_level_beobachten_file = parsed_toml.get("beobachten_file").cloned();
|
|
||||||
let stun_servers_is_explicit = network_table
|
let stun_servers_is_explicit = network_table
|
||||||
.map(|table| table.contains_key("stun_servers"))
|
.map(|table| table.contains_key("stun_servers"))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
@@ -381,59 +364,6 @@ impl ProxyConfig {
|
|||||||
config.general.update_every = None;
|
config.general.update_every = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backward compatibility: legacy top-level beobachten* keys.
|
|
||||||
// Prefer `[general].*` when both are present.
|
|
||||||
let mut legacy_beobachten_applied = false;
|
|
||||||
if !beobachten_is_explicit && let Some(value) = legacy_top_level_beobachten.as_ref() {
|
|
||||||
let parsed = value.as_bool().ok_or_else(|| {
|
|
||||||
ProxyError::Config("beobachten (top-level) must be a boolean".to_string())
|
|
||||||
})?;
|
|
||||||
config.general.beobachten = parsed;
|
|
||||||
legacy_beobachten_applied = true;
|
|
||||||
}
|
|
||||||
if !beobachten_minutes_is_explicit
|
|
||||||
&& let Some(value) = legacy_top_level_beobachten_minutes.as_ref()
|
|
||||||
{
|
|
||||||
let raw = value.as_integer().ok_or_else(|| {
|
|
||||||
ProxyError::Config("beobachten_minutes (top-level) must be an integer".to_string())
|
|
||||||
})?;
|
|
||||||
let parsed = u64::try_from(raw).map_err(|_| {
|
|
||||||
ProxyError::Config(
|
|
||||||
"beobachten_minutes (top-level) must be within u64 range".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
config.general.beobachten_minutes = parsed;
|
|
||||||
legacy_beobachten_applied = true;
|
|
||||||
}
|
|
||||||
if !beobachten_flush_secs_is_explicit
|
|
||||||
&& let Some(value) = legacy_top_level_beobachten_flush_secs.as_ref()
|
|
||||||
{
|
|
||||||
let raw = value.as_integer().ok_or_else(|| {
|
|
||||||
ProxyError::Config(
|
|
||||||
"beobachten_flush_secs (top-level) must be an integer".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let parsed = u64::try_from(raw).map_err(|_| {
|
|
||||||
ProxyError::Config(
|
|
||||||
"beobachten_flush_secs (top-level) must be within u64 range".to_string(),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
config.general.beobachten_flush_secs = parsed;
|
|
||||||
legacy_beobachten_applied = true;
|
|
||||||
}
|
|
||||||
if !beobachten_file_is_explicit
|
|
||||||
&& let Some(value) = legacy_top_level_beobachten_file.as_ref()
|
|
||||||
{
|
|
||||||
let parsed = value.as_str().ok_or_else(|| {
|
|
||||||
ProxyError::Config("beobachten_file (top-level) must be a string".to_string())
|
|
||||||
})?;
|
|
||||||
config.general.beobachten_file = parsed.to_string();
|
|
||||||
legacy_beobachten_applied = true;
|
|
||||||
}
|
|
||||||
if legacy_beobachten_applied {
|
|
||||||
warn!("top-level beobachten* keys are deprecated; use general.beobachten* instead");
|
|
||||||
}
|
|
||||||
|
|
||||||
let legacy_nat_stun = config.general.middle_proxy_nat_stun.take();
|
let legacy_nat_stun = config.general.middle_proxy_nat_stun.take();
|
||||||
let legacy_nat_stun_servers =
|
let legacy_nat_stun_servers =
|
||||||
std::mem::take(&mut config.general.middle_proxy_nat_stun_servers);
|
std::mem::take(&mut config.general.middle_proxy_nat_stun_servers);
|
||||||
@@ -1326,7 +1256,6 @@ 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,
|
||||||
@@ -1338,7 +1267,6 @@ 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,
|
||||||
@@ -1347,13 +1275,6 @@ impl ProxyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Migration: listeners[].port fallback to legacy server.port.
|
|
||||||
for listener in &mut config.server.listeners {
|
|
||||||
if listener.port.is_none() {
|
|
||||||
listener.port = Some(config.server.port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migration: announce_ip → announce for each listener.
|
// 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()
|
||||||
@@ -1374,14 +1295,11 @@ impl ProxyConfig {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1473,21 +1391,6 @@ mod tests {
|
|||||||
const TEST_SHADOWSOCKS_URL: &str =
|
const TEST_SHADOWSOCKS_URL: &str =
|
||||||
"ss://2022-blake3-aes-256-gcm:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=@127.0.0.1:8388";
|
"ss://2022-blake3-aes-256-gcm:MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDE=@127.0.0.1:8388";
|
||||||
|
|
||||||
fn load_config_from_temp_toml(toml: &str) -> ProxyConfig {
|
|
||||||
let nonce = std::time::SystemTime::now()
|
|
||||||
.duration_since(std::time::UNIX_EPOCH)
|
|
||||||
.unwrap()
|
|
||||||
.as_nanos();
|
|
||||||
let dir = std::env::temp_dir().join(format!("telemt_load_cfg_{nonce}"));
|
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
|
||||||
let path = dir.join("config.toml");
|
|
||||||
std::fs::write(&path, toml).unwrap();
|
|
||||||
let cfg = ProxyConfig::load(&path).unwrap();
|
|
||||||
let _ = std::fs::remove_file(path);
|
|
||||||
let _ = std::fs::remove_dir(dir);
|
|
||||||
cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn serde_defaults_remain_unchanged_for_present_sections() {
|
fn serde_defaults_remain_unchanged_for_present_sections() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
@@ -1584,7 +1487,6 @@ mod tests {
|
|||||||
cfg.general.rpc_proxy_req_every,
|
cfg.general.rpc_proxy_req_every,
|
||||||
default_rpc_proxy_req_every()
|
default_rpc_proxy_req_every()
|
||||||
);
|
);
|
||||||
assert_eq!(cfg.general.beobachten_file, default_beobachten_file());
|
|
||||||
assert_eq!(cfg.general.update_every, default_update_every());
|
assert_eq!(cfg.general.update_every, default_update_every());
|
||||||
assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4());
|
assert_eq!(cfg.server.listen_addr_ipv4, default_listen_addr_ipv4());
|
||||||
assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt());
|
assert_eq!(cfg.server.listen_addr_ipv6, default_listen_addr_ipv6_opt());
|
||||||
@@ -1595,7 +1497,6 @@ mod tests {
|
|||||||
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
|
assert_eq!(cfg.censorship.unknown_sni_action, UnknownSniAction::Drop);
|
||||||
assert_eq!(cfg.server.api.listen, default_api_listen());
|
assert_eq!(cfg.server.api.listen, default_api_listen());
|
||||||
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
||||||
assert_eq!(cfg.server.api.gray_action, ApiGrayAction::Drop);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.server.api.request_body_limit_bytes,
|
cfg.server.api.request_body_limit_bytes,
|
||||||
default_api_request_body_limit_bytes()
|
default_api_request_body_limit_bytes()
|
||||||
@@ -1752,7 +1653,6 @@ mod tests {
|
|||||||
default_upstream_connect_failfast_hard_errors()
|
default_upstream_connect_failfast_hard_errors()
|
||||||
);
|
);
|
||||||
assert_eq!(general.rpc_proxy_req_every, default_rpc_proxy_req_every());
|
assert_eq!(general.rpc_proxy_req_every, default_rpc_proxy_req_every());
|
||||||
assert_eq!(general.beobachten_file, default_beobachten_file());
|
|
||||||
assert_eq!(general.update_every, default_update_every());
|
assert_eq!(general.update_every, default_update_every());
|
||||||
|
|
||||||
let server = ServerConfig::default();
|
let server = ServerConfig::default();
|
||||||
@@ -1767,7 +1667,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
assert_eq!(server.api.listen, default_api_listen());
|
assert_eq!(server.api.listen, default_api_listen());
|
||||||
assert_eq!(server.api.whitelist, default_api_whitelist());
|
assert_eq!(server.api.whitelist, default_api_whitelist());
|
||||||
assert_eq!(server.api.gray_action, ApiGrayAction::Drop);
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
server.api.request_body_limit_bytes,
|
server.api.request_body_limit_bytes,
|
||||||
default_api_request_body_limit_bytes()
|
default_api_request_body_limit_bytes()
|
||||||
@@ -1831,7 +1730,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn proxy_protocol_trusted_cidrs_missing_uses_trust_all_but_explicit_empty_stays_empty() {
|
fn proxy_protocol_trusted_cidrs_missing_defaults_to_empty_and_explicit_empty_stays_empty() {
|
||||||
let cfg_missing: ProxyConfig = toml::from_str(
|
let cfg_missing: ProxyConfig = toml::from_str(
|
||||||
r#"
|
r#"
|
||||||
[server]
|
[server]
|
||||||
@@ -1841,10 +1740,7 @@ mod tests {
|
|||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(
|
assert!(cfg_missing.server.proxy_protocol_trusted_cidrs.is_empty());
|
||||||
cfg_missing.server.proxy_protocol_trusted_cidrs,
|
|
||||||
default_proxy_protocol_trusted_cidrs()
|
|
||||||
);
|
|
||||||
|
|
||||||
let cfg_explicit_empty: ProxyConfig = toml::from_str(
|
let cfg_explicit_empty: ProxyConfig = toml::from_str(
|
||||||
r#"
|
r#"
|
||||||
@@ -1865,6 +1761,46 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn runtime_user_auth_snapshot_order_is_stable_across_hashmap_insertion_orders() {
|
||||||
|
let mut left_users = HashMap::new();
|
||||||
|
left_users.insert(
|
||||||
|
"beta".to_string(),
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
|
||||||
|
);
|
||||||
|
left_users.insert(
|
||||||
|
"alpha".to_string(),
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut right_users = HashMap::new();
|
||||||
|
right_users.insert(
|
||||||
|
"alpha".to_string(),
|
||||||
|
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_string(),
|
||||||
|
);
|
||||||
|
right_users.insert(
|
||||||
|
"beta".to_string(),
|
||||||
|
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb".to_string(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let left_snapshot = UserAuthSnapshot::from_users(&left_users).unwrap();
|
||||||
|
let right_snapshot = UserAuthSnapshot::from_users(&right_users).unwrap();
|
||||||
|
|
||||||
|
let left_names: Vec<_> = left_snapshot
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.user.as_str())
|
||||||
|
.collect();
|
||||||
|
let right_names: Vec<_> = right_snapshot
|
||||||
|
.entries()
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.user.as_str())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert_eq!(left_names, ["alpha", "beta"]);
|
||||||
|
assert_eq!(left_names, right_names);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn unknown_sni_action_parses_and_defaults_to_drop() {
|
fn unknown_sni_action_parses_and_defaults_to_drop() {
|
||||||
let cfg_default: ProxyConfig = toml::from_str(
|
let cfg_default: ProxyConfig = toml::from_str(
|
||||||
@@ -1915,107 +1851,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn api_gray_action_parses_and_defaults_to_drop() {
|
|
||||||
let cfg_default: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(cfg_default.server.api.gray_action, ApiGrayAction::Drop);
|
|
||||||
|
|
||||||
let cfg_api: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
[server.api]
|
|
||||||
gray_action = "api"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(cfg_api.server.api.gray_action, ApiGrayAction::Api);
|
|
||||||
|
|
||||||
let cfg_200: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
[server.api]
|
|
||||||
gray_action = "200"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(cfg_200.server.api.gray_action, ApiGrayAction::Ok200);
|
|
||||||
|
|
||||||
let cfg_drop: ProxyConfig = toml::from_str(
|
|
||||||
r#"
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
[server.api]
|
|
||||||
gray_action = "drop"
|
|
||||||
"#,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(cfg_drop.server.api.gray_action, ApiGrayAction::Drop);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn top_level_beobachten_keys_migrate_to_general_when_general_not_explicit() {
|
|
||||||
let cfg = load_config_from_temp_toml(
|
|
||||||
r#"
|
|
||||||
beobachten = false
|
|
||||||
beobachten_minutes = 7
|
|
||||||
beobachten_flush_secs = 3
|
|
||||||
beobachten_file = "tmp/legacy-beob.txt"
|
|
||||||
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(!cfg.general.beobachten);
|
|
||||||
assert_eq!(cfg.general.beobachten_minutes, 7);
|
|
||||||
assert_eq!(cfg.general.beobachten_flush_secs, 3);
|
|
||||||
assert_eq!(cfg.general.beobachten_file, "tmp/legacy-beob.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn general_beobachten_keys_have_priority_over_legacy_top_level() {
|
|
||||||
let cfg = load_config_from_temp_toml(
|
|
||||||
r#"
|
|
||||||
beobachten = true
|
|
||||||
beobachten_minutes = 30
|
|
||||||
beobachten_flush_secs = 30
|
|
||||||
beobachten_file = "tmp/legacy-beob.txt"
|
|
||||||
|
|
||||||
[server]
|
|
||||||
[general]
|
|
||||||
beobachten = false
|
|
||||||
beobachten_minutes = 5
|
|
||||||
beobachten_flush_secs = 2
|
|
||||||
beobachten_file = "tmp/general-beob.txt"
|
|
||||||
[network]
|
|
||||||
[access]
|
|
||||||
"#,
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(!cfg.general.beobachten);
|
|
||||||
assert_eq!(cfg.general.beobachten_minutes, 5);
|
|
||||||
assert_eq!(cfg.general.beobachten_flush_secs, 2);
|
|
||||||
assert_eq!(cfg.general.beobachten_file, "tmp/general-beob.txt");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dc_overrides_allow_string_and_array() {
|
fn dc_overrides_allow_string_and_array() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
|
|||||||
+3
-58
@@ -1153,8 +1153,7 @@ pub struct LinksConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub public_host: Option<String>,
|
pub public_host: Option<String>,
|
||||||
|
|
||||||
/// Public port for tg:// link generation.
|
/// Public port for tg:// link generation (overrides server.port).
|
||||||
/// Overrides listener ports and legacy `server.port`.
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub public_port: Option<u16>,
|
pub public_port: Option<u16>,
|
||||||
}
|
}
|
||||||
@@ -1184,13 +1183,6 @@ pub struct ApiConfig {
|
|||||||
#[serde(default = "default_api_whitelist")]
|
#[serde(default = "default_api_whitelist")]
|
||||||
pub whitelist: Vec<IpNetwork>,
|
pub whitelist: Vec<IpNetwork>,
|
||||||
|
|
||||||
/// Behavior for requests from source IPs outside `whitelist`.
|
|
||||||
/// - `api`: return structured API forbidden response.
|
|
||||||
/// - `200`: return `200 OK` with an empty body.
|
|
||||||
/// - `drop`: close the connection without HTTP response.
|
|
||||||
#[serde(default)]
|
|
||||||
pub gray_action: ApiGrayAction,
|
|
||||||
|
|
||||||
/// Optional static value for `Authorization` header validation.
|
/// Optional static value for `Authorization` header validation.
|
||||||
/// Empty string disables header auth.
|
/// Empty string disables header auth.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -1235,7 +1227,6 @@ impl Default for ApiConfig {
|
|||||||
enabled: default_true(),
|
enabled: default_true(),
|
||||||
listen: default_api_listen(),
|
listen: default_api_listen(),
|
||||||
whitelist: default_api_whitelist(),
|
whitelist: default_api_whitelist(),
|
||||||
gray_action: ApiGrayAction::default(),
|
|
||||||
auth_header: String::new(),
|
auth_header: String::new(),
|
||||||
request_body_limit_bytes: default_api_request_body_limit_bytes(),
|
request_body_limit_bytes: default_api_request_body_limit_bytes(),
|
||||||
minimal_runtime_enabled: default_api_minimal_runtime_enabled(),
|
minimal_runtime_enabled: default_api_minimal_runtime_enabled(),
|
||||||
@@ -1249,19 +1240,6 @@ impl Default for ApiConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
||||||
#[serde(rename_all = "lowercase")]
|
|
||||||
pub enum ApiGrayAction {
|
|
||||||
/// Preserve current API behavior for denied source IPs.
|
|
||||||
Api,
|
|
||||||
/// Mimic a plain web endpoint by returning `200 OK` with an empty body.
|
|
||||||
#[serde(rename = "200")]
|
|
||||||
Ok200,
|
|
||||||
/// Drop connection without HTTP response for denied source IPs.
|
|
||||||
#[default]
|
|
||||||
Drop,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
pub enum ConntrackMode {
|
pub enum ConntrackMode {
|
||||||
@@ -1376,8 +1354,6 @@ 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,
|
||||||
|
|
||||||
@@ -1411,9 +1387,8 @@ pub struct ServerConfig {
|
|||||||
|
|
||||||
/// Trusted source CIDRs allowed to send incoming PROXY protocol headers.
|
/// Trusted source CIDRs allowed to send incoming PROXY protocol headers.
|
||||||
///
|
///
|
||||||
/// If this field is omitted in config, it defaults to trust-all CIDRs
|
/// If this field is omitted in config, it defaults to an empty list and
|
||||||
/// (`0.0.0.0/0` and `::/0`). If it is explicitly set to an empty list,
|
/// all PROXY protocol headers are rejected until trusted CIDRs are set.
|
||||||
/// all PROXY protocol headers are rejected.
|
|
||||||
#[serde(default = "default_proxy_protocol_trusted_cidrs")]
|
#[serde(default = "default_proxy_protocol_trusted_cidrs")]
|
||||||
pub proxy_protocol_trusted_cidrs: Vec<IpNetwork>,
|
pub proxy_protocol_trusted_cidrs: Vec<IpNetwork>,
|
||||||
|
|
||||||
@@ -1734,19 +1709,6 @@ pub struct AntiCensorshipConfig {
|
|||||||
#[serde(default = "default_mask_relay_max_bytes")]
|
#[serde(default = "default_mask_relay_max_bytes")]
|
||||||
pub mask_relay_max_bytes: usize,
|
pub mask_relay_max_bytes: usize,
|
||||||
|
|
||||||
/// Wall-clock cap for the full masking relay on non-MTProto fallback paths.
|
|
||||||
/// Raise when the mask target is a long-lived service (e.g. WebSocket).
|
|
||||||
/// Default: 60 000 ms (60 s).
|
|
||||||
#[serde(default = "default_mask_relay_timeout_ms")]
|
|
||||||
pub mask_relay_timeout_ms: u64,
|
|
||||||
|
|
||||||
/// Per-read idle timeout on masking relay and drain paths.
|
|
||||||
/// Limits resource consumption by slow-loris attacks and port scanners.
|
|
||||||
/// A read call stalling beyond this is treated as an abandoned connection.
|
|
||||||
/// Default: 5 000 ms (5 s).
|
|
||||||
#[serde(default = "default_mask_relay_idle_timeout_ms")]
|
|
||||||
pub mask_relay_idle_timeout_ms: u64,
|
|
||||||
|
|
||||||
/// Prefetch timeout (ms) for extending fragmented masking classifier window.
|
/// Prefetch timeout (ms) for extending fragmented masking classifier window.
|
||||||
#[serde(default = "default_mask_classifier_prefetch_timeout_ms")]
|
#[serde(default = "default_mask_classifier_prefetch_timeout_ms")]
|
||||||
pub mask_classifier_prefetch_timeout_ms: u64,
|
pub mask_classifier_prefetch_timeout_ms: u64,
|
||||||
@@ -1792,8 +1754,6 @@ impl Default for AntiCensorshipConfig {
|
|||||||
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(),
|
mask_shape_above_cap_blur: default_mask_shape_above_cap_blur(),
|
||||||
mask_shape_above_cap_blur_max_bytes: default_mask_shape_above_cap_blur_max_bytes(),
|
mask_shape_above_cap_blur_max_bytes: default_mask_shape_above_cap_blur_max_bytes(),
|
||||||
mask_relay_max_bytes: default_mask_relay_max_bytes(),
|
mask_relay_max_bytes: default_mask_relay_max_bytes(),
|
||||||
mask_relay_timeout_ms: default_mask_relay_timeout_ms(),
|
|
||||||
mask_relay_idle_timeout_ms: default_mask_relay_idle_timeout_ms(),
|
|
||||||
mask_classifier_prefetch_timeout_ms: default_mask_classifier_prefetch_timeout_ms(),
|
mask_classifier_prefetch_timeout_ms: default_mask_classifier_prefetch_timeout_ms(),
|
||||||
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
|
mask_timing_normalization_enabled: default_mask_timing_normalization_enabled(),
|
||||||
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
|
mask_timing_normalization_floor_ms: default_mask_timing_normalization_floor_ms(),
|
||||||
@@ -1880,10 +1840,6 @@ pub enum UpstreamType {
|
|||||||
interface: Option<String>,
|
interface: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
bind_addresses: Option<Vec<String>>,
|
bind_addresses: Option<Vec<String>>,
|
||||||
/// Linux-only hard interface pinning via `SO_BINDTODEVICE`.
|
|
||||||
/// Optional alias: `force_bind`.
|
|
||||||
#[serde(default, alias = "force_bind")]
|
|
||||||
bindtodevice: Option<String>,
|
|
||||||
},
|
},
|
||||||
Socks4 {
|
Socks4 {
|
||||||
address: String,
|
address: String,
|
||||||
@@ -1920,22 +1876,11 @@ 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)]
|
||||||
|
|||||||
+21
-40
@@ -343,28 +343,15 @@ fn command_exists(binary: &str) -> bool {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn listener_port_set(cfg: &ProxyConfig) -> Vec<u16> {
|
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr>>) {
|
||||||
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>, u16)> = BTreeSet::new();
|
let mut v4_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
|
||||||
let mut v6_targets: BTreeSet<(Option<IpAddr>, u16)> = BTreeSet::new();
|
let mut v6_targets: BTreeSet<Option<IpAddr>> = 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
|
||||||
@@ -372,9 +359,9 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<(Option<IpAddr>, u16)>, Vec<(Optio
|
|||||||
.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, port));
|
v4_targets.insert(None);
|
||||||
} else {
|
} else {
|
||||||
v4_targets.insert((Some(ipv4), port));
|
v4_targets.insert(Some(ipv4));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(ipv6) = cfg
|
if let Some(ipv6) = cfg
|
||||||
@@ -384,39 +371,33 @@ fn notrack_targets(cfg: &ProxyConfig) -> (Vec<(Option<IpAddr>, u16)>, Vec<(Optio
|
|||||||
.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, port));
|
v6_targets.insert(None);
|
||||||
} else {
|
} else {
|
||||||
v6_targets.insert((Some(ipv6), port));
|
v6_targets.insert(Some(ipv6));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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, port));
|
v4_targets.insert(None);
|
||||||
} else {
|
} else {
|
||||||
v4_targets.insert((Some(listener.ip), port));
|
v4_targets.insert(Some(listener.ip));
|
||||||
}
|
}
|
||||||
} else if listener.ip.is_unspecified() {
|
} else if listener.ip.is_unspecified() {
|
||||||
v6_targets.insert((None, port));
|
v6_targets.insert(None);
|
||||||
} else {
|
} else {
|
||||||
v6_targets.insert((Some(listener.ip), port));
|
v6_targets.insert(Some(listener.ip));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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() {
|
||||||
for port in &ports {
|
v4_targets.insert(Some(*ip));
|
||||||
v4_targets.insert((Some(*ip), *port));
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for port in &ports {
|
v6_targets.insert(Some(*ip));
|
||||||
v6_targets.insert((Some(*ip), *port));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -441,19 +422,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, port) in v4_targets {
|
for ip in v4_targets {
|
||||||
let rule = if let Some(ip) = ip {
|
let rule = if let Some(ip) = ip {
|
||||||
format!("tcp dport {} ip daddr {} notrack", port, ip)
|
format!("tcp dport {} ip daddr {} notrack", cfg.server.port, ip)
|
||||||
} else {
|
} else {
|
||||||
format!("tcp dport {} notrack", port)
|
format!("tcp dport {} notrack", cfg.server.port)
|
||||||
};
|
};
|
||||||
rules.push(rule);
|
rules.push(rule);
|
||||||
}
|
}
|
||||||
for (ip, port) in v6_targets {
|
for ip in v6_targets {
|
||||||
let rule = if let Some(ip) = ip {
|
let rule = if let Some(ip) = ip {
|
||||||
format!("tcp dport {} ip6 daddr {} notrack", port, ip)
|
format!("tcp dport {} ip6 daddr {} notrack", cfg.server.port, ip)
|
||||||
} else {
|
} else {
|
||||||
format!("tcp dport {} notrack", port)
|
format!("tcp dport {} notrack", cfg.server.port)
|
||||||
};
|
};
|
||||||
rules.push(rule);
|
rules.push(rule);
|
||||||
}
|
}
|
||||||
@@ -517,7 +498,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, port) in selected {
|
for ip in selected {
|
||||||
let mut args = vec![
|
let mut args = vec![
|
||||||
"-t".to_string(),
|
"-t".to_string(),
|
||||||
"raw".to_string(),
|
"raw".to_string(),
|
||||||
@@ -526,7 +507,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(),
|
||||||
port.to_string(),
|
cfg.server.port.to_string(),
|
||||||
];
|
];
|
||||||
if let Some(ip) = ip {
|
if let Some(ip) = ip {
|
||||||
args.push("-d".to_string());
|
args.push("-d".to_string());
|
||||||
|
|||||||
@@ -31,19 +31,6 @@ 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>,
|
||||||
@@ -76,8 +63,7 @@ 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 listener_port = listener_port_or_legacy(listener_conf, config);
|
let addr = SocketAddr::new(listener_conf.ip, config.server.port);
|
||||||
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;
|
||||||
@@ -120,7 +106,11 @@ 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.general.links.public_port.unwrap_or(listener_port);
|
let link_port = config
|
||||||
|
.general
|
||||||
|
.links
|
||||||
|
.public_port
|
||||||
|
.unwrap_or(config.server.port);
|
||||||
print_proxy_links(&public_host, link_port, config);
|
print_proxy_links(&public_host, link_port, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +158,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
.general
|
.general
|
||||||
.links
|
.links
|
||||||
.public_port
|
.public_port
|
||||||
.unwrap_or(default_link_port(config)),
|
.unwrap_or(config.server.port),
|
||||||
)
|
)
|
||||||
} 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());
|
||||||
@@ -183,7 +173,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
.general
|
.general
|
||||||
.links
|
.links
|
||||||
.public_port
|
.public_port
|
||||||
.unwrap_or(default_link_port(config)),
|
.unwrap_or(config.server.port),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+27
-42
@@ -81,11 +81,23 @@ pub async fn run() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared maestro startup and main loop. `drop_after_bind` runs on Unix after listeners are bound
|
#[cfg(unix)]
|
||||||
// (for privilege drop); it is a no-op on other platforms.
|
async fn run_inner(
|
||||||
async fn run_telemt_core(
|
daemon_opts: DaemonOptions,
|
||||||
drop_after_bind: impl FnOnce(),
|
|
||||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Acquire PID file if daemonizing or if explicitly requested
|
||||||
|
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
|
||||||
|
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
|
||||||
|
let mut pf = PidFile::new(daemon_opts.pid_file_path());
|
||||||
|
if let Err(e) = pf.acquire() {
|
||||||
|
eprintln!("[telemt] {}", e);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
Some(pf)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let process_started_at = Instant::now();
|
let process_started_at = Instant::now();
|
||||||
let process_started_at_epoch_secs = SystemTime::now()
|
let process_started_at_epoch_secs = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
@@ -749,8 +761,17 @@ async fn run_telemt_core(
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// On Unix, caller supplies privilege drop after bind (may require root for port < 1024).
|
// Drop privileges after binding sockets (which may require root for port < 1024)
|
||||||
drop_after_bind();
|
if daemon_opts.user.is_some() || daemon_opts.group.is_some() {
|
||||||
|
if let Err(e) = drop_privileges(
|
||||||
|
daemon_opts.user.as_deref(),
|
||||||
|
daemon_opts.group.as_deref(),
|
||||||
|
_pid_file.as_ref(),
|
||||||
|
) {
|
||||||
|
error!(error = %e, "Failed to drop privileges");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
runtime_tasks::apply_runtime_log_filter(
|
runtime_tasks::apply_runtime_log_filter(
|
||||||
has_rust_log,
|
has_rust_log,
|
||||||
@@ -798,39 +819,3 @@ async fn run_telemt_core(
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
async fn run_inner(
|
|
||||||
daemon_opts: DaemonOptions,
|
|
||||||
) -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|
||||||
// Acquire PID file if daemonizing or if explicitly requested
|
|
||||||
// Keep it alive until shutdown (underscore prefix = intentionally kept for RAII cleanup)
|
|
||||||
let _pid_file = if daemon_opts.daemonize || daemon_opts.pid_file.is_some() {
|
|
||||||
let mut pf = PidFile::new(daemon_opts.pid_file_path());
|
|
||||||
if let Err(e) = pf.acquire() {
|
|
||||||
eprintln!("[telemt] {}", e);
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
Some(pf)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = daemon_opts.user.clone();
|
|
||||||
let group = daemon_opts.group.clone();
|
|
||||||
|
|
||||||
run_telemt_core(|| {
|
|
||||||
if user.is_some() || group.is_some() {
|
|
||||||
if let Err(e) = drop_privileges(user.as_deref(), group.as_deref(), _pid_file.as_ref()) {
|
|
||||||
error!(error = %e, "Failed to drop privileges");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(unix))]
|
|
||||||
async fn run_inner() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
|
||||||
run_telemt_core(|| {}).await
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -97,7 +97,6 @@ pub async fn run_probe(
|
|||||||
let UpstreamType::Direct {
|
let UpstreamType::Direct {
|
||||||
interface,
|
interface,
|
||||||
bind_addresses,
|
bind_addresses,
|
||||||
..
|
|
||||||
} = &upstream.upstream_type
|
} = &upstream.upstream_type
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+52
-4
@@ -8,8 +8,6 @@ use hmac::{Hmac, Mac};
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
#[cfg(test)]
|
|
||||||
use std::collections::hash_map::RandomState;
|
|
||||||
use std::hash::{BuildHasher, Hash, Hasher};
|
use std::hash::{BuildHasher, Hash, Hasher};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::net::{IpAddr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv6Addr};
|
||||||
@@ -55,6 +53,7 @@ const STICKY_HINT_MAX_ENTRIES: usize = 65_536;
|
|||||||
const CANDIDATE_HINT_TRACK_CAP: usize = 64;
|
const CANDIDATE_HINT_TRACK_CAP: usize = 64;
|
||||||
const OVERLOAD_CANDIDATE_BUDGET_HINTED: usize = 16;
|
const OVERLOAD_CANDIDATE_BUDGET_HINTED: usize = 16;
|
||||||
const OVERLOAD_CANDIDATE_BUDGET_UNHINTED: usize = 8;
|
const OVERLOAD_CANDIDATE_BUDGET_UNHINTED: usize = 8;
|
||||||
|
const OVERLOAD_FULL_SCAN_USER_THRESHOLD: usize = CANDIDATE_HINT_TRACK_CAP;
|
||||||
const RECENT_USER_RING_SCAN_LIMIT: usize = 32;
|
const RECENT_USER_RING_SCAN_LIMIT: usize = 32;
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
@@ -242,6 +241,9 @@ fn budget_for_validation(total_users: usize, overload: bool, has_hint: bool) ->
|
|||||||
if !overload {
|
if !overload {
|
||||||
return total_users;
|
return total_users;
|
||||||
}
|
}
|
||||||
|
if total_users <= OVERLOAD_FULL_SCAN_USER_THRESHOLD {
|
||||||
|
return total_users;
|
||||||
|
}
|
||||||
let cap = if has_hint {
|
let cap = if has_hint {
|
||||||
OVERLOAD_CANDIDATE_BUDGET_HINTED
|
OVERLOAD_CANDIDATE_BUDGET_HINTED
|
||||||
} else {
|
} else {
|
||||||
@@ -250,6 +252,38 @@ fn budget_for_validation(total_users: usize, overload: bool, has_hint: bool) ->
|
|||||||
total_users.min(cap.max(1))
|
total_users.min(cap.max(1))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fold the peer address into a stable scan offset seed without invoking any
|
||||||
|
// cryptographic or keyed hashing. This only needs to fan peers out across the
|
||||||
|
// overload validation ring so repeated partial scans do not start at the same slot.
|
||||||
|
fn candidate_scan_peer_seed(peer_ip: IpAddr) -> usize {
|
||||||
|
match peer_ip {
|
||||||
|
IpAddr::V4(ip) => u32::from_be_bytes(ip.octets()) as usize,
|
||||||
|
IpAddr::V6(ip) => {
|
||||||
|
let raw = u128::from_be_bytes(ip.octets());
|
||||||
|
((raw >> 64) as u64 ^ raw as u64) as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate partial overload scans across larger snapshots so one truncated
|
||||||
|
// validation window does not permanently starve the same cold users.
|
||||||
|
fn candidate_scan_start_offset_in(
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
peer_ip: IpAddr,
|
||||||
|
total_users: usize,
|
||||||
|
candidate_budget: usize,
|
||||||
|
) -> usize {
|
||||||
|
if total_users == 0 || candidate_budget >= total_users {
|
||||||
|
return total_users.saturating_sub(total_users);
|
||||||
|
}
|
||||||
|
|
||||||
|
let seq = shared
|
||||||
|
.handshake
|
||||||
|
.auth_candidate_scan_seq
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
candidate_scan_peer_seed(peer_ip).wrapping_add(seq as usize) % total_users
|
||||||
|
}
|
||||||
|
|
||||||
fn parse_tls_auth_material(
|
fn parse_tls_auth_material(
|
||||||
handshake: &[u8],
|
handshake: &[u8],
|
||||||
ignore_time_skew: bool,
|
ignore_time_skew: bool,
|
||||||
@@ -1312,7 +1346,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matched && !budget_exhausted {
|
if !matched && !budget_exhausted {
|
||||||
for idx in 0..snapshot.entries().len() {
|
let fallback_start = candidate_scan_start_offset_in(
|
||||||
|
shared,
|
||||||
|
peer.ip(),
|
||||||
|
snapshot.entries().len(),
|
||||||
|
candidate_budget,
|
||||||
|
);
|
||||||
|
for offset in 0..snapshot.entries().len() {
|
||||||
|
let idx = (fallback_start + offset) % snapshot.entries().len();
|
||||||
let Some(user_id) = u32::try_from(idx).ok() else {
|
let Some(user_id) = u32::try_from(idx).ok() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
@@ -1679,7 +1720,14 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !matched && !budget_exhausted {
|
if !matched && !budget_exhausted {
|
||||||
for idx in 0..snapshot.entries().len() {
|
let fallback_start = candidate_scan_start_offset_in(
|
||||||
|
shared,
|
||||||
|
peer.ip(),
|
||||||
|
snapshot.entries().len(),
|
||||||
|
candidate_budget,
|
||||||
|
);
|
||||||
|
for offset in 0..snapshot.entries().len() {
|
||||||
|
let idx = (fallback_start + offset) % snapshot.entries().len();
|
||||||
let Some(user_id) = u32::try_from(idx).ok() else {
|
let Some(user_id) = u32::try_from(idx).ok() else {
|
||||||
break;
|
break;
|
||||||
};
|
};
|
||||||
|
|||||||
+117
-87
@@ -28,10 +28,14 @@ use tracing::debug;
|
|||||||
const MASK_TIMEOUT: Duration = Duration::from_secs(5);
|
const MASK_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const MASK_TIMEOUT: Duration = Duration::from_millis(50);
|
const MASK_TIMEOUT: Duration = Duration::from_millis(50);
|
||||||
/// Maximum duration for the entire masking relay under test (replaced by config at runtime).
|
/// Maximum duration for the entire masking relay.
|
||||||
|
/// Limits resource consumption from slow-loris attacks and port scanners.
|
||||||
|
#[cfg(not(test))]
|
||||||
|
const MASK_RELAY_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const MASK_RELAY_TIMEOUT: Duration = Duration::from_millis(200);
|
const MASK_RELAY_TIMEOUT: Duration = Duration::from_millis(200);
|
||||||
/// Per-read idle timeout for masking relay and drain paths under test (replaced by config at runtime).
|
#[cfg(not(test))]
|
||||||
|
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
|
const MASK_RELAY_IDLE_TIMEOUT: Duration = Duration::from_millis(100);
|
||||||
const MASK_BUFFER_SIZE: usize = 8192;
|
const MASK_BUFFER_SIZE: usize = 8192;
|
||||||
@@ -51,7 +55,6 @@ async fn copy_with_idle_timeout<R, W>(
|
|||||||
writer: &mut W,
|
writer: &mut W,
|
||||||
byte_cap: usize,
|
byte_cap: usize,
|
||||||
shutdown_on_eof: bool,
|
shutdown_on_eof: bool,
|
||||||
idle_timeout: Duration,
|
|
||||||
) -> CopyOutcome
|
) -> CopyOutcome
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
@@ -75,7 +78,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
|
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
|
||||||
let read_res = timeout(idle_timeout, reader.read(&mut buf[..read_len])).await;
|
let read_res = timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf[..read_len])).await;
|
||||||
let n = match read_res {
|
let n = match read_res {
|
||||||
Ok(Ok(n)) => n,
|
Ok(Ok(n)) => n,
|
||||||
Ok(Err(_)) | Err(_) => break,
|
Ok(Err(_)) | Err(_) => break,
|
||||||
@@ -83,13 +86,13 @@ where
|
|||||||
if n == 0 {
|
if n == 0 {
|
||||||
ended_by_eof = true;
|
ended_by_eof = true;
|
||||||
if shutdown_on_eof {
|
if shutdown_on_eof {
|
||||||
let _ = timeout(idle_timeout, writer.shutdown()).await;
|
let _ = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.shutdown()).await;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
total = total.saturating_add(n);
|
total = total.saturating_add(n);
|
||||||
|
|
||||||
let write_res = timeout(idle_timeout, writer.write_all(&buf[..n])).await;
|
let write_res = timeout(MASK_RELAY_IDLE_TIMEOUT, writer.write_all(&buf[..n])).await;
|
||||||
match write_res {
|
match write_res {
|
||||||
Ok(Ok(())) => {}
|
Ok(Ok(())) => {}
|
||||||
Ok(Err(_)) | Err(_) => break,
|
Ok(Err(_)) | Err(_) => break,
|
||||||
@@ -227,20 +230,13 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn consume_client_data_with_timeout_and_cap<R>(
|
async fn consume_client_data_with_timeout_and_cap<R>(reader: R, byte_cap: usize)
|
||||||
reader: R,
|
where
|
||||||
byte_cap: usize,
|
|
||||||
relay_timeout: Duration,
|
|
||||||
idle_timeout: Duration,
|
|
||||||
) where
|
|
||||||
R: AsyncRead + Unpin,
|
R: AsyncRead + Unpin,
|
||||||
{
|
{
|
||||||
if timeout(
|
if timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, byte_cap))
|
||||||
relay_timeout,
|
.await
|
||||||
consume_client_data(reader, byte_cap, idle_timeout),
|
.is_err()
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_err()
|
|
||||||
{
|
{
|
||||||
debug!("Timed out while consuming client data on masking fallback path");
|
debug!("Timed out while consuming client data on masking fallback path");
|
||||||
}
|
}
|
||||||
@@ -510,6 +506,40 @@ fn is_mask_target_local_listener_with_interfaces(
|
|||||||
local_addr: SocketAddr,
|
local_addr: SocketAddr,
|
||||||
resolved_override: Option<SocketAddr>,
|
resolved_override: Option<SocketAddr>,
|
||||||
interface_ips: &[IpAddr],
|
interface_ips: &[IpAddr],
|
||||||
|
) -> bool {
|
||||||
|
let resolved_candidates = resolved_override
|
||||||
|
.as_ref()
|
||||||
|
.map(std::slice::from_ref)
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
is_mask_target_local_listener_candidates_with_interfaces(
|
||||||
|
mask_host,
|
||||||
|
mask_port,
|
||||||
|
local_addr,
|
||||||
|
resolved_candidates,
|
||||||
|
interface_ips,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_ip_targets_local_listener(
|
||||||
|
mask_ip: IpAddr,
|
||||||
|
local_ip: IpAddr,
|
||||||
|
interface_ips: &[IpAddr],
|
||||||
|
) -> bool {
|
||||||
|
let mask_ip = canonical_ip(mask_ip);
|
||||||
|
if mask_ip == local_ip {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
local_ip.is_unspecified()
|
||||||
|
&& (mask_ip.is_loopback() || mask_ip.is_unspecified() || interface_ips.contains(&mask_ip))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_mask_target_local_listener_candidates_with_interfaces(
|
||||||
|
mask_host: &str,
|
||||||
|
mask_port: u16,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
resolved_candidates: &[SocketAddr],
|
||||||
|
interface_ips: &[IpAddr],
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if mask_port != local_addr.port() {
|
if mask_port != local_addr.port() {
|
||||||
return false;
|
return false;
|
||||||
@@ -518,31 +548,14 @@ fn is_mask_target_local_listener_with_interfaces(
|
|||||||
let local_ip = canonical_ip(local_addr.ip());
|
let local_ip = canonical_ip(local_addr.ip());
|
||||||
let literal_mask_ip = parse_mask_host_ip_literal(mask_host).map(canonical_ip);
|
let literal_mask_ip = parse_mask_host_ip_literal(mask_host).map(canonical_ip);
|
||||||
|
|
||||||
if let Some(addr) = resolved_override {
|
for addr in resolved_candidates {
|
||||||
let resolved_ip = canonical_ip(addr.ip());
|
if mask_ip_targets_local_listener(addr.ip(), local_ip, interface_ips) {
|
||||||
if resolved_ip == local_ip {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if local_ip.is_unspecified()
|
|
||||||
&& (resolved_ip.is_loopback()
|
|
||||||
|| resolved_ip.is_unspecified()
|
|
||||||
|| interface_ips.contains(&resolved_ip))
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mask_ip) = literal_mask_ip {
|
if let Some(mask_ip) = literal_mask_ip {
|
||||||
if mask_ip == local_ip {
|
if mask_ip_targets_local_listener(mask_ip, local_ip, interface_ips) {
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if local_ip.is_unspecified()
|
|
||||||
&& (mask_ip.is_loopback()
|
|
||||||
|| mask_ip.is_unspecified()
|
|
||||||
|| interface_ips.contains(&mask_ip))
|
|
||||||
{
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -576,21 +589,67 @@ async fn is_mask_target_local_listener_async(
|
|||||||
mask_port: u16,
|
mask_port: u16,
|
||||||
local_addr: SocketAddr,
|
local_addr: SocketAddr,
|
||||||
resolved_override: Option<SocketAddr>,
|
resolved_override: Option<SocketAddr>,
|
||||||
|
) -> bool {
|
||||||
|
let resolved_candidates = resolved_override
|
||||||
|
.as_ref()
|
||||||
|
.map(std::slice::from_ref)
|
||||||
|
.unwrap_or(&[]);
|
||||||
|
is_mask_target_local_listener_candidates_async(
|
||||||
|
mask_host,
|
||||||
|
mask_port,
|
||||||
|
local_addr,
|
||||||
|
resolved_candidates,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn is_mask_target_local_listener_candidates_async(
|
||||||
|
mask_host: &str,
|
||||||
|
mask_port: u16,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
resolved_candidates: &[SocketAddr],
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if mask_port != local_addr.port() {
|
if mask_port != local_addr.port() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let interfaces = local_interface_ips_async().await;
|
let interfaces = local_interface_ips_async().await;
|
||||||
is_mask_target_local_listener_with_interfaces(
|
is_mask_target_local_listener_candidates_with_interfaces(
|
||||||
mask_host,
|
mask_host,
|
||||||
mask_port,
|
mask_port,
|
||||||
local_addr,
|
local_addr,
|
||||||
resolved_override,
|
resolved_candidates,
|
||||||
&interfaces,
|
&interfaces,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolve hostnames through the same OS DNS path `TcpStream::connect` uses so
|
||||||
|
// self-target rejection also catches loopback and local-interface hostnames.
|
||||||
|
async fn resolve_mask_target_candidates(
|
||||||
|
mask_host: &str,
|
||||||
|
mask_port: u16,
|
||||||
|
resolved_override: Option<SocketAddr>,
|
||||||
|
) -> Vec<SocketAddr> {
|
||||||
|
if let Some(addr) = resolved_override {
|
||||||
|
return vec![addr];
|
||||||
|
}
|
||||||
|
|
||||||
|
if parse_mask_host_ip_literal(mask_host).is_some() {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut resolved = Vec::new();
|
||||||
|
if let Ok(addrs) = tokio::net::lookup_host((mask_host, mask_port)).await {
|
||||||
|
for addr in addrs {
|
||||||
|
if !resolved.contains(&addr) {
|
||||||
|
resolved.push(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolved
|
||||||
|
}
|
||||||
|
|
||||||
fn masking_beobachten_ttl(config: &ProxyConfig) -> Duration {
|
fn masking_beobachten_ttl(config: &ProxyConfig) -> Duration {
|
||||||
let minutes = config.general.beobachten_minutes;
|
let minutes = config.general.beobachten_minutes;
|
||||||
let clamped = minutes.clamp(1, 24 * 60);
|
let clamped = minutes.clamp(1, 24 * 60);
|
||||||
@@ -643,18 +702,10 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
beobachten.record(client_type, peer.ip(), ttl);
|
beobachten.record(client_type, peer.ip(), ttl);
|
||||||
}
|
}
|
||||||
|
|
||||||
let relay_timeout = Duration::from_millis(config.censorship.mask_relay_timeout_ms);
|
|
||||||
let idle_timeout = Duration::from_millis(config.censorship.mask_relay_idle_timeout_ms);
|
|
||||||
|
|
||||||
if !config.censorship.mask {
|
if !config.censorship.mask {
|
||||||
// Masking disabled, just consume data
|
// Masking disabled, just consume data
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
|
||||||
reader,
|
.await;
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
relay_timeout,
|
|
||||||
idle_timeout,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -686,7 +737,7 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if timeout(
|
if timeout(
|
||||||
relay_timeout,
|
MASK_RELAY_TIMEOUT,
|
||||||
relay_to_mask(
|
relay_to_mask(
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
@@ -700,7 +751,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
||||||
config.censorship.mask_shape_hardening_aggressive_mode,
|
config.censorship.mask_shape_hardening_aggressive_mode,
|
||||||
config.censorship.mask_relay_max_bytes,
|
config.censorship.mask_relay_max_bytes,
|
||||||
idle_timeout,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -716,8 +766,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(
|
||||||
reader,
|
reader,
|
||||||
config.censorship.mask_relay_max_bytes,
|
config.censorship.mask_relay_max_bytes,
|
||||||
relay_timeout,
|
|
||||||
idle_timeout,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
@@ -727,8 +775,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(
|
||||||
reader,
|
reader,
|
||||||
config.censorship.mask_relay_max_bytes,
|
config.censorship.mask_relay_max_bytes,
|
||||||
relay_timeout,
|
|
||||||
idle_timeout,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
@@ -748,8 +794,15 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
// Self-referential masking can create recursive proxy loops under
|
// Self-referential masking can create recursive proxy loops under
|
||||||
// misconfiguration and leak distinguishable load spikes to adversaries.
|
// misconfiguration and leak distinguishable load spikes to adversaries.
|
||||||
let resolved_mask_addr = resolve_socket_addr(mask_host, mask_port);
|
let resolved_mask_addr = resolve_socket_addr(mask_host, mask_port);
|
||||||
if is_mask_target_local_listener_async(mask_host, mask_port, local_addr, resolved_mask_addr)
|
let resolved_mask_candidates =
|
||||||
.await
|
resolve_mask_target_candidates(mask_host, mask_port, resolved_mask_addr).await;
|
||||||
|
if is_mask_target_local_listener_candidates_async(
|
||||||
|
mask_host,
|
||||||
|
mask_port,
|
||||||
|
local_addr,
|
||||||
|
&resolved_mask_candidates,
|
||||||
|
)
|
||||||
|
.await
|
||||||
{
|
{
|
||||||
let outcome_started = Instant::now();
|
let outcome_started = Instant::now();
|
||||||
debug!(
|
debug!(
|
||||||
@@ -759,13 +812,8 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
local = %local_addr,
|
local = %local_addr,
|
||||||
"Mask target resolves to local listener; refusing self-referential masking fallback"
|
"Mask target resolves to local listener; refusing self-referential masking fallback"
|
||||||
);
|
);
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, config.censorship.mask_relay_max_bytes)
|
||||||
reader,
|
.await;
|
||||||
config.censorship.mask_relay_max_bytes,
|
|
||||||
relay_timeout,
|
|
||||||
idle_timeout,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -799,7 +847,7 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if timeout(
|
if timeout(
|
||||||
relay_timeout,
|
MASK_RELAY_TIMEOUT,
|
||||||
relay_to_mask(
|
relay_to_mask(
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
@@ -813,7 +861,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
||||||
config.censorship.mask_shape_hardening_aggressive_mode,
|
config.censorship.mask_shape_hardening_aggressive_mode,
|
||||||
config.censorship.mask_relay_max_bytes,
|
config.censorship.mask_relay_max_bytes,
|
||||||
idle_timeout,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
@@ -829,8 +876,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(
|
||||||
reader,
|
reader,
|
||||||
config.censorship.mask_relay_max_bytes,
|
config.censorship.mask_relay_max_bytes,
|
||||||
relay_timeout,
|
|
||||||
idle_timeout,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
@@ -840,8 +885,6 @@ pub async fn handle_bad_client<R, W>(
|
|||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(
|
||||||
reader,
|
reader,
|
||||||
config.censorship.mask_relay_max_bytes,
|
config.censorship.mask_relay_max_bytes,
|
||||||
relay_timeout,
|
|
||||||
idle_timeout,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
wait_mask_outcome_budget(outcome_started, config).await;
|
wait_mask_outcome_budget(outcome_started, config).await;
|
||||||
@@ -863,7 +906,6 @@ async fn relay_to_mask<R, W, MR, MW>(
|
|||||||
shape_above_cap_blur_max_bytes: usize,
|
shape_above_cap_blur_max_bytes: usize,
|
||||||
shape_hardening_aggressive_mode: bool,
|
shape_hardening_aggressive_mode: bool,
|
||||||
mask_relay_max_bytes: usize,
|
mask_relay_max_bytes: usize,
|
||||||
idle_timeout: Duration,
|
|
||||||
) where
|
) where
|
||||||
R: AsyncRead + Unpin + Send + 'static,
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
W: AsyncWrite + Unpin + Send + 'static,
|
W: AsyncWrite + Unpin + Send + 'static,
|
||||||
@@ -885,19 +927,11 @@ async fn relay_to_mask<R, W, MR, MW>(
|
|||||||
&mut mask_write,
|
&mut mask_write,
|
||||||
mask_relay_max_bytes,
|
mask_relay_max_bytes,
|
||||||
!shape_hardening_enabled,
|
!shape_hardening_enabled,
|
||||||
idle_timeout,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
},
|
},
|
||||||
async {
|
async {
|
||||||
copy_with_idle_timeout(
|
copy_with_idle_timeout(&mut mask_read, &mut writer, mask_relay_max_bytes, true).await
|
||||||
&mut mask_read,
|
|
||||||
&mut writer,
|
|
||||||
mask_relay_max_bytes,
|
|
||||||
true,
|
|
||||||
idle_timeout,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -925,11 +959,7 @@ async fn relay_to_mask<R, W, MR, MW>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Just consume all data from client without responding.
|
/// Just consume all data from client without responding.
|
||||||
async fn consume_client_data<R: AsyncRead + Unpin>(
|
async fn consume_client_data<R: AsyncRead + Unpin>(mut reader: R, byte_cap: usize) {
|
||||||
mut reader: R,
|
|
||||||
byte_cap: usize,
|
|
||||||
idle_timeout: Duration,
|
|
||||||
) {
|
|
||||||
if byte_cap == 0 {
|
if byte_cap == 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -945,7 +975,7 @@ async fn consume_client_data<R: AsyncRead + Unpin>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
|
let read_len = remaining_budget.min(MASK_BUFFER_SIZE);
|
||||||
let n = match timeout(idle_timeout, reader.read(&mut buf[..read_len])).await {
|
let n = match timeout(MASK_RELAY_IDLE_TIMEOUT, reader.read(&mut buf[..read_len])).await {
|
||||||
Ok(Ok(n)) => n,
|
Ok(Ok(n)) => n,
|
||||||
Ok(Err(_)) | Err(_) => break,
|
Ok(Err(_)) | Err(_) => break,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ pub(crate) struct HandshakeSharedState {
|
|||||||
pub(crate) sticky_user_by_sni_hash: 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: Box<[AtomicU32]>,
|
||||||
pub(crate) recent_user_ring_seq: AtomicU64,
|
pub(crate) recent_user_ring_seq: AtomicU64,
|
||||||
|
pub(crate) auth_candidate_scan_seq: AtomicU64,
|
||||||
pub(crate) auth_expensive_checks_total: AtomicU64,
|
pub(crate) auth_expensive_checks_total: AtomicU64,
|
||||||
pub(crate) auth_budget_exhausted_total: AtomicU64,
|
pub(crate) auth_budget_exhausted_total: AtomicU64,
|
||||||
}
|
}
|
||||||
@@ -86,6 +87,7 @@ impl ProxySharedState {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_boxed_slice(),
|
.into_boxed_slice(),
|
||||||
recent_user_ring_seq: AtomicU64::new(0),
|
recent_user_ring_seq: AtomicU64::new(0),
|
||||||
|
auth_candidate_scan_seq: AtomicU64::new(0),
|
||||||
auth_expensive_checks_total: AtomicU64::new(0),
|
auth_expensive_checks_total: AtomicU64::new(0),
|
||||||
auth_budget_exhausted_total: AtomicU64::new(0),
|
auth_budget_exhausted_total: AtomicU64::new(0),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -31,14 +31,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -27,14 +27,11 @@ fn build_harness(config: ProxyConfig) -> PipelineHarness {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -25,14 +25,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -38,14 +38,11 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -16,14 +16,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -39,14 +39,11 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> RedTeamHarness {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -232,14 +229,11 @@ async fn redteam_03_masking_duration_must_be_less_than_1ms_when_backend_down() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -476,14 +470,11 @@ async fn measure_invalid_probe_duration_ms(delay_ms: u64, tls_len: u16, body_sen
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -553,14 +544,11 @@ async fn capture_forwarded_probe_len(tls_len: u16, body_sent: usize) -> usize {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -13,14 +13,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -11,14 +11,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -25,14 +25,11 @@ fn new_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -332,14 +332,11 @@ async fn relay_task_abort_releases_user_gate_and_ip_reservation() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -449,14 +446,11 @@ async fn relay_cutover_releases_user_gate_and_ip_reservation() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -576,14 +570,11 @@ async fn integration_route_cutover_and_quota_overlap_fails_closed_and_releases_s
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -749,14 +740,11 @@ async fn proxy_protocol_header_is_rejected_when_trust_list_is_empty() {
|
|||||||
upstream_type: crate::config::UpstreamType::Direct {
|
upstream_type: crate::config::UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -829,14 +817,11 @@ async fn proxy_protocol_header_from_untrusted_peer_range_is_rejected_under_load(
|
|||||||
upstream_type: crate::config::UpstreamType::Direct {
|
upstream_type: crate::config::UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -992,14 +977,11 @@ async fn short_tls_probe_is_masked_through_client_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1083,14 +1065,11 @@ async fn tls12_record_probe_is_masked_through_client_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1172,14 +1151,11 @@ async fn handle_client_stream_increments_connects_all_exactly_once() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1268,14 +1244,11 @@ async fn running_client_handler_increments_connects_all_exactly_once() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1361,14 +1334,11 @@ async fn idle_pooled_connection_closes_cleanly_in_generic_stream_path() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1435,14 +1405,11 @@ async fn idle_pooled_connection_closes_cleanly_in_client_handler_path() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1524,14 +1491,11 @@ async fn partial_tls_header_stall_triggers_handshake_timeout() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1852,14 +1816,11 @@ async fn valid_tls_path_does_not_fall_back_to_mask_backend() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1964,14 +1925,11 @@ async fn valid_tls_with_invalid_mtproto_falls_back_to_mask_backend() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -2074,14 +2032,11 @@ async fn client_handler_tls_bad_mtproto_is_forwarded_to_mask_backend() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -2199,14 +2154,11 @@ async fn alpn_mismatch_tls_probe_is_masked_through_client_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -2295,14 +2247,11 @@ async fn invalid_hmac_tls_probe_is_masked_through_client_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -2397,14 +2346,11 @@ async fn burst_invalid_tls_probes_are_masked_verbatim() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -3305,14 +3251,11 @@ async fn relay_connect_error_releases_user_and_ip_before_return() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -3869,14 +3812,11 @@ async fn untrusted_proxy_header_source_is_rejected() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -3942,14 +3882,11 @@ async fn empty_proxy_trusted_cidrs_rejects_proxy_header_by_default() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -4042,14 +3979,11 @@ async fn oversized_tls_record_is_masked_in_generic_stream_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -4148,14 +4082,11 @@ async fn oversized_tls_record_is_masked_in_client_handler_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -4268,14 +4199,11 @@ async fn tls_record_len_min_minus_1_is_rejected_in_generic_stream_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -4374,14 +4302,11 @@ async fn tls_record_len_min_minus_1_is_rejected_in_client_handler_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -4483,14 +4408,11 @@ async fn tls_record_len_16384_is_accepted_in_generic_stream_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -4587,14 +4509,11 @@ async fn tls_record_len_16384_is_accepted_in_client_handler_pipeline() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -24,14 +24,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -26,14 +26,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -27,14 +27,11 @@ fn make_test_upstream_manager(stats: Arc<Stats>) -> Arc<UpstreamManager> {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -41,14 +41,11 @@ fn build_harness(secret_hex: &str, mask_port: u16) -> PipelineHarness {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -1293,14 +1293,11 @@ async fn direct_relay_abort_midflight_releases_route_gauge() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1403,14 +1400,11 @@ async fn direct_relay_cutover_midflight_releases_route_gauge() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1528,14 +1522,11 @@ async fn direct_relay_cutover_storm_multi_session_keeps_generic_errors_and_relea
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
@@ -1767,11 +1758,8 @@ async fn negative_direct_relay_dc_connection_refused_fails_fast() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
100,
|
100,
|
||||||
@@ -1861,11 +1849,8 @@ async fn adversarial_direct_relay_cutover_integrity() {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
100,
|
100,
|
||||||
|
|||||||
@@ -1146,9 +1146,9 @@ async fn tls_overload_budget_limits_candidate_scan_depth() {
|
|||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
config.access.users.clear();
|
config.access.users.clear();
|
||||||
config.access.ignore_time_skew = true;
|
config.access.ignore_time_skew = true;
|
||||||
for idx in 0..32u8 {
|
for idx in 0..96u8 {
|
||||||
config.access.users.insert(
|
config.access.users.insert(
|
||||||
format!("user-{idx}"),
|
format!("user-{idx:02}"),
|
||||||
format!("{:032x}", u128::from(idx) + 1),
|
format!("{:032x}", u128::from(idx) + 1),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1203,6 +1203,64 @@ async fn tls_overload_budget_limits_candidate_scan_depth() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tls_overload_full_scans_small_runtime_snapshot_to_preserve_cold_user_auth() {
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config.access.users.clear();
|
||||||
|
config.access.ignore_time_skew = true;
|
||||||
|
for idx in 0..32u8 {
|
||||||
|
config.access.users.insert(
|
||||||
|
format!("user-{idx:02}"),
|
||||||
|
format!("{:032x}", u128::from(idx) + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
config.rebuild_runtime_user_auth().unwrap();
|
||||||
|
|
||||||
|
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
||||||
|
let rng = SecureRandom::new();
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let now = Instant::now();
|
||||||
|
{
|
||||||
|
let mut saturation = shared.handshake.auth_probe_saturation.lock().unwrap();
|
||||||
|
*saturation = Some(AuthProbeSaturationState {
|
||||||
|
fail_streak: AUTH_PROBE_BACKOFF_START_FAILS,
|
||||||
|
blocked_until: now + Duration::from_millis(200),
|
||||||
|
last_seen: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let peer: SocketAddr = "198.51.100.214:44326".parse().unwrap();
|
||||||
|
let mut secret = [0u8; 16];
|
||||||
|
secret[15] = 32;
|
||||||
|
let handshake = make_valid_tls_handshake(&secret, 0);
|
||||||
|
|
||||||
|
let result = handle_tls_handshake_with_shared(
|
||||||
|
&handshake,
|
||||||
|
tokio::io::empty(),
|
||||||
|
tokio::io::sink(),
|
||||||
|
peer,
|
||||||
|
&config,
|
||||||
|
&replay_checker,
|
||||||
|
&rng,
|
||||||
|
None,
|
||||||
|
shared.as_ref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(result, HandshakeResult::Success(_)),
|
||||||
|
"overload mode must still authenticate valid cold users when runtime snapshot stays small"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shared
|
||||||
|
.handshake
|
||||||
|
.auth_expensive_checks_total
|
||||||
|
.load(Ordering::Relaxed),
|
||||||
|
32,
|
||||||
|
"small saturated snapshots must remain fully scannable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn mtproto_runtime_snapshot_prefers_preferred_user_hint() {
|
async fn mtproto_runtime_snapshot_prefers_preferred_user_hint() {
|
||||||
let mut config = ProxyConfig::default();
|
let mut config = ProxyConfig::default();
|
||||||
@@ -1255,6 +1313,63 @@ async fn mtproto_runtime_snapshot_prefers_preferred_user_hint() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn mtproto_overload_full_scans_small_runtime_snapshot_to_preserve_cold_user_auth() {
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config.general.modes.secure = true;
|
||||||
|
config.access.users.clear();
|
||||||
|
config.access.ignore_time_skew = true;
|
||||||
|
for idx in 0..32u8 {
|
||||||
|
config.access.users.insert(
|
||||||
|
format!("user-{idx:02}"),
|
||||||
|
format!("{:032x}", u128::from(idx) + 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
config.rebuild_runtime_user_auth().unwrap();
|
||||||
|
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let now = Instant::now();
|
||||||
|
{
|
||||||
|
let mut saturation = shared.handshake.auth_probe_saturation.lock().unwrap();
|
||||||
|
*saturation = Some(AuthProbeSaturationState {
|
||||||
|
fail_streak: AUTH_PROBE_BACKOFF_START_FAILS,
|
||||||
|
blocked_until: now + Duration::from_millis(200),
|
||||||
|
last_seen: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
||||||
|
let handshake =
|
||||||
|
make_valid_mtproto_handshake("00000000000000000000000000000020", ProtoTag::Secure, 2);
|
||||||
|
let peer: SocketAddr = "198.51.100.215:44326".parse().unwrap();
|
||||||
|
|
||||||
|
let result = handle_mtproto_handshake_with_shared(
|
||||||
|
&handshake,
|
||||||
|
tokio::io::empty(),
|
||||||
|
tokio::io::sink(),
|
||||||
|
peer,
|
||||||
|
&config,
|
||||||
|
&replay_checker,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
shared.as_ref(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
matches!(result, HandshakeResult::Success(_)),
|
||||||
|
"overload mode must still authenticate valid direct MTProto users when runtime snapshot stays small"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
shared
|
||||||
|
.handshake
|
||||||
|
.auth_expensive_checks_total
|
||||||
|
.load(Ordering::Relaxed),
|
||||||
|
32,
|
||||||
|
"small saturated MTProto snapshots must remain fully scannable"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn alpn_enforce_rejects_unsupported_client_alpn() {
|
async fn alpn_enforce_rejects_unsupported_client_alpn() {
|
||||||
let secret = [0x33u8; 16];
|
let secret = [0x33u8; 16];
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ async fn consume_client_data_stops_after_byte_cap_without_eof() {
|
|||||||
};
|
};
|
||||||
let cap = 10_000usize;
|
let cap = 10_000usize;
|
||||||
|
|
||||||
consume_client_data(reader, cap, MASK_RELAY_IDLE_TIMEOUT).await;
|
consume_client_data(reader, cap).await;
|
||||||
|
|
||||||
let total = produced.load(Ordering::Relaxed);
|
let total = produced.load(Ordering::Relaxed);
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ async fn stalling_client_terminates_at_idle_not_relay_timeout() {
|
|||||||
|
|
||||||
let result = tokio::time::timeout(
|
let result = tokio::time::timeout(
|
||||||
MASK_RELAY_TIMEOUT,
|
MASK_RELAY_TIMEOUT,
|
||||||
consume_client_data(reader, MASK_BUFFER_SIZE * 4, MASK_RELAY_IDLE_TIMEOUT),
|
consume_client_data(reader, MASK_BUFFER_SIZE * 4),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -57,12 +57,9 @@ async fn fast_reader_drains_to_eof() {
|
|||||||
let data = vec![0xAAu8; 32 * 1024];
|
let data = vec![0xAAu8; 32 * 1024];
|
||||||
let reader = std::io::Cursor::new(data);
|
let reader = std::io::Cursor::new(data);
|
||||||
|
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(MASK_RELAY_TIMEOUT, consume_client_data(reader, usize::MAX))
|
||||||
MASK_RELAY_TIMEOUT,
|
.await
|
||||||
consume_client_data(reader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
|
.expect("consume_client_data did not complete for fast EOF reader");
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("consume_client_data did not complete for fast EOF reader");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -84,7 +81,7 @@ async fn io_error_terminates_cleanly() {
|
|||||||
|
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
MASK_RELAY_TIMEOUT,
|
MASK_RELAY_TIMEOUT,
|
||||||
consume_client_data(ErrReader, usize::MAX, MASK_RELAY_IDLE_TIMEOUT),
|
consume_client_data(ErrReader, usize::MAX),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("consume_client_data did not return on I/O error");
|
.expect("consume_client_data did not return on I/O error");
|
||||||
|
|||||||
@@ -34,11 +34,7 @@ async fn consume_stall_stress_finishes_within_idle_budget() {
|
|||||||
set.spawn(async {
|
set.spawn(async {
|
||||||
tokio::time::timeout(
|
tokio::time::timeout(
|
||||||
MASK_RELAY_TIMEOUT,
|
MASK_RELAY_TIMEOUT,
|
||||||
consume_client_data(
|
consume_client_data(OneByteThenStall { sent: false }, usize::MAX),
|
||||||
OneByteThenStall { sent: false },
|
|
||||||
usize::MAX,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("consume_client_data exceeded relay timeout under stall load");
|
.expect("consume_client_data exceeded relay timeout under stall load");
|
||||||
@@ -60,7 +56,7 @@ async fn consume_stall_stress_finishes_within_idle_budget() {
|
|||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn consume_zero_cap_returns_immediately() {
|
async fn consume_zero_cap_returns_immediately() {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
consume_client_data(tokio::io::empty(), 0, MASK_RELAY_IDLE_TIMEOUT).await;
|
consume_client_data(tokio::io::empty(), 0).await;
|
||||||
assert!(
|
assert!(
|
||||||
started.elapsed() < MASK_RELAY_IDLE_TIMEOUT,
|
started.elapsed() < MASK_RELAY_IDLE_TIMEOUT,
|
||||||
"zero byte cap must return immediately"
|
"zero byte cap must return immediately"
|
||||||
|
|||||||
@@ -127,14 +127,7 @@ async fn positive_copy_with_production_cap_stops_exactly_at_budget() {
|
|||||||
let mut reader = FinitePatternReader::new(PROD_CAP_BYTES + (256 * 1024), 4096, read_calls);
|
let mut reader = FinitePatternReader::new(PROD_CAP_BYTES + (256 * 1024), 4096, read_calls);
|
||||||
let mut writer = CountingWriter::default();
|
let mut writer = CountingWriter::default();
|
||||||
|
|
||||||
let outcome = copy_with_idle_timeout(
|
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await;
|
||||||
&mut reader,
|
|
||||||
&mut writer,
|
|
||||||
PROD_CAP_BYTES,
|
|
||||||
true,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
outcome.total, PROD_CAP_BYTES,
|
outcome.total, PROD_CAP_BYTES,
|
||||||
@@ -152,13 +145,7 @@ async fn negative_consume_with_zero_cap_performs_no_reads() {
|
|||||||
let read_calls = Arc::new(AtomicUsize::new(0));
|
let read_calls = Arc::new(AtomicUsize::new(0));
|
||||||
let reader = FinitePatternReader::new(1024, 64, Arc::clone(&read_calls));
|
let reader = FinitePatternReader::new(1024, 64, Arc::clone(&read_calls));
|
||||||
|
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, 0).await;
|
||||||
reader,
|
|
||||||
0,
|
|
||||||
MASK_RELAY_TIMEOUT,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read_calls.load(Ordering::Relaxed),
|
read_calls.load(Ordering::Relaxed),
|
||||||
@@ -174,14 +161,7 @@ async fn edge_copy_below_cap_reports_eof_without_overread() {
|
|||||||
let mut reader = FinitePatternReader::new(payload, 3072, read_calls);
|
let mut reader = FinitePatternReader::new(payload, 3072, read_calls);
|
||||||
let mut writer = CountingWriter::default();
|
let mut writer = CountingWriter::default();
|
||||||
|
|
||||||
let outcome = copy_with_idle_timeout(
|
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await;
|
||||||
&mut reader,
|
|
||||||
&mut writer,
|
|
||||||
PROD_CAP_BYTES,
|
|
||||||
true,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert_eq!(outcome.total, payload);
|
assert_eq!(outcome.total, payload);
|
||||||
assert_eq!(writer.written, payload);
|
assert_eq!(writer.written, payload);
|
||||||
@@ -195,13 +175,7 @@ async fn edge_copy_below_cap_reports_eof_without_overread() {
|
|||||||
async fn adversarial_blackhat_never_ready_reader_is_bounded_by_timeout_guards() {
|
async fn adversarial_blackhat_never_ready_reader_is_bounded_by_timeout_guards() {
|
||||||
let started = Instant::now();
|
let started = Instant::now();
|
||||||
|
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(NeverReadyReader, PROD_CAP_BYTES).await;
|
||||||
NeverReadyReader,
|
|
||||||
PROD_CAP_BYTES,
|
|
||||||
MASK_RELAY_TIMEOUT,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
started.elapsed() < Duration::from_millis(350),
|
started.elapsed() < Duration::from_millis(350),
|
||||||
@@ -216,12 +190,7 @@ async fn integration_consume_path_honors_production_cap_for_large_payload() {
|
|||||||
|
|
||||||
let bounded = timeout(
|
let bounded = timeout(
|
||||||
Duration::from_millis(350),
|
Duration::from_millis(350),
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, PROD_CAP_BYTES),
|
||||||
reader,
|
|
||||||
PROD_CAP_BYTES,
|
|
||||||
MASK_RELAY_TIMEOUT,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -237,13 +206,7 @@ async fn adversarial_consume_path_never_reads_beyond_declared_byte_cap() {
|
|||||||
let total_read = Arc::new(AtomicUsize::new(0));
|
let total_read = Arc::new(AtomicUsize::new(0));
|
||||||
let reader = BudgetProbeReader::new(256 * 1024, Arc::clone(&total_read));
|
let reader = BudgetProbeReader::new(256 * 1024, Arc::clone(&total_read));
|
||||||
|
|
||||||
consume_client_data_with_timeout_and_cap(
|
consume_client_data_with_timeout_and_cap(reader, byte_cap).await;
|
||||||
reader,
|
|
||||||
byte_cap,
|
|
||||||
MASK_RELAY_TIMEOUT,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
total_read.load(Ordering::Relaxed) <= byte_cap,
|
total_read.load(Ordering::Relaxed) <= byte_cap,
|
||||||
@@ -268,9 +231,7 @@ async fn light_fuzz_cap_and_payload_matrix_preserves_min_budget_invariant() {
|
|||||||
let mut reader = FinitePatternReader::new(payload, chunk, read_calls);
|
let mut reader = FinitePatternReader::new(payload, chunk, read_calls);
|
||||||
let mut writer = CountingWriter::default();
|
let mut writer = CountingWriter::default();
|
||||||
|
|
||||||
let outcome =
|
let outcome = copy_with_idle_timeout(&mut reader, &mut writer, cap, true).await;
|
||||||
copy_with_idle_timeout(&mut reader, &mut writer, cap, true, MASK_RELAY_IDLE_TIMEOUT)
|
|
||||||
.await;
|
|
||||||
let expected = payload.min(cap);
|
let expected = payload.min(cap);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -300,14 +261,7 @@ async fn stress_parallel_copy_tasks_with_production_cap_complete_without_leaks()
|
|||||||
read_calls,
|
read_calls,
|
||||||
);
|
);
|
||||||
let mut writer = CountingWriter::default();
|
let mut writer = CountingWriter::default();
|
||||||
copy_with_idle_timeout(
|
copy_with_idle_timeout(&mut reader, &mut writer, PROD_CAP_BYTES, true).await
|
||||||
&mut reader,
|
|
||||||
&mut writer,
|
|
||||||
PROD_CAP_BYTES,
|
|
||||||
true,
|
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ async fn relay_to_mask_enforces_masking_session_byte_cap() {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
32 * 1024,
|
32 * 1024,
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
@@ -82,7 +81,6 @@ async fn relay_to_mask_propagates_client_half_close_without_waiting_for_other_di
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
32 * 1024,
|
32 * 1024,
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1377,7 +1377,6 @@ async fn relay_to_mask_keeps_backend_to_client_flow_when_client_to_backend_stall
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
5 * 1024 * 1024,
|
5 * 1024 * 1024,
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
@@ -1509,7 +1508,6 @@ async fn relay_to_mask_timeout_cancels_and_drops_all_io_endpoints() {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
5 * 1024 * 1024,
|
5 * 1024 * 1024,
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|||||||
@@ -88,6 +88,45 @@ async fn self_target_fallback_refuses_recursive_loopback_connect() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn self_target_fallback_refuses_recursive_hostname_connect() {
|
||||||
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let local_addr = listener.local_addr().unwrap();
|
||||||
|
let accept_task = tokio::spawn(async move {
|
||||||
|
timeout(Duration::from_millis(120), listener.accept())
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut config = ProxyConfig::default();
|
||||||
|
config.general.beobachten = false;
|
||||||
|
config.censorship.mask = true;
|
||||||
|
config.censorship.mask_unix_sock = None;
|
||||||
|
config.censorship.mask_host = Some("localhost".to_string());
|
||||||
|
config.censorship.mask_port = local_addr.port();
|
||||||
|
config.censorship.mask_proxy_protocol = 0;
|
||||||
|
|
||||||
|
let peer: SocketAddr = "203.0.113.99:55099".parse().unwrap();
|
||||||
|
let beobachten = BeobachtenStore::new();
|
||||||
|
|
||||||
|
handle_bad_client(
|
||||||
|
tokio::io::empty(),
|
||||||
|
tokio::io::sink(),
|
||||||
|
b"GET /",
|
||||||
|
peer,
|
||||||
|
local_addr,
|
||||||
|
&config,
|
||||||
|
&beobachten,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let accepted = accept_task.await.unwrap();
|
||||||
|
assert!(
|
||||||
|
!accepted,
|
||||||
|
"hostname self-target masking must fail closed without connecting to local listener"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn same_ip_different_port_still_forwards_to_mask_backend() {
|
async fn same_ip_different_port_still_forwards_to_mask_backend() {
|
||||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
@@ -228,7 +267,6 @@ async fn relay_path_idle_timeout_eviction_remains_effective() {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
5 * 1024 * 1024,
|
5 * 1024 * 1024,
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ async fn run_relay_case(
|
|||||||
above_cap_blur_max_bytes,
|
above_cap_blur_max_bytes,
|
||||||
false,
|
false,
|
||||||
5 * 1024 * 1024,
|
5 * 1024 * 1024,
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -89,7 +89,6 @@ async fn relay_to_mask_applies_cap_clamped_padding_for_non_power_of_two_cap() {
|
|||||||
0,
|
0,
|
||||||
false,
|
false,
|
||||||
5 * 1024 * 1024,
|
5 * 1024 * 1024,
|
||||||
MASK_RELAY_IDLE_TIMEOUT,
|
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -53,14 +53,11 @@ fn new_client_harness() -> ClientHarness {
|
|||||||
upstream_type: UpstreamType::Direct {
|
upstream_type: UpstreamType::Direct {
|
||||||
interface: None,
|
interface: None,
|
||||||
bind_addresses: None,
|
bind_addresses: None,
|
||||||
bindtodevice: None,
|
|
||||||
},
|
},
|
||||||
weight: 1,
|
weight: 1,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
scopes: String::new(),
|
scopes: String::new(),
|
||||||
selected_scope: String::new(),
|
selected_scope: String::new(),
|
||||||
ipv4: None,
|
|
||||||
ipv6: None,
|
|
||||||
}],
|
}],
|
||||||
1,
|
1,
|
||||||
1,
|
1,
|
||||||
|
|||||||
@@ -67,8 +67,10 @@ struct FamilyReconnectOutcome {
|
|||||||
key: (i32, IpFamily),
|
key: (i32, IpFamily),
|
||||||
dc: i32,
|
dc: i32,
|
||||||
family: IpFamily,
|
family: IpFamily,
|
||||||
|
alive: usize,
|
||||||
required: usize,
|
required: usize,
|
||||||
endpoint_count: usize,
|
endpoint_count: usize,
|
||||||
|
restored: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_connections: usize) {
|
pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_connections: usize) {
|
||||||
@@ -80,6 +82,8 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
|
|||||||
let mut single_endpoint_outage: HashSet<(i32, IpFamily)> = HashSet::new();
|
let mut single_endpoint_outage: HashSet<(i32, IpFamily)> = HashSet::new();
|
||||||
let mut shadow_rotate_deadline: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
let mut shadow_rotate_deadline: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
||||||
let mut idle_refresh_next_attempt: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
let mut idle_refresh_next_attempt: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
||||||
|
let mut adaptive_idle_since: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
||||||
|
let mut adaptive_recover_until: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
||||||
let mut floor_warn_next_allowed: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
let mut floor_warn_next_allowed: HashMap<(i32, IpFamily), Instant> = HashMap::new();
|
||||||
let mut drain_warn_next_allowed: HashMap<u64, Instant> = HashMap::new();
|
let mut drain_warn_next_allowed: HashMap<u64, Instant> = HashMap::new();
|
||||||
let mut degraded_interval = true;
|
let mut degraded_interval = true;
|
||||||
@@ -105,6 +109,8 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
|
|||||||
&mut single_endpoint_outage,
|
&mut single_endpoint_outage,
|
||||||
&mut shadow_rotate_deadline,
|
&mut shadow_rotate_deadline,
|
||||||
&mut idle_refresh_next_attempt,
|
&mut idle_refresh_next_attempt,
|
||||||
|
&mut adaptive_idle_since,
|
||||||
|
&mut adaptive_recover_until,
|
||||||
&mut floor_warn_next_allowed,
|
&mut floor_warn_next_allowed,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -120,6 +126,8 @@ pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, _min_c
|
|||||||
&mut single_endpoint_outage,
|
&mut single_endpoint_outage,
|
||||||
&mut shadow_rotate_deadline,
|
&mut shadow_rotate_deadline,
|
||||||
&mut idle_refresh_next_attempt,
|
&mut idle_refresh_next_attempt,
|
||||||
|
&mut adaptive_idle_since,
|
||||||
|
&mut adaptive_recover_until,
|
||||||
&mut floor_warn_next_allowed,
|
&mut floor_warn_next_allowed,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
@@ -352,6 +360,8 @@ async fn check_family(
|
|||||||
single_endpoint_outage: &mut HashSet<(i32, IpFamily)>,
|
single_endpoint_outage: &mut HashSet<(i32, IpFamily)>,
|
||||||
shadow_rotate_deadline: &mut HashMap<(i32, IpFamily), Instant>,
|
shadow_rotate_deadline: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
idle_refresh_next_attempt: &mut HashMap<(i32, IpFamily), Instant>,
|
idle_refresh_next_attempt: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
|
adaptive_idle_since: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
|
adaptive_recover_until: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
floor_warn_next_allowed: &mut HashMap<(i32, IpFamily), Instant>,
|
floor_warn_next_allowed: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let enabled = match family {
|
let enabled = match family {
|
||||||
@@ -383,7 +393,10 @@ async fn check_family(
|
|||||||
let reconnect_budget = health_reconnect_budget(pool, dc_endpoints.len());
|
let reconnect_budget = health_reconnect_budget(pool, dc_endpoints.len());
|
||||||
let reconnect_sem = Arc::new(Semaphore::new(reconnect_budget));
|
let reconnect_sem = Arc::new(Semaphore::new(reconnect_budget));
|
||||||
|
|
||||||
if pool.floor_mode() == MeFloorMode::Static {}
|
if pool.floor_mode() == MeFloorMode::Static {
|
||||||
|
adaptive_idle_since.clear();
|
||||||
|
adaptive_recover_until.clear();
|
||||||
|
}
|
||||||
|
|
||||||
let mut live_addr_counts = HashMap::<(i32, SocketAddr), usize>::new();
|
let mut live_addr_counts = HashMap::<(i32, SocketAddr), usize>::new();
|
||||||
let mut live_writer_ids_by_addr = HashMap::<(i32, SocketAddr), Vec<u64>>::new();
|
let mut live_writer_ids_by_addr = HashMap::<(i32, SocketAddr), Vec<u64>>::new();
|
||||||
@@ -422,6 +435,8 @@ async fn check_family(
|
|||||||
&live_addr_counts,
|
&live_addr_counts,
|
||||||
&live_writer_ids_by_addr,
|
&live_writer_ids_by_addr,
|
||||||
&bound_clients_by_writer,
|
&bound_clients_by_writer,
|
||||||
|
adaptive_idle_since,
|
||||||
|
adaptive_recover_until,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
pool.set_adaptive_floor_runtime_caps(
|
pool.set_adaptive_floor_runtime_caps(
|
||||||
@@ -488,6 +503,8 @@ async fn check_family(
|
|||||||
outage_next_attempt.remove(&key);
|
outage_next_attempt.remove(&key);
|
||||||
shadow_rotate_deadline.remove(&key);
|
shadow_rotate_deadline.remove(&key);
|
||||||
idle_refresh_next_attempt.remove(&key);
|
idle_refresh_next_attempt.remove(&key);
|
||||||
|
adaptive_idle_since.remove(&key);
|
||||||
|
adaptive_recover_until.remove(&key);
|
||||||
info!(
|
info!(
|
||||||
dc = %dc,
|
dc = %dc,
|
||||||
?family,
|
?family,
|
||||||
@@ -615,28 +632,22 @@ async fn check_family(
|
|||||||
restored += 1;
|
restored += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
pool_for_reconnect
|
||||||
let base_req = pool_for_reconnect
|
.stats
|
||||||
.required_writers_for_dc_with_floor_mode(endpoints_for_dc.len(), false);
|
.increment_me_floor_cap_block_total();
|
||||||
if alive + restored >= base_req {
|
pool_for_reconnect
|
||||||
pool_for_reconnect
|
.stats
|
||||||
.stats
|
.increment_me_floor_swap_idle_failed_total();
|
||||||
.increment_me_floor_cap_block_total();
|
debug!(
|
||||||
pool_for_reconnect
|
dc = %dc,
|
||||||
.stats
|
?family,
|
||||||
.increment_me_floor_swap_idle_failed_total();
|
alive,
|
||||||
debug!(
|
required,
|
||||||
dc = %dc,
|
active_cap_effective_total,
|
||||||
?family,
|
"Adaptive floor cap reached, reconnect attempt blocked"
|
||||||
alive,
|
);
|
||||||
required,
|
break;
|
||||||
active_cap_effective_total,
|
|
||||||
"Adaptive floor cap reached, reconnect attempt blocked"
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pool_for_reconnect.stats.increment_me_reconnect_attempt();
|
|
||||||
let res = tokio::time::timeout(
|
let res = tokio::time::timeout(
|
||||||
pool_for_reconnect.reconnect_runtime.me_one_timeout,
|
pool_for_reconnect.reconnect_runtime.me_one_timeout,
|
||||||
pool_for_reconnect.connect_endpoints_round_robin(
|
pool_for_reconnect.connect_endpoints_round_robin(
|
||||||
@@ -652,9 +663,11 @@ async fn check_family(
|
|||||||
pool_for_reconnect.stats.increment_me_reconnect_success();
|
pool_for_reconnect.stats.increment_me_reconnect_success();
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(false) => {
|
||||||
|
pool_for_reconnect.stats.increment_me_reconnect_attempt();
|
||||||
debug!(dc = %dc, ?family, "ME round-robin reconnect failed")
|
debug!(dc = %dc, ?family, "ME round-robin reconnect failed")
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
pool_for_reconnect.stats.increment_me_reconnect_attempt();
|
||||||
debug!(dc = %dc, ?family, "ME reconnect timed out");
|
debug!(dc = %dc, ?family, "ME reconnect timed out");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -665,8 +678,10 @@ async fn check_family(
|
|||||||
key,
|
key,
|
||||||
dc,
|
dc,
|
||||||
family,
|
family,
|
||||||
|
alive,
|
||||||
required,
|
required,
|
||||||
endpoint_count: endpoints_for_dc.len(),
|
endpoint_count: endpoints_for_dc.len(),
|
||||||
|
restored,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -680,7 +695,7 @@ async fn check_family(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let now_alive = live_active_writers_for_dc_family(pool, outcome.dc, outcome.family).await;
|
let now_alive = outcome.alive + outcome.restored;
|
||||||
if now_alive >= outcome.required {
|
if now_alive >= outcome.required {
|
||||||
info!(
|
info!(
|
||||||
dc = %outcome.dc,
|
dc = %outcome.dc,
|
||||||
@@ -836,33 +851,6 @@ fn should_emit_rate_limited_warn(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn live_active_writers_for_dc_family(pool: &Arc<MePool>, dc: i32, family: IpFamily) -> usize {
|
|
||||||
let writers = pool.writers.read().await;
|
|
||||||
writers
|
|
||||||
.iter()
|
|
||||||
.filter(|writer| {
|
|
||||||
if writer.draining.load(std::sync::atomic::Ordering::Relaxed) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if writer.writer_dc != dc {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if !matches!(
|
|
||||||
super::pool::WriterContour::from_u8(
|
|
||||||
writer.contour.load(std::sync::atomic::Ordering::Relaxed),
|
|
||||||
),
|
|
||||||
super::pool::WriterContour::Active
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
match family {
|
|
||||||
IpFamily::V4 => writer.addr.is_ipv4(),
|
|
||||||
IpFamily::V6 => writer.addr.is_ipv6(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn adaptive_floor_class_min(
|
fn adaptive_floor_class_min(
|
||||||
pool: &Arc<MePool>,
|
pool: &Arc<MePool>,
|
||||||
endpoint_count: usize,
|
endpoint_count: usize,
|
||||||
@@ -916,6 +904,8 @@ async fn build_family_floor_plan(
|
|||||||
live_addr_counts: &HashMap<(i32, SocketAddr), usize>,
|
live_addr_counts: &HashMap<(i32, SocketAddr), usize>,
|
||||||
live_writer_ids_by_addr: &HashMap<(i32, SocketAddr), Vec<u64>>,
|
live_writer_ids_by_addr: &HashMap<(i32, SocketAddr), Vec<u64>>,
|
||||||
bound_clients_by_writer: &HashMap<u64, usize>,
|
bound_clients_by_writer: &HashMap<u64, usize>,
|
||||||
|
adaptive_idle_since: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
|
adaptive_recover_until: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
) -> FamilyFloorPlan {
|
) -> FamilyFloorPlan {
|
||||||
let mut entries = Vec::<DcFloorPlanEntry>::new();
|
let mut entries = Vec::<DcFloorPlanEntry>::new();
|
||||||
let mut by_dc = HashMap::<i32, DcFloorPlanEntry>::new();
|
let mut by_dc = HashMap::<i32, DcFloorPlanEntry>::new();
|
||||||
@@ -931,7 +921,18 @@ async fn build_family_floor_plan(
|
|||||||
if endpoints.is_empty() {
|
if endpoints.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let _key = (*dc, family);
|
let key = (*dc, family);
|
||||||
|
let reduce_for_idle = should_reduce_floor_for_idle(
|
||||||
|
pool,
|
||||||
|
key,
|
||||||
|
*dc,
|
||||||
|
endpoints,
|
||||||
|
live_writer_ids_by_addr,
|
||||||
|
bound_clients_by_writer,
|
||||||
|
adaptive_idle_since,
|
||||||
|
adaptive_recover_until,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
let base_required = pool.required_writers_for_dc(endpoints.len()).max(1);
|
let base_required = pool.required_writers_for_dc(endpoints.len()).max(1);
|
||||||
let min_required = if is_adaptive {
|
let min_required = if is_adaptive {
|
||||||
adaptive_floor_class_min(pool, endpoints.len(), base_required)
|
adaptive_floor_class_min(pool, endpoints.len(), base_required)
|
||||||
@@ -946,11 +947,11 @@ async fn build_family_floor_plan(
|
|||||||
if max_required < min_required {
|
if max_required < min_required {
|
||||||
max_required = min_required;
|
max_required = min_required;
|
||||||
}
|
}
|
||||||
// We initialize target_required at base_required to prevent 0-writer blackouts
|
let desired_raw = if is_adaptive && reduce_for_idle {
|
||||||
// caused by proactively dropping an idle DC to a single fragile connection.
|
min_required
|
||||||
// The Adaptive Floor constraint loop below will gracefully compress idle DCs
|
} else {
|
||||||
// (prioritized via has_bound_clients = false) to min_required only when global capacity is reached.
|
base_required
|
||||||
let desired_raw = base_required;
|
};
|
||||||
let target_required = desired_raw.clamp(min_required, max_required);
|
let target_required = desired_raw.clamp(min_required, max_required);
|
||||||
let alive = endpoints
|
let alive = endpoints
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1277,6 +1278,43 @@ async fn maybe_refresh_idle_writer_for_dc(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn should_reduce_floor_for_idle(
|
||||||
|
pool: &Arc<MePool>,
|
||||||
|
key: (i32, IpFamily),
|
||||||
|
dc: i32,
|
||||||
|
endpoints: &[SocketAddr],
|
||||||
|
live_writer_ids_by_addr: &HashMap<(i32, SocketAddr), Vec<u64>>,
|
||||||
|
bound_clients_by_writer: &HashMap<u64, usize>,
|
||||||
|
adaptive_idle_since: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
|
adaptive_recover_until: &mut HashMap<(i32, IpFamily), Instant>,
|
||||||
|
) -> bool {
|
||||||
|
if pool.floor_mode() != MeFloorMode::Adaptive {
|
||||||
|
adaptive_idle_since.remove(&key);
|
||||||
|
adaptive_recover_until.remove(&key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let now = Instant::now();
|
||||||
|
let writer_ids = list_writer_ids_for_endpoints(dc, endpoints, live_writer_ids_by_addr);
|
||||||
|
let has_bound_clients = has_bound_clients_on_endpoint(&writer_ids, bound_clients_by_writer);
|
||||||
|
if has_bound_clients {
|
||||||
|
adaptive_idle_since.remove(&key);
|
||||||
|
adaptive_recover_until.insert(key, now + pool.adaptive_floor_recover_grace_duration());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(recover_until) = adaptive_recover_until.get(&key)
|
||||||
|
&& now < *recover_until
|
||||||
|
{
|
||||||
|
adaptive_idle_since.remove(&key);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
adaptive_recover_until.remove(&key);
|
||||||
|
|
||||||
|
let idle_since = adaptive_idle_since.entry(key).or_insert(now);
|
||||||
|
now.saturating_duration_since(*idle_since) >= pool.adaptive_floor_idle_duration()
|
||||||
|
}
|
||||||
|
|
||||||
fn has_bound_clients_on_endpoint(
|
fn has_bound_clients_on_endpoint(
|
||||||
writer_ids: &[u64],
|
writer_ids: &[u64],
|
||||||
bound_clients_by_writer: &HashMap<u64, usize>,
|
bound_clients_by_writer: &HashMap<u64, usize>,
|
||||||
@@ -1326,7 +1364,6 @@ async fn recover_single_endpoint_outage(
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
pool.stats.increment_me_reconnect_attempt();
|
|
||||||
pool.stats
|
pool.stats
|
||||||
.increment_me_single_endpoint_outage_reconnect_attempt_total();
|
.increment_me_single_endpoint_outage_reconnect_attempt_total();
|
||||||
|
|
||||||
@@ -1402,6 +1439,7 @@ async fn recover_single_endpoint_outage(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pool.stats.increment_me_reconnect_attempt();
|
||||||
let current_ms = *outage_backoff.get(&key).unwrap_or(&min_backoff_ms);
|
let current_ms = *outage_backoff.get(&key).unwrap_or(&min_backoff_ms);
|
||||||
let next_ms = current_ms.saturating_mul(2).min(max_backoff_ms);
|
let next_ms = current_ms.saturating_mul(2).min(max_backoff_ms);
|
||||||
outage_backoff.insert(key, next_ms);
|
outage_backoff.insert(key, next_ms);
|
||||||
|
|||||||
@@ -67,7 +67,6 @@ pub fn format_sample_line(sample: &MePingSample) -> String {
|
|||||||
fn format_direct_with_config(
|
fn format_direct_with_config(
|
||||||
interface: &Option<String>,
|
interface: &Option<String>,
|
||||||
bind_addresses: &Option<Vec<String>>,
|
bind_addresses: &Option<Vec<String>>,
|
||||||
bindtodevice: &Option<String>,
|
|
||||||
) -> Option<String> {
|
) -> Option<String> {
|
||||||
let mut direct_parts: Vec<String> = Vec::new();
|
let mut direct_parts: Vec<String> = Vec::new();
|
||||||
if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) {
|
if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) {
|
||||||
@@ -76,9 +75,6 @@ fn format_direct_with_config(
|
|||||||
if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
||||||
direct_parts.push(format!("src={}", src.join(",")));
|
direct_parts.push(format!("src={}", src.join(",")));
|
||||||
}
|
}
|
||||||
if let Some(device) = bindtodevice.as_deref().filter(|v| !v.is_empty()) {
|
|
||||||
direct_parts.push(format!("bindtodevice={device}"));
|
|
||||||
}
|
|
||||||
if direct_parts.is_empty() {
|
if direct_parts.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -235,11 +231,8 @@ pub async fn format_me_route(
|
|||||||
UpstreamType::Direct {
|
UpstreamType::Direct {
|
||||||
interface,
|
interface,
|
||||||
bind_addresses,
|
bind_addresses,
|
||||||
bindtodevice,
|
|
||||||
} => {
|
} => {
|
||||||
if let Some(route) =
|
if let Some(route) = format_direct_with_config(interface, bind_addresses) {
|
||||||
format_direct_with_config(interface, bind_addresses, bindtodevice)
|
|
||||||
{
|
|
||||||
route
|
route
|
||||||
} else {
|
} else {
|
||||||
detect_direct_route_details(reports, prefer_ipv6, v4_ok, v6_ok)
|
detect_direct_route_details(reports, prefer_ipv6, v4_ok, v6_ok)
|
||||||
|
|||||||
@@ -1422,6 +1422,22 @@ impl MePool {
|
|||||||
MeFloorMode::from_u8(self.floor_runtime.me_floor_mode.load(Ordering::Relaxed))
|
MeFloorMode::from_u8(self.floor_runtime.me_floor_mode.load(Ordering::Relaxed))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn adaptive_floor_idle_duration(&self) -> Duration {
|
||||||
|
Duration::from_secs(
|
||||||
|
self.floor_runtime
|
||||||
|
.me_adaptive_floor_idle_secs
|
||||||
|
.load(Ordering::Relaxed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn adaptive_floor_recover_grace_duration(&self) -> Duration {
|
||||||
|
Duration::from_secs(
|
||||||
|
self.floor_runtime
|
||||||
|
.me_adaptive_floor_recover_grace_secs
|
||||||
|
.load(Ordering::Relaxed),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn adaptive_floor_min_writers_multi_endpoint(&self) -> usize {
|
pub(super) fn adaptive_floor_min_writers_multi_endpoint(&self) -> usize {
|
||||||
(self
|
(self
|
||||||
.floor_runtime
|
.floor_runtime
|
||||||
@@ -1643,7 +1659,6 @@ impl MePool {
|
|||||||
&self,
|
&self,
|
||||||
contour: WriterContour,
|
contour: WriterContour,
|
||||||
allow_coverage_override: bool,
|
allow_coverage_override: bool,
|
||||||
writer_dc: i32,
|
|
||||||
) -> bool {
|
) -> bool {
|
||||||
let (active_writers, warm_writers, _) = self.non_draining_writer_counts_by_contour().await;
|
let (active_writers, warm_writers, _) = self.non_draining_writer_counts_by_contour().await;
|
||||||
match contour {
|
match contour {
|
||||||
@@ -1655,43 +1670,6 @@ impl MePool {
|
|||||||
if !allow_coverage_override {
|
if !allow_coverage_override {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut endpoints_len = 0;
|
|
||||||
let now_epoch = Self::now_epoch_secs();
|
|
||||||
if self.family_enabled_for_drain_coverage(IpFamily::V4, now_epoch) {
|
|
||||||
if let Some(addrs) = self.proxy_map_v4.read().await.get(&writer_dc) {
|
|
||||||
endpoints_len += addrs.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if self.family_enabled_for_drain_coverage(IpFamily::V6, now_epoch) {
|
|
||||||
if let Some(addrs) = self.proxy_map_v6.read().await.get(&writer_dc) {
|
|
||||||
endpoints_len += addrs.len();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpoints_len > 0 {
|
|
||||||
let base_req =
|
|
||||||
self.required_writers_for_dc_with_floor_mode(endpoints_len, false);
|
|
||||||
let active_for_dc = {
|
|
||||||
let ws = self.writers.read().await;
|
|
||||||
ws.iter()
|
|
||||||
.filter(|w| {
|
|
||||||
!w.draining.load(std::sync::atomic::Ordering::Relaxed)
|
|
||||||
&& w.writer_dc == writer_dc
|
|
||||||
&& matches!(
|
|
||||||
WriterContour::from_u8(
|
|
||||||
w.contour.load(std::sync::atomic::Ordering::Relaxed),
|
|
||||||
),
|
|
||||||
WriterContour::Active
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.count()
|
|
||||||
};
|
|
||||||
if active_for_dc < base_req {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let coverage_required = self.active_coverage_required_total().await;
|
let coverage_required = self.active_coverage_required_total().await;
|
||||||
active_writers < coverage_required
|
active_writers < coverage_required
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,12 +77,6 @@ impl MePool {
|
|||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoints.len() == 1 && self.single_endpoint_outage_disable_quarantine() {
|
|
||||||
let mut guard = self.endpoint_quarantine.lock().await;
|
|
||||||
guard.retain(|_, expiry| *expiry > Instant::now());
|
|
||||||
return endpoints.to_vec();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut guard = self.endpoint_quarantine.lock().await;
|
let mut guard = self.endpoint_quarantine.lock().await;
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
guard.retain(|_, expiry| *expiry > now);
|
guard.retain(|_, expiry| *expiry > now);
|
||||||
@@ -242,18 +236,8 @@ impl MePool {
|
|||||||
let fast_retries = self.reconnect_runtime.me_reconnect_fast_retry_count.max(1);
|
let fast_retries = self.reconnect_runtime.me_reconnect_fast_retry_count.max(1);
|
||||||
let mut total_attempts = 0u32;
|
let mut total_attempts = 0u32;
|
||||||
let same_endpoint_quarantined = self.is_endpoint_quarantined(addr).await;
|
let same_endpoint_quarantined = self.is_endpoint_quarantined(addr).await;
|
||||||
let dc_endpoints = self.endpoints_for_dc(writer_dc).await;
|
|
||||||
let single_endpoint_dc = dc_endpoints.len() == 1 && dc_endpoints[0] == addr;
|
|
||||||
let bypass_quarantine_for_single_endpoint =
|
|
||||||
single_endpoint_dc && self.single_endpoint_outage_disable_quarantine();
|
|
||||||
|
|
||||||
if !same_endpoint_quarantined || bypass_quarantine_for_single_endpoint {
|
if !same_endpoint_quarantined {
|
||||||
if same_endpoint_quarantined && bypass_quarantine_for_single_endpoint {
|
|
||||||
debug!(
|
|
||||||
%addr,
|
|
||||||
"Bypassing quarantine for immediate reconnect on single-endpoint DC"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for attempt in 0..fast_retries {
|
for attempt in 0..fast_retries {
|
||||||
if total_attempts >= ME_REFILL_TOTAL_ATTEMPT_CAP {
|
if total_attempts >= ME_REFILL_TOTAL_ATTEMPT_CAP {
|
||||||
break;
|
break;
|
||||||
@@ -292,6 +276,7 @@ impl MePool {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dc_endpoints = self.endpoints_for_dc(writer_dc).await;
|
||||||
if dc_endpoints.is_empty() {
|
if dc_endpoints.is_empty() {
|
||||||
self.stats.increment_me_refill_failed_total();
|
self.stats.increment_me_refill_failed_total();
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -342,7 +342,7 @@ impl MePool {
|
|||||||
allow_coverage_override: bool,
|
allow_coverage_override: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if !self
|
if !self
|
||||||
.can_open_writer_for_contour(contour, allow_coverage_override, writer_dc)
|
.can_open_writer_for_contour(contour, allow_coverage_override)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
return Err(ProxyError::Proxy(format!(
|
return Err(ProxyError::Proxy(format!(
|
||||||
|
|||||||
@@ -109,16 +109,18 @@ async fn connectable_endpoints_waits_until_quarantine_expires() {
|
|||||||
|
|
||||||
{
|
{
|
||||||
let mut guard = pool.endpoint_quarantine.lock().await;
|
let mut guard = pool.endpoint_quarantine.lock().await;
|
||||||
guard.insert(addr, Instant::now() + Duration::from_millis(500));
|
guard.insert(addr, Instant::now() + Duration::from_millis(80));
|
||||||
}
|
}
|
||||||
|
|
||||||
let endpoints = tokio::time::timeout(
|
let started = Instant::now();
|
||||||
Duration::from_millis(120),
|
let endpoints = pool.connectable_endpoints_for_test(&[addr]).await;
|
||||||
pool.connectable_endpoints_for_test(&[addr]),
|
let elapsed = started.elapsed();
|
||||||
)
|
|
||||||
.await
|
|
||||||
.expect("single-endpoint outage mode should bypass quarantine delay");
|
|
||||||
assert_eq!(endpoints, vec![addr]);
|
assert_eq!(endpoints, vec![addr]);
|
||||||
|
assert!(
|
||||||
|
elapsed >= Duration::from_millis(50),
|
||||||
|
"single-endpoint DC should honor quarantine before retry"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -158,56 +158,6 @@ pub fn create_outgoing_socket_bound(addr: SocketAddr, bind_addr: Option<IpAddr>)
|
|||||||
Ok(socket)
|
Ok(socket)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pin an outgoing socket to a specific Linux network interface via SO_BINDTODEVICE.
|
|
||||||
#[cfg(target_os = "linux")]
|
|
||||||
pub fn bind_outgoing_socket_to_device(socket: &Socket, device: &str) -> Result<()> {
|
|
||||||
use std::io::{Error, ErrorKind};
|
|
||||||
use std::os::fd::AsRawFd;
|
|
||||||
|
|
||||||
let name = device.trim();
|
|
||||||
if name.is_empty() {
|
|
||||||
return Err(Error::new(
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"bindtodevice must not be empty",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The kernel expects an interface name buffer with a trailing NUL.
|
|
||||||
if name.len() >= libc::IFNAMSIZ {
|
|
||||||
return Err(Error::new(
|
|
||||||
ErrorKind::InvalidInput,
|
|
||||||
"bindtodevice exceeds IFNAMSIZ",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut ifname = [0u8; libc::IFNAMSIZ];
|
|
||||||
ifname[..name.len()].copy_from_slice(name.as_bytes());
|
|
||||||
|
|
||||||
let rc = unsafe {
|
|
||||||
libc::setsockopt(
|
|
||||||
socket.as_raw_fd(),
|
|
||||||
libc::SOL_SOCKET,
|
|
||||||
libc::SO_BINDTODEVICE,
|
|
||||||
ifname.as_ptr().cast::<libc::c_void>(),
|
|
||||||
(name.len() + 1) as libc::socklen_t,
|
|
||||||
)
|
|
||||||
};
|
|
||||||
if rc != 0 {
|
|
||||||
return Err(Error::last_os_error());
|
|
||||||
}
|
|
||||||
debug!("Pinned outgoing socket to interface {}", name);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stub for non-Linux targets where SO_BINDTODEVICE is unavailable.
|
|
||||||
#[cfg(not(target_os = "linux"))]
|
|
||||||
pub fn bind_outgoing_socket_to_device(_socket: &Socket, _device: &str) -> Result<()> {
|
|
||||||
use std::io::{Error, ErrorKind};
|
|
||||||
Err(Error::new(
|
|
||||||
ErrorKind::Unsupported,
|
|
||||||
"bindtodevice is supported only on Linux",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get local address of a socket
|
/// Get local address of a socket
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub fn get_local_addr(stream: &TcpStream) -> Option<SocketAddr> {
|
pub fn get_local_addr(stream: &TcpStream) -> Option<SocketAddr> {
|
||||||
|
|||||||
+25
-233
@@ -26,9 +26,7 @@ use crate::stats::Stats;
|
|||||||
use crate::transport::shadowsocks::{
|
use crate::transport::shadowsocks::{
|
||||||
ShadowsocksStream, connect_shadowsocks, sanitize_shadowsocks_url,
|
ShadowsocksStream, connect_shadowsocks, sanitize_shadowsocks_url,
|
||||||
};
|
};
|
||||||
use crate::transport::socket::{
|
use crate::transport::socket::{create_outgoing_socket_bound, resolve_interface_ip};
|
||||||
bind_outgoing_socket_to_device, create_outgoing_socket_bound, resolve_interface_ip,
|
|
||||||
};
|
|
||||||
use crate::transport::socks::{connect_socks4, connect_socks5};
|
use crate::transport::socks::{connect_socks4, connect_socks5};
|
||||||
|
|
||||||
/// Number of Telegram datacenters
|
/// Number of Telegram datacenters
|
||||||
@@ -329,17 +327,6 @@ 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,
|
||||||
@@ -466,87 +453,6 @@ 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;
|
||||||
@@ -820,28 +726,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, bind_rr, dc_preference) = {
|
let mut upstream = {
|
||||||
let guard = self.upstreams.read().await;
|
let guard = self.upstreams.read().await;
|
||||||
let state = &guard[idx];
|
guard[idx].config.clone()
|
||||||
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 target = if dc_idx.is_some() {
|
let bind_rr = {
|
||||||
Self::resolve_runtime_dc_target(target, dc_idx, &upstream, dc_preference)?
|
let guard = self.upstreams.read().await;
|
||||||
} else {
|
guard.get(idx).map(|u| u.bind_rr.clone())
|
||||||
target
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (stream, _) = self
|
let (stream, _) = self
|
||||||
@@ -862,18 +758,9 @@ 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, bind_rr, dc_preference) = {
|
let mut upstream = {
|
||||||
let guard = self.upstreams.read().await;
|
let guard = self.upstreams.read().await;
|
||||||
let state = &guard[idx];
|
guard[idx].config.clone()
|
||||||
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
|
||||||
@@ -881,10 +768,9 @@ impl UpstreamManager {
|
|||||||
upstream.selected_scope = s.to_string();
|
upstream.selected_scope = s.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let target = if dc_idx.is_some() {
|
let bind_rr = {
|
||||||
Self::resolve_runtime_dc_target(target, dc_idx, &upstream, dc_preference)?
|
let guard = self.upstreams.read().await;
|
||||||
} else {
|
guard.get(idx).map(|u| u.bind_rr.clone())
|
||||||
target
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let (stream, egress) = self
|
let (stream, egress) = self
|
||||||
@@ -1042,7 +928,6 @@ impl UpstreamManager {
|
|||||||
UpstreamType::Direct {
|
UpstreamType::Direct {
|
||||||
interface,
|
interface,
|
||||||
bind_addresses,
|
bind_addresses,
|
||||||
bindtodevice,
|
|
||||||
} => {
|
} => {
|
||||||
let bind_ip = Self::resolve_bind_address(
|
let bind_ip = Self::resolve_bind_address(
|
||||||
interface,
|
interface,
|
||||||
@@ -1058,10 +943,6 @@ impl UpstreamManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let socket = create_outgoing_socket_bound(target, bind_ip)?;
|
let socket = create_outgoing_socket_bound(target, bind_ip)?;
|
||||||
if let Some(device) = bindtodevice.as_deref().filter(|value| !value.is_empty()) {
|
|
||||||
bind_outgoing_socket_to_device(&socket, device).map_err(ProxyError::Io)?;
|
|
||||||
debug!(bindtodevice = %device, target = %target, "Pinned socket to interface");
|
|
||||||
}
|
|
||||||
if let Some(ip) = bind_ip {
|
if let Some(ip) = bind_ip {
|
||||||
debug!(bind = %ip, target = %target, "Bound outgoing socket");
|
debug!(bind = %ip, target = %target, "Bound outgoing socket");
|
||||||
} else if interface.is_some() || bind_addresses.is_some() {
|
} else if interface.is_some() || bind_addresses.is_some() {
|
||||||
@@ -1320,26 +1201,14 @@ impl UpstreamManager {
|
|||||||
.map(|(i, u)| (i, u.config.clone(), u.bind_rr.clone()))
|
.map(|(i, u)| (i, u.config.clone(), u.bind_rr.clone()))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
let has_unscoped = upstreams
|
|
||||||
.iter()
|
|
||||||
.any(|(_, cfg, _)| Self::is_unscoped_upstream(cfg));
|
|
||||||
|
|
||||||
let mut all_results = Vec::new();
|
let mut all_results = Vec::new();
|
||||||
|
|
||||||
for (upstream_idx, upstream_config, bind_rr) in &upstreams {
|
for (upstream_idx, upstream_config, bind_rr) in &upstreams {
|
||||||
// DC connectivity checks should follow the default routing path.
|
|
||||||
// Scoped upstreams are included only when no unscoped upstream exists.
|
|
||||||
if !Self::should_check_in_default_dc_connectivity(has_unscoped, upstream_config) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (upstream_ipv4_enabled, upstream_ipv6_enabled) =
|
|
||||||
Self::resolve_probe_dc_families(upstream_config, ipv4_enabled, ipv6_enabled);
|
|
||||||
let upstream_name = match &upstream_config.upstream_type {
|
let upstream_name = match &upstream_config.upstream_type {
|
||||||
UpstreamType::Direct {
|
UpstreamType::Direct {
|
||||||
interface,
|
interface,
|
||||||
bind_addresses,
|
bind_addresses,
|
||||||
bindtodevice,
|
|
||||||
} => {
|
} => {
|
||||||
let mut direct_parts = Vec::new();
|
let mut direct_parts = Vec::new();
|
||||||
if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) {
|
if let Some(dev) = interface.as_deref().filter(|v| !v.is_empty()) {
|
||||||
@@ -1348,9 +1217,6 @@ impl UpstreamManager {
|
|||||||
if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
if let Some(src) = bind_addresses.as_ref().filter(|v| !v.is_empty()) {
|
||||||
direct_parts.push(format!("src={}", src.join(",")));
|
direct_parts.push(format!("src={}", src.join(",")));
|
||||||
}
|
}
|
||||||
if let Some(device) = bindtodevice.as_deref().filter(|v| !v.is_empty()) {
|
|
||||||
direct_parts.push(format!("bindtodevice={device}"));
|
|
||||||
}
|
|
||||||
if direct_parts.is_empty() {
|
if direct_parts.is_empty() {
|
||||||
"direct".to_string()
|
"direct".to_string()
|
||||||
} else {
|
} else {
|
||||||
@@ -1367,7 +1233,7 @@ impl UpstreamManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut v6_results = Vec::with_capacity(NUM_DCS);
|
let mut v6_results = Vec::with_capacity(NUM_DCS);
|
||||||
if upstream_ipv6_enabled {
|
if 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);
|
||||||
@@ -1418,17 +1284,13 @@ 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(if ipv6_enabled {
|
error: Some("ipv6 disabled".to_string()),
|
||||||
"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 upstream_ipv4_enabled {
|
if 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);
|
||||||
@@ -1479,11 +1341,7 @@ 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(if ipv4_enabled {
|
error: Some("ipv4 disabled".to_string()),
|
||||||
"ipv4 disabled by upstream policy".to_string()
|
|
||||||
} else {
|
|
||||||
"ipv4 disabled".to_string()
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1503,9 +1361,7 @@ 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 && !upstream_ipv6_enabled)
|
if (is_v6 && !ipv6_enabled) || (!is_v6 && !ipv4_enabled) {
|
||||||
|| (!is_v6 && !upstream_ipv4_enabled)
|
|
||||||
{
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let result = tokio::time::timeout(
|
let result = tokio::time::timeout(
|
||||||
@@ -1740,32 +1596,13 @@ impl UpstreamManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let target_upstreams: Vec<usize> = {
|
let count = self.upstreams.read().await.len();
|
||||||
let guard = self.upstreams.read().await;
|
for i in 0..count {
|
||||||
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();
|
||||||
@@ -1781,30 +1618,14 @@ impl UpstreamManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let filtered_endpoints: Vec<SocketAddr> = endpoints
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|endpoint| {
|
|
||||||
if endpoint.is_ipv4() {
|
|
||||||
upstream_ipv4_enabled
|
|
||||||
} else {
|
|
||||||
upstream_ipv6_enabled
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if filtered_endpoints.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rotation_key = (i, group.dc_idx, is_primary);
|
let rotation_key = (i, group.dc_idx, is_primary);
|
||||||
let start_idx = *endpoint_rotation.entry(rotation_key).or_insert(0)
|
let start_idx =
|
||||||
% filtered_endpoints.len();
|
*endpoint_rotation.entry(rotation_key).or_insert(0) % endpoints.len();
|
||||||
let mut next_idx = (start_idx + 1) % filtered_endpoints.len();
|
let mut next_idx = (start_idx + 1) % endpoints.len();
|
||||||
|
|
||||||
for step in 0..filtered_endpoints.len() {
|
for step in 0..endpoints.len() {
|
||||||
let endpoint_idx = (start_idx + step) % filtered_endpoints.len();
|
let endpoint_idx = (start_idx + step) % endpoints.len();
|
||||||
let endpoint = filtered_endpoints[endpoint_idx];
|
let endpoint = endpoints[endpoint_idx];
|
||||||
|
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
let result = tokio::time::timeout(
|
let result = tokio::time::timeout(
|
||||||
@@ -1823,7 +1644,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) % filtered_endpoints.len();
|
next_idx = (endpoint_idx + 1) % endpoints.len();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Ok(Err(e)) => {
|
Ok(Err(e)) => {
|
||||||
@@ -2038,33 +1859,6 @@ 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();
|
||||||
@@ -2105,8 +1899,6 @@ 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,
|
||||||
|
|||||||
Reference in New Issue
Block a user