mirror of
https://github.com/telemt/telemt.git
synced 2026-06-26 04:41:10 +03:00
Compare commits
19 Commits
7b25f62438
...
3.3.38
| Author | SHA1 | Date | |
|---|---|---|---|
| e630ea0045 | |||
| 4574e423c6 | |||
| 5f5582865e | |||
| 1f54e4a203 | |||
| defa37da05 | |||
| 5fd058b6fd | |||
| 977ee53b72 | |||
| 5b11522620 | |||
| 8fe6fcb7eb | |||
| 486e439ae6 | |||
| 8e7b27a16d | |||
| 7f0057acd7 | |||
| c2f16a343a | |||
| bb6237151c | |||
| f6704d7d65 | |||
| 3d20002e56 | |||
| 8fcd0fa950 | |||
| 645e968778 | |||
| b46216d357 |
@@ -1,58 +0,0 @@
|
|||||||
# Architect Mode Rules for Telemt
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
graph TB
|
|
||||||
subgraph Entry
|
|
||||||
Client[Clients] --> Listener[TCP/Unix Listener]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Proxy Layer
|
|
||||||
Listener --> ClientHandler[ClientHandler]
|
|
||||||
ClientHandler --> Handshake[Handshake Validator]
|
|
||||||
Handshake --> |Valid| Relay[Relay Layer]
|
|
||||||
Handshake --> |Invalid| Masking[Masking/TLS Fronting]
|
|
||||||
end
|
|
||||||
|
|
||||||
subgraph Transport
|
|
||||||
Relay --> MiddleProxy[Middle-End Proxy Pool]
|
|
||||||
Relay --> DirectRelay[Direct DC Relay]
|
|
||||||
MiddleProxy --> TelegramDC[Telegram DCs]
|
|
||||||
DirectRelay --> TelegramDC
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
## Module Dependencies
|
|
||||||
- [`src/main.rs`](src/main.rs) - Entry point, spawns all async tasks
|
|
||||||
- [`src/config/`](src/config/) - Configuration loading with auto-migration
|
|
||||||
- [`src/error.rs`](src/error.rs) - Error types, must be used by all modules
|
|
||||||
- [`src/crypto/`](src/crypto/) - AES, SHA, random number generation
|
|
||||||
- [`src/protocol/`](src/protocol/) - MTProto constants, frame encoding, obfuscation
|
|
||||||
- [`src/stream/`](src/stream/) - Stream wrappers, buffer pool, frame codecs
|
|
||||||
- [`src/proxy/`](src/proxy/) - Client handling, handshake, relay logic
|
|
||||||
- [`src/transport/`](src/transport/) - Upstream management, middle-proxy, SOCKS support
|
|
||||||
- [`src/stats/`](src/stats/) - Statistics and replay protection
|
|
||||||
- [`src/ip_tracker.rs`](src/ip_tracker.rs) - Per-user IP tracking
|
|
||||||
|
|
||||||
## Key Architectural Constraints
|
|
||||||
|
|
||||||
### Middle-End Proxy Mode
|
|
||||||
- Requires public IP on interface OR 1:1 NAT with STUN probing
|
|
||||||
- Uses separate `proxy-secret` from Telegram (NOT user secrets)
|
|
||||||
- Falls back to direct mode automatically on STUN mismatch
|
|
||||||
|
|
||||||
### TLS Fronting
|
|
||||||
- Invalid handshakes are transparently proxied to `mask_host`
|
|
||||||
- This is critical for DPI evasion - do not change this behavior
|
|
||||||
- `mask_unix_sock` and `mask_host` are mutually exclusive
|
|
||||||
|
|
||||||
### Stream Architecture
|
|
||||||
- Buffer pool is shared globally via Arc - prevents allocation storms
|
|
||||||
- Frame codecs implement tokio-util Encoder/Decoder traits
|
|
||||||
- State machine in [`src/stream/state.rs`](src/stream/state.rs) manages stream transitions
|
|
||||||
|
|
||||||
### Configuration Migration
|
|
||||||
- [`ProxyConfig::load()`](src/config/mod.rs:641) mutates config in-place
|
|
||||||
- New fields must have sensible defaults
|
|
||||||
- DC203 override is auto-injected for CDN/media support
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Code Mode Rules for Telemt
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
- Always use [`ProxyError`](src/error.rs:168) from [`src/error.rs`](src/error.rs) for proxy operations
|
|
||||||
- [`HandshakeResult<T,R,W>`](src/error.rs:292) returns streams on bad client - these MUST be returned for masking, never dropped
|
|
||||||
- Use [`Recoverable`](src/error.rs:110) trait to check if errors are retryable
|
|
||||||
|
|
||||||
## Configuration Changes
|
|
||||||
- [`ProxyConfig::load()`](src/config/mod.rs:641) auto-mutates config - new fields should have defaults
|
|
||||||
- DC203 override is auto-injected if missing - do not remove this behavior
|
|
||||||
- When adding config fields, add migration logic in [`ProxyConfig::load()`](src/config/mod.rs:641)
|
|
||||||
|
|
||||||
## Crypto Code
|
|
||||||
- [`SecureRandom`](src/crypto/random.rs) from [`src/crypto/random.rs`](src/crypto/random.rs) must be used for all crypto operations
|
|
||||||
- Never use `rand::thread_rng()` directly - use the shared `Arc<SecureRandom>`
|
|
||||||
|
|
||||||
## Stream Handling
|
|
||||||
- Buffer pool [`BufferPool`](src/stream/buffer_pool.rs) is shared via Arc - always use it instead of allocating
|
|
||||||
- Frame codecs in [`src/stream/frame_codec.rs`](src/stream/frame_codec.rs) implement tokio-util's Encoder/Decoder traits
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
- Tests are inline in modules using `#[cfg(test)]`
|
|
||||||
- Use `cargo test --lib <module_name>` to run tests for specific modules
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Debug Mode Rules for Telemt
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
- `RUST_LOG` environment variable takes absolute priority over all config log levels
|
|
||||||
- Log levels: `trace`, `debug`, `info`, `warn`, `error`
|
|
||||||
- Use `RUST_LOG=debug cargo run` for detailed operational logs
|
|
||||||
- Use `RUST_LOG=trace cargo run` for full protocol-level debugging
|
|
||||||
|
|
||||||
## Middle-End Proxy Debugging
|
|
||||||
- Set `ME_DIAG=1` environment variable for high-precision cryptography diagnostics
|
|
||||||
- STUN probe results are logged at startup - check for mismatch between local and reflected IP
|
|
||||||
- If Middle-End fails, check `proxy_secret_path` points to valid file from https://core.telegram.org/getProxySecret
|
|
||||||
|
|
||||||
## Connection Issues
|
|
||||||
- DC connectivity is logged at startup with RTT measurements
|
|
||||||
- If DC ping fails, check `dc_overrides` for custom addresses
|
|
||||||
- Use `prefer_ipv6=false` in config if IPv6 is unreliable
|
|
||||||
|
|
||||||
## TLS Fronting Issues
|
|
||||||
- Invalid handshakes are proxied to `mask_host` - check this host is reachable
|
|
||||||
- `mask_unix_sock` and `mask_host` are mutually exclusive - only one can be set
|
|
||||||
- If `mask_unix_sock` is set, socket must exist before connections arrive
|
|
||||||
|
|
||||||
## Common Errors
|
|
||||||
- `ReplayAttack` - client replayed a handshake nonce, potential attack
|
|
||||||
- `TimeSkew` - client clock is off, can disable with `ignore_time_skew=true`
|
|
||||||
- `TgHandshakeTimeout` - upstream DC connection failed, check network
|
|
||||||
Generated
+1
-1
@@ -2780,7 +2780,7 @@ checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.3.36"
|
version = "3.3.38"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes",
|
"aes",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "telemt"
|
name = "telemt"
|
||||||
version = "3.3.36"
|
version = "3.3.38"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
***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***
|
||||||
|
|
||||||
[**Telemt Chat in Telegram**](https://t.me/telemtrs)
|
### [**Telemt Chat in Telegram**](https://t.me/telemtrs)
|
||||||
|
#### Fixed TLS ClientHello is now available in Telegram Desktop starting from version 6.7.2: to work with EE-MTProxy, please update your client;
|
||||||
|
#### Fixed TLS ClientHello for Telegram Android Client is available in [our chat](https://t.me/telemtrs/30234/36441); official releases for Android and iOS are "work in progress";
|
||||||
|
|
||||||
|
|
||||||
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
|
**Telemt** is a fast, secure, and feature-rich server written in Rust: it fully implements the official Telegram proxy algo and adds many production-ready improvements such as:
|
||||||
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md)
|
- [ME Pool + Reader/Writer + Registry + Refill + Adaptive Floor + Trio-State + Generation Lifecycle](https://github.com/telemt/telemt/blob/main/docs/model/MODEL.en.md)
|
||||||
@@ -51,8 +54,12 @@
|
|||||||
- [FAQ EN](docs/FAQ.en.md)
|
- [FAQ EN](docs/FAQ.en.md)
|
||||||
|
|
||||||
### Recognizability for DPI and crawler
|
### Recognizability for DPI and crawler
|
||||||
Since version 1.1.0.0, we have debugged masking perfectly: for all clients without "presenting" a key,
|
|
||||||
we transparently direct traffic to the target host!
|
On April 1, 2026, we became aware of a method for detecting MTProxy Fake-TLS,
|
||||||
|
based on the ECH extension and the ordering of cipher suites,
|
||||||
|
as well as an overall unique JA3/JA4 fingerprint
|
||||||
|
that does not occur in modern browsers:
|
||||||
|
we have already submitted initial changes to the Telegram Desktop developers and are working on updates for other clients.
|
||||||
|
|
||||||
- We consider this a breakthrough aspect, which has no stable analogues today
|
- 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
|
||||||
|
|||||||
+3058
-317
File diff suppressed because it is too large
Load Diff
+31
-3
@@ -20,6 +20,13 @@ TARGET_VERSION="${VERSION:-latest}"
|
|||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-h|--help) ACTION="help"; shift ;;
|
-h|--help) ACTION="help"; shift ;;
|
||||||
|
-d|--domain)
|
||||||
|
if [ "$#" -lt 2 ] || [ -z "$2" ]; then
|
||||||
|
printf '[ERROR] %s requires a domain argument.\n' "$1" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TLS_DOMAIN="$2"
|
||||||
|
shift 2 ;;
|
||||||
uninstall|--uninstall)
|
uninstall|--uninstall)
|
||||||
if [ "$ACTION" != "purge" ]; then ACTION="uninstall"; fi
|
if [ "$ACTION" != "purge" ]; then ACTION="uninstall"; fi
|
||||||
shift ;;
|
shift ;;
|
||||||
@@ -52,11 +59,12 @@ cleanup() {
|
|||||||
trap cleanup EXIT INT TERM
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
show_help() {
|
show_help() {
|
||||||
say "Usage: $0 [ <version> | install | uninstall | purge | --help ]"
|
say "Usage: $0 [ <version> | install | uninstall | purge ] [ -d <domain> ] [ --help ]"
|
||||||
say " <version> Install specific version (e.g. 3.3.15, default: latest)"
|
say " <version> Install specific version (e.g. 3.3.15, default: latest)"
|
||||||
say " install Install the latest version"
|
say " install Install the latest version"
|
||||||
say " uninstall Remove the binary and service (keeps config and user)"
|
say " uninstall Remove the binary and service (keeps config and user)"
|
||||||
say " purge Remove everything including configuration, data, and user"
|
say " purge Remove everything including configuration, data, and user"
|
||||||
|
say " -d, --domain Set TLS domain (default: petrovich.ru)"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +200,13 @@ verify_install_deps() {
|
|||||||
detect_arch() {
|
detect_arch() {
|
||||||
sys_arch="$(uname -m)"
|
sys_arch="$(uname -m)"
|
||||||
case "$sys_arch" in
|
case "$sys_arch" in
|
||||||
x86_64|amd64) echo "x86_64" ;;
|
x86_64|amd64)
|
||||||
|
if [ -r /proc/cpuinfo ] && grep -q "avx2" /proc/cpuinfo 2>/dev/null && grep -q "bmi2" /proc/cpuinfo 2>/dev/null; then
|
||||||
|
echo "x86_64-v3"
|
||||||
|
else
|
||||||
|
echo "x86_64"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
aarch64|arm64) echo "aarch64" ;;
|
aarch64|arm64) echo "aarch64" ;;
|
||||||
*) die "Unsupported architecture: $sys_arch" ;;
|
*) die "Unsupported architecture: $sys_arch" ;;
|
||||||
esac
|
esac
|
||||||
@@ -500,7 +514,21 @@ case "$ACTION" in
|
|||||||
die "Temp directory is invalid or was not created"
|
die "Temp directory is invalid or was not created"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}" || die "Download failed"
|
if ! fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}"; then
|
||||||
|
if [ "$ARCH" = "x86_64-v3" ]; then
|
||||||
|
say " -> x86_64-v3 build not found, falling back to standard x86_64..."
|
||||||
|
ARCH="x86_64"
|
||||||
|
FILE_NAME="${BIN_NAME}-${ARCH}-linux-${LIBC}.tar.gz"
|
||||||
|
if [ "$TARGET_VERSION" = "latest" ]; then
|
||||||
|
DL_URL="https://github.com/${REPO}/releases/latest/download/${FILE_NAME}"
|
||||||
|
else
|
||||||
|
DL_URL="https://github.com/${REPO}/releases/download/${TARGET_VERSION}/${FILE_NAME}"
|
||||||
|
fi
|
||||||
|
fetch_file "$DL_URL" "${TEMP_DIR}/${FILE_NAME}" || die "Download failed"
|
||||||
|
else
|
||||||
|
die "Download failed"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
say ">>> Stage 3: Extracting archive"
|
say ">>> Stage 3: Extracting archive"
|
||||||
if ! gzip -dc "${TEMP_DIR}/${FILE_NAME}" | tar -xf - -C "$TEMP_DIR" 2>/dev/null; then
|
if ! gzip -dc "${TEMP_DIR}/${FILE_NAME}" | tar -xf - -C "$TEMP_DIR" 2>/dev/null; then
|
||||||
|
|||||||
@@ -81,10 +81,21 @@ pub(super) struct ZeroCoreData {
|
|||||||
pub(super) connections_total: u64,
|
pub(super) connections_total: u64,
|
||||||
pub(super) connections_bad_total: u64,
|
pub(super) connections_bad_total: u64,
|
||||||
pub(super) handshake_timeouts_total: u64,
|
pub(super) handshake_timeouts_total: u64,
|
||||||
|
pub(super) accept_permit_timeout_total: u64,
|
||||||
pub(super) configured_users: usize,
|
pub(super) configured_users: usize,
|
||||||
pub(super) telemetry_core_enabled: bool,
|
pub(super) telemetry_core_enabled: bool,
|
||||||
pub(super) telemetry_user_enabled: bool,
|
pub(super) telemetry_user_enabled: bool,
|
||||||
pub(super) telemetry_me_level: String,
|
pub(super) telemetry_me_level: String,
|
||||||
|
pub(super) conntrack_control_enabled: bool,
|
||||||
|
pub(super) conntrack_control_available: bool,
|
||||||
|
pub(super) conntrack_pressure_active: bool,
|
||||||
|
pub(super) conntrack_event_queue_depth: u64,
|
||||||
|
pub(super) conntrack_rule_apply_ok: bool,
|
||||||
|
pub(super) conntrack_delete_attempt_total: u64,
|
||||||
|
pub(super) conntrack_delete_success_total: u64,
|
||||||
|
pub(super) conntrack_delete_not_found_total: u64,
|
||||||
|
pub(super) conntrack_delete_error_total: u64,
|
||||||
|
pub(super) conntrack_close_event_drop_total: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Clone)]
|
#[derive(Serialize, Clone)]
|
||||||
|
|||||||
@@ -39,10 +39,21 @@ pub(super) fn build_zero_all_data(stats: &Stats, configured_users: usize) -> Zer
|
|||||||
connections_total: stats.get_connects_all(),
|
connections_total: stats.get_connects_all(),
|
||||||
connections_bad_total: stats.get_connects_bad(),
|
connections_bad_total: stats.get_connects_bad(),
|
||||||
handshake_timeouts_total: stats.get_handshake_timeouts(),
|
handshake_timeouts_total: stats.get_handshake_timeouts(),
|
||||||
|
accept_permit_timeout_total: stats.get_accept_permit_timeout_total(),
|
||||||
configured_users,
|
configured_users,
|
||||||
telemetry_core_enabled: telemetry.core_enabled,
|
telemetry_core_enabled: telemetry.core_enabled,
|
||||||
telemetry_user_enabled: telemetry.user_enabled,
|
telemetry_user_enabled: telemetry.user_enabled,
|
||||||
telemetry_me_level: telemetry.me_level.to_string(),
|
telemetry_me_level: telemetry.me_level.to_string(),
|
||||||
|
conntrack_control_enabled: stats.get_conntrack_control_enabled(),
|
||||||
|
conntrack_control_available: stats.get_conntrack_control_available(),
|
||||||
|
conntrack_pressure_active: stats.get_conntrack_pressure_active(),
|
||||||
|
conntrack_event_queue_depth: stats.get_conntrack_event_queue_depth(),
|
||||||
|
conntrack_rule_apply_ok: stats.get_conntrack_rule_apply_ok(),
|
||||||
|
conntrack_delete_attempt_total: stats.get_conntrack_delete_attempt_total(),
|
||||||
|
conntrack_delete_success_total: stats.get_conntrack_delete_success_total(),
|
||||||
|
conntrack_delete_not_found_total: stats.get_conntrack_delete_not_found_total(),
|
||||||
|
conntrack_delete_error_total: stats.get_conntrack_delete_error_total(),
|
||||||
|
conntrack_close_event_drop_total: stats.get_conntrack_close_event_drop_total(),
|
||||||
},
|
},
|
||||||
upstream: build_zero_upstream_data(stats),
|
upstream: build_zero_upstream_data(stats),
|
||||||
middle_proxy: ZeroMiddleProxyData {
|
middle_proxy: ZeroMiddleProxyData {
|
||||||
|
|||||||
+23
-3
@@ -48,6 +48,10 @@ const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_BUDGET_PER_CORE: u16 = 16;
|
|||||||
const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_COOLDOWN_MS: u64 = 1000;
|
const DEFAULT_ME_POOL_DRAIN_SOFT_EVICT_COOLDOWN_MS: u64 = 1000;
|
||||||
const DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS: u64 = 30;
|
const DEFAULT_USER_MAX_UNIQUE_IPS_WINDOW_SECS: u64 = 30;
|
||||||
const DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS: u64 = 250;
|
const DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS: u64 = 250;
|
||||||
|
const DEFAULT_CONNTRACK_CONTROL_ENABLED: bool = true;
|
||||||
|
const DEFAULT_CONNTRACK_PRESSURE_HIGH_WATERMARK_PCT: u8 = 85;
|
||||||
|
const DEFAULT_CONNTRACK_PRESSURE_LOW_WATERMARK_PCT: u8 = 70;
|
||||||
|
const DEFAULT_CONNTRACK_DELETE_BUDGET_PER_SEC: u64 = 4096;
|
||||||
const DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS: u32 = 2;
|
const DEFAULT_UPSTREAM_CONNECT_RETRY_ATTEMPTS: u32 = 2;
|
||||||
const DEFAULT_UPSTREAM_UNHEALTHY_FAIL_THRESHOLD: u32 = 5;
|
const DEFAULT_UPSTREAM_UNHEALTHY_FAIL_THRESHOLD: u32 = 5;
|
||||||
const DEFAULT_UPSTREAM_CONNECT_BUDGET_MS: u64 = 3000;
|
const DEFAULT_UPSTREAM_CONNECT_BUDGET_MS: u64 = 3000;
|
||||||
@@ -96,7 +100,7 @@ pub(crate) fn default_fake_cert_len() -> usize {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tls_front_dir() -> String {
|
pub(crate) fn default_tls_front_dir() -> String {
|
||||||
"tlsfront".to_string()
|
"/etc/telemt/tlsfront".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_replay_check_len() -> usize {
|
pub(crate) fn default_replay_check_len() -> usize {
|
||||||
@@ -221,6 +225,22 @@ pub(crate) fn default_accept_permit_timeout_ms() -> u64 {
|
|||||||
DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS
|
DEFAULT_ACCEPT_PERMIT_TIMEOUT_MS
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_control_enabled() -> bool {
|
||||||
|
DEFAULT_CONNTRACK_CONTROL_ENABLED
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_pressure_high_watermark_pct() -> u8 {
|
||||||
|
DEFAULT_CONNTRACK_PRESSURE_HIGH_WATERMARK_PCT
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_pressure_low_watermark_pct() -> u8 {
|
||||||
|
DEFAULT_CONNTRACK_PRESSURE_LOW_WATERMARK_PCT
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn default_conntrack_delete_budget_per_sec() -> u64 {
|
||||||
|
DEFAULT_CONNTRACK_DELETE_BUDGET_PER_SEC
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn default_prefer_4() -> u8 {
|
pub(crate) fn default_prefer_4() -> u8 {
|
||||||
4
|
4
|
||||||
}
|
}
|
||||||
@@ -282,7 +302,7 @@ pub(crate) fn default_me2dc_fallback() -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_me2dc_fast() -> bool {
|
pub(crate) fn default_me2dc_fast() -> bool {
|
||||||
false
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_keepalive_interval() -> u64 {
|
pub(crate) fn default_keepalive_interval() -> u64 {
|
||||||
@@ -538,7 +558,7 @@ pub(crate) fn default_beobachten_flush_secs() -> u64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_beobachten_file() -> String {
|
pub(crate) fn default_beobachten_file() -> String {
|
||||||
"cache/beobachten.txt".to_string()
|
"/etc/telemt/beobachten.txt".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn default_tls_new_session_tickets() -> u8 {
|
pub(crate) fn default_tls_new_session_tickets() -> u8 {
|
||||||
|
|||||||
@@ -922,6 +922,43 @@ impl ProxyConfig {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if config.server.conntrack_control.pressure_high_watermark_pct == 0
|
||||||
|
|| config.server.conntrack_control.pressure_high_watermark_pct > 100
|
||||||
|
{
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"server.conntrack_control.pressure_high_watermark_pct must be within [1, 100]"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.server.conntrack_control.pressure_low_watermark_pct
|
||||||
|
>= config.server.conntrack_control.pressure_high_watermark_pct
|
||||||
|
{
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"server.conntrack_control.pressure_low_watermark_pct must be < pressure_high_watermark_pct"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.server.conntrack_control.delete_budget_per_sec == 0 {
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"server.conntrack_control.delete_budget_per_sec must be > 0".to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(config.server.conntrack_control.mode, ConntrackMode::Hybrid)
|
||||||
|
&& config
|
||||||
|
.server
|
||||||
|
.conntrack_control
|
||||||
|
.hybrid_listener_ips
|
||||||
|
.is_empty()
|
||||||
|
{
|
||||||
|
return Err(ProxyError::Config(
|
||||||
|
"server.conntrack_control.hybrid_listener_ips must be non-empty in mode=hybrid"
|
||||||
|
.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if config.general.effective_me_pool_force_close_secs() > 0
|
if config.general.effective_me_pool_force_close_secs() > 0
|
||||||
&& config.general.effective_me_pool_force_close_secs()
|
&& config.general.effective_me_pool_force_close_secs()
|
||||||
< config.general.me_pool_drain_ttl_secs
|
< config.general.me_pool_drain_ttl_secs
|
||||||
@@ -1327,6 +1364,31 @@ mod tests {
|
|||||||
cfg.server.api.runtime_edge_events_capacity,
|
cfg.server.api.runtime_edge_events_capacity,
|
||||||
default_api_runtime_edge_events_capacity()
|
default_api_runtime_edge_events_capacity()
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.server.conntrack_control.inline_conntrack_control,
|
||||||
|
default_conntrack_control_enabled()
|
||||||
|
);
|
||||||
|
assert_eq!(cfg.server.conntrack_control.mode, ConntrackMode::default());
|
||||||
|
assert_eq!(
|
||||||
|
cfg.server.conntrack_control.backend,
|
||||||
|
ConntrackBackend::default()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.server.conntrack_control.profile,
|
||||||
|
ConntrackPressureProfile::default()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.server.conntrack_control.pressure_high_watermark_pct,
|
||||||
|
default_conntrack_pressure_high_watermark_pct()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.server.conntrack_control.pressure_low_watermark_pct,
|
||||||
|
default_conntrack_pressure_low_watermark_pct()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.server.conntrack_control.delete_budget_per_sec,
|
||||||
|
default_conntrack_delete_budget_per_sec()
|
||||||
|
);
|
||||||
assert_eq!(cfg.access.users, default_access_users());
|
assert_eq!(cfg.access.users, default_access_users());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
cfg.access.user_max_tcp_conns_global_each,
|
cfg.access.user_max_tcp_conns_global_each,
|
||||||
@@ -1472,6 +1534,31 @@ mod tests {
|
|||||||
server.api.runtime_edge_events_capacity,
|
server.api.runtime_edge_events_capacity,
|
||||||
default_api_runtime_edge_events_capacity()
|
default_api_runtime_edge_events_capacity()
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
server.conntrack_control.inline_conntrack_control,
|
||||||
|
default_conntrack_control_enabled()
|
||||||
|
);
|
||||||
|
assert_eq!(server.conntrack_control.mode, ConntrackMode::default());
|
||||||
|
assert_eq!(
|
||||||
|
server.conntrack_control.backend,
|
||||||
|
ConntrackBackend::default()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
server.conntrack_control.profile,
|
||||||
|
ConntrackPressureProfile::default()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
server.conntrack_control.pressure_high_watermark_pct,
|
||||||
|
default_conntrack_pressure_high_watermark_pct()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
server.conntrack_control.pressure_low_watermark_pct,
|
||||||
|
default_conntrack_pressure_low_watermark_pct()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
server.conntrack_control.delete_budget_per_sec,
|
||||||
|
default_conntrack_delete_budget_per_sec()
|
||||||
|
);
|
||||||
|
|
||||||
let access = AccessConfig::default();
|
let access = AccessConfig::default();
|
||||||
assert_eq!(access.users, default_access_users());
|
assert_eq!(access.users, default_access_users());
|
||||||
@@ -2404,6 +2491,118 @@ mod tests {
|
|||||||
let _ = std::fs::remove_file(path);
|
let _ = std::fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conntrack_pressure_high_watermark_out_of_range_is_rejected() {
|
||||||
|
let toml = r#"
|
||||||
|
[server.conntrack_control]
|
||||||
|
pressure_high_watermark_pct = 0
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "example.com"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
user = "00000000000000000000000000000000"
|
||||||
|
"#;
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let path = dir.join("telemt_conntrack_high_watermark_invalid_test.toml");
|
||||||
|
std::fs::write(&path, toml).unwrap();
|
||||||
|
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||||
|
assert!(err.contains(
|
||||||
|
"server.conntrack_control.pressure_high_watermark_pct must be within [1, 100]"
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conntrack_pressure_low_watermark_must_be_below_high() {
|
||||||
|
let toml = r#"
|
||||||
|
[server.conntrack_control]
|
||||||
|
pressure_high_watermark_pct = 50
|
||||||
|
pressure_low_watermark_pct = 50
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "example.com"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
user = "00000000000000000000000000000000"
|
||||||
|
"#;
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let path = dir.join("telemt_conntrack_low_watermark_invalid_test.toml");
|
||||||
|
std::fs::write(&path, toml).unwrap();
|
||||||
|
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||||
|
assert!(
|
||||||
|
err.contains(
|
||||||
|
"server.conntrack_control.pressure_low_watermark_pct must be < pressure_high_watermark_pct"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conntrack_delete_budget_zero_is_rejected() {
|
||||||
|
let toml = r#"
|
||||||
|
[server.conntrack_control]
|
||||||
|
delete_budget_per_sec = 0
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "example.com"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
user = "00000000000000000000000000000000"
|
||||||
|
"#;
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let path = dir.join("telemt_conntrack_delete_budget_invalid_test.toml");
|
||||||
|
std::fs::write(&path, toml).unwrap();
|
||||||
|
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||||
|
assert!(err.contains("server.conntrack_control.delete_budget_per_sec must be > 0"));
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conntrack_hybrid_mode_requires_listener_allow_list() {
|
||||||
|
let toml = r#"
|
||||||
|
[server.conntrack_control]
|
||||||
|
mode = "hybrid"
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "example.com"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
user = "00000000000000000000000000000000"
|
||||||
|
"#;
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let path = dir.join("telemt_conntrack_hybrid_requires_ips_test.toml");
|
||||||
|
std::fs::write(&path, toml).unwrap();
|
||||||
|
let err = ProxyConfig::load(&path).unwrap_err().to_string();
|
||||||
|
assert!(err.contains(
|
||||||
|
"server.conntrack_control.hybrid_listener_ips must be non-empty in mode=hybrid"
|
||||||
|
));
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn conntrack_profile_is_loaded_from_config() {
|
||||||
|
let toml = r#"
|
||||||
|
[server.conntrack_control]
|
||||||
|
profile = "aggressive"
|
||||||
|
|
||||||
|
[censorship]
|
||||||
|
tls_domain = "example.com"
|
||||||
|
|
||||||
|
[access.users]
|
||||||
|
user = "00000000000000000000000000000000"
|
||||||
|
"#;
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let path = dir.join("telemt_conntrack_profile_parse_test.toml");
|
||||||
|
std::fs::write(&path, toml).unwrap();
|
||||||
|
let cfg = ProxyConfig::load(&path).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
cfg.server.conntrack_control.profile,
|
||||||
|
ConntrackPressureProfile::Aggressive
|
||||||
|
);
|
||||||
|
let _ = std::fs::remove_file(path);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn force_close_default_matches_drain_ttl() {
|
fn force_close_default_matches_drain_ttl() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
|
|||||||
@@ -1216,6 +1216,118 @@ impl Default for ApiConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConntrackMode {
|
||||||
|
#[default]
|
||||||
|
Tracked,
|
||||||
|
Notrack,
|
||||||
|
Hybrid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConntrackBackend {
|
||||||
|
#[default]
|
||||||
|
Auto,
|
||||||
|
Nftables,
|
||||||
|
Iptables,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum ConntrackPressureProfile {
|
||||||
|
Conservative,
|
||||||
|
#[default]
|
||||||
|
Balanced,
|
||||||
|
Aggressive,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ConntrackPressureProfile {
|
||||||
|
pub fn client_first_byte_idle_cap_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 30,
|
||||||
|
Self::Balanced => 20,
|
||||||
|
Self::Aggressive => 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn direct_activity_timeout_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 180,
|
||||||
|
Self::Balanced => 120,
|
||||||
|
Self::Aggressive => 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn middle_soft_idle_cap_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 60,
|
||||||
|
Self::Balanced => 30,
|
||||||
|
Self::Aggressive => 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn middle_hard_idle_cap_secs(self) -> u64 {
|
||||||
|
match self {
|
||||||
|
Self::Conservative => 180,
|
||||||
|
Self::Balanced => 90,
|
||||||
|
Self::Aggressive => 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct ConntrackControlConfig {
|
||||||
|
/// Enables runtime conntrack-control worker for pressure mitigation.
|
||||||
|
#[serde(default = "default_conntrack_control_enabled")]
|
||||||
|
pub inline_conntrack_control: bool,
|
||||||
|
|
||||||
|
/// Conntrack mode for listener ingress traffic.
|
||||||
|
#[serde(default)]
|
||||||
|
pub mode: ConntrackMode,
|
||||||
|
|
||||||
|
/// Netfilter backend used to reconcile notrack rules.
|
||||||
|
#[serde(default)]
|
||||||
|
pub backend: ConntrackBackend,
|
||||||
|
|
||||||
|
/// Pressure profile for timeout caps under resource saturation.
|
||||||
|
#[serde(default)]
|
||||||
|
pub profile: ConntrackPressureProfile,
|
||||||
|
|
||||||
|
/// Listener IP allow-list for hybrid mode.
|
||||||
|
/// Ignored in tracked/notrack mode.
|
||||||
|
#[serde(default)]
|
||||||
|
pub hybrid_listener_ips: Vec<IpAddr>,
|
||||||
|
|
||||||
|
/// Pressure high watermark as percentage.
|
||||||
|
#[serde(default = "default_conntrack_pressure_high_watermark_pct")]
|
||||||
|
pub pressure_high_watermark_pct: u8,
|
||||||
|
|
||||||
|
/// Pressure low watermark as percentage.
|
||||||
|
#[serde(default = "default_conntrack_pressure_low_watermark_pct")]
|
||||||
|
pub pressure_low_watermark_pct: u8,
|
||||||
|
|
||||||
|
/// Maximum conntrack delete operations per second.
|
||||||
|
#[serde(default = "default_conntrack_delete_budget_per_sec")]
|
||||||
|
pub delete_budget_per_sec: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ConntrackControlConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
inline_conntrack_control: default_conntrack_control_enabled(),
|
||||||
|
mode: ConntrackMode::default(),
|
||||||
|
backend: ConntrackBackend::default(),
|
||||||
|
profile: ConntrackPressureProfile::default(),
|
||||||
|
hybrid_listener_ips: Vec::new(),
|
||||||
|
pressure_high_watermark_pct: default_conntrack_pressure_high_watermark_pct(),
|
||||||
|
pressure_low_watermark_pct: default_conntrack_pressure_low_watermark_pct(),
|
||||||
|
delete_budget_per_sec: default_conntrack_delete_budget_per_sec(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ServerConfig {
|
pub struct ServerConfig {
|
||||||
#[serde(default = "default_port")]
|
#[serde(default = "default_port")]
|
||||||
@@ -1291,6 +1403,10 @@ pub struct ServerConfig {
|
|||||||
/// `0` keeps legacy unbounded wait behavior.
|
/// `0` keeps legacy unbounded wait behavior.
|
||||||
#[serde(default = "default_accept_permit_timeout_ms")]
|
#[serde(default = "default_accept_permit_timeout_ms")]
|
||||||
pub accept_permit_timeout_ms: u64,
|
pub accept_permit_timeout_ms: u64,
|
||||||
|
|
||||||
|
/// Runtime conntrack control and pressure policy.
|
||||||
|
#[serde(default)]
|
||||||
|
pub conntrack_control: ConntrackControlConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServerConfig {
|
impl Default for ServerConfig {
|
||||||
@@ -1313,6 +1429,7 @@ impl Default for ServerConfig {
|
|||||||
listen_backlog: default_listen_backlog(),
|
listen_backlog: default_listen_backlog(),
|
||||||
max_connections: default_server_max_connections(),
|
max_connections: default_server_max_connections(),
|
||||||
accept_permit_timeout_ms: default_accept_permit_timeout_ms(),
|
accept_permit_timeout_ms: default_accept_permit_timeout_ms(),
|
||||||
|
conntrack_control: ConntrackControlConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,755 @@
|
|||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::net::IpAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tokio::io::AsyncWriteExt;
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tokio::sync::{mpsc, watch};
|
||||||
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
|
use crate::config::{ConntrackBackend, ConntrackMode, ProxyConfig};
|
||||||
|
use crate::proxy::middle_relay::note_global_relay_pressure;
|
||||||
|
use crate::proxy::shared_state::{ConntrackCloseEvent, ConntrackCloseReason, ProxySharedState};
|
||||||
|
use crate::stats::Stats;
|
||||||
|
|
||||||
|
const CONNTRACK_EVENT_QUEUE_CAPACITY: usize = 32_768;
|
||||||
|
const PRESSURE_RELEASE_TICKS: u8 = 3;
|
||||||
|
const PRESSURE_SAMPLE_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum NetfilterBackend {
|
||||||
|
Nftables,
|
||||||
|
Iptables,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct PressureSample {
|
||||||
|
conn_pct: Option<u8>,
|
||||||
|
fd_pct: Option<u8>,
|
||||||
|
accept_timeout_delta: u64,
|
||||||
|
me_queue_pressure_delta: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PressureState {
|
||||||
|
active: bool,
|
||||||
|
low_streak: u8,
|
||||||
|
prev_accept_timeout_total: u64,
|
||||||
|
prev_me_queue_pressure_total: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PressureState {
|
||||||
|
fn new(stats: &Stats) -> Self {
|
||||||
|
Self {
|
||||||
|
active: false,
|
||||||
|
low_streak: 0,
|
||||||
|
prev_accept_timeout_total: stats.get_accept_permit_timeout_total(),
|
||||||
|
prev_me_queue_pressure_total: stats.get_me_c2me_send_full_total(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spawn_conntrack_controller(
|
||||||
|
config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
|
) {
|
||||||
|
if !cfg!(target_os = "linux") {
|
||||||
|
let enabled = config_rx
|
||||||
|
.borrow()
|
||||||
|
.server
|
||||||
|
.conntrack_control
|
||||||
|
.inline_conntrack_control;
|
||||||
|
stats.set_conntrack_control_enabled(enabled);
|
||||||
|
stats.set_conntrack_control_available(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_event_queue_depth(0);
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
shared.disable_conntrack_close_sender();
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
if enabled {
|
||||||
|
warn!(
|
||||||
|
"conntrack control is configured but unsupported on this OS; disabling runtime worker"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (tx, rx) = mpsc::channel(CONNTRACK_EVENT_QUEUE_CAPACITY);
|
||||||
|
shared.set_conntrack_close_sender(tx);
|
||||||
|
tokio::spawn(async move {
|
||||||
|
run_conntrack_controller(config_rx, stats, shared, rx).await;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_conntrack_controller(
|
||||||
|
mut config_rx: watch::Receiver<Arc<ProxyConfig>>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
|
mut close_rx: mpsc::Receiver<ConntrackCloseEvent>,
|
||||||
|
) {
|
||||||
|
let mut cfg = config_rx.borrow().clone();
|
||||||
|
let mut pressure_state = PressureState::new(stats.as_ref());
|
||||||
|
let mut delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||||
|
let mut backend = pick_backend(cfg.server.conntrack_control.backend);
|
||||||
|
|
||||||
|
apply_runtime_state(
|
||||||
|
stats.as_ref(),
|
||||||
|
shared.as_ref(),
|
||||||
|
&cfg,
|
||||||
|
backend.is_some(),
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
reconcile_rules(&cfg, backend, stats.as_ref()).await;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
tokio::select! {
|
||||||
|
changed = config_rx.changed() => {
|
||||||
|
if changed.is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
cfg = config_rx.borrow_and_update().clone();
|
||||||
|
backend = pick_backend(cfg.server.conntrack_control.backend);
|
||||||
|
delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||||
|
apply_runtime_state(stats.as_ref(), shared.as_ref(), &cfg, backend.is_some(), pressure_state.active);
|
||||||
|
reconcile_rules(&cfg, backend, stats.as_ref()).await;
|
||||||
|
}
|
||||||
|
event = close_rx.recv() => {
|
||||||
|
let Some(event) = event else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
stats.set_conntrack_event_queue_depth(close_rx.len() as u64);
|
||||||
|
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !pressure_state.active {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !matches!(event.reason, ConntrackCloseReason::Timeout | ConntrackCloseReason::Pressure | ConntrackCloseReason::Reset) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if delete_budget_tokens == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
stats.increment_conntrack_delete_attempt_total();
|
||||||
|
match delete_conntrack_entry(event).await {
|
||||||
|
DeleteOutcome::Deleted => {
|
||||||
|
delete_budget_tokens = delete_budget_tokens.saturating_sub(1);
|
||||||
|
stats.increment_conntrack_delete_success_total();
|
||||||
|
}
|
||||||
|
DeleteOutcome::NotFound => {
|
||||||
|
delete_budget_tokens = delete_budget_tokens.saturating_sub(1);
|
||||||
|
stats.increment_conntrack_delete_not_found_total();
|
||||||
|
}
|
||||||
|
DeleteOutcome::Error => {
|
||||||
|
delete_budget_tokens = delete_budget_tokens.saturating_sub(1);
|
||||||
|
stats.increment_conntrack_delete_error_total();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = tokio::time::sleep(PRESSURE_SAMPLE_INTERVAL) => {
|
||||||
|
delete_budget_tokens = cfg.server.conntrack_control.delete_budget_per_sec;
|
||||||
|
stats.set_conntrack_event_queue_depth(close_rx.len() as u64);
|
||||||
|
let sample = collect_pressure_sample(stats.as_ref(), &cfg, &mut pressure_state);
|
||||||
|
update_pressure_state(
|
||||||
|
stats.as_ref(),
|
||||||
|
shared.as_ref(),
|
||||||
|
&cfg,
|
||||||
|
&sample,
|
||||||
|
&mut pressure_state,
|
||||||
|
);
|
||||||
|
if pressure_state.active {
|
||||||
|
note_global_relay_pressure(shared.as_ref());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
shared.disable_conntrack_close_sender();
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_runtime_state(
|
||||||
|
stats: &Stats,
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
backend_available: bool,
|
||||||
|
pressure_active: bool,
|
||||||
|
) {
|
||||||
|
let enabled = cfg.server.conntrack_control.inline_conntrack_control;
|
||||||
|
let available = enabled && backend_available && has_cap_net_admin();
|
||||||
|
if enabled && !available {
|
||||||
|
warn!(
|
||||||
|
"conntrack control enabled but unavailable (missing CAP_NET_ADMIN or backend binaries)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
stats.set_conntrack_control_enabled(enabled);
|
||||||
|
stats.set_conntrack_control_available(available);
|
||||||
|
shared.set_conntrack_pressure_active(enabled && pressure_active);
|
||||||
|
stats.set_conntrack_pressure_active(enabled && pressure_active);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_pressure_sample(
|
||||||
|
stats: &Stats,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
state: &mut PressureState,
|
||||||
|
) -> PressureSample {
|
||||||
|
let current_connections = stats.get_current_connections_total();
|
||||||
|
let conn_pct = if cfg.server.max_connections == 0 {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
((current_connections.saturating_mul(100)) / u64::from(cfg.server.max_connections))
|
||||||
|
.min(100) as u8,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let fd_pct = fd_usage_pct();
|
||||||
|
|
||||||
|
let accept_total = stats.get_accept_permit_timeout_total();
|
||||||
|
let accept_delta = accept_total.saturating_sub(state.prev_accept_timeout_total);
|
||||||
|
state.prev_accept_timeout_total = accept_total;
|
||||||
|
|
||||||
|
let me_total = stats.get_me_c2me_send_full_total();
|
||||||
|
let me_delta = me_total.saturating_sub(state.prev_me_queue_pressure_total);
|
||||||
|
state.prev_me_queue_pressure_total = me_total;
|
||||||
|
|
||||||
|
PressureSample {
|
||||||
|
conn_pct,
|
||||||
|
fd_pct,
|
||||||
|
accept_timeout_delta: accept_delta,
|
||||||
|
me_queue_pressure_delta: me_delta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_pressure_state(
|
||||||
|
stats: &Stats,
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
sample: &PressureSample,
|
||||||
|
state: &mut PressureState,
|
||||||
|
) {
|
||||||
|
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||||
|
if state.active {
|
||||||
|
state.active = false;
|
||||||
|
state.low_streak = 0;
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
info!("Conntrack pressure mode deactivated (feature disabled)");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let high = cfg.server.conntrack_control.pressure_high_watermark_pct;
|
||||||
|
let low = cfg.server.conntrack_control.pressure_low_watermark_pct;
|
||||||
|
|
||||||
|
let high_hit = sample.conn_pct.is_some_and(|v| v >= high)
|
||||||
|
|| sample.fd_pct.is_some_and(|v| v >= high)
|
||||||
|
|| sample.accept_timeout_delta > 0
|
||||||
|
|| sample.me_queue_pressure_delta > 0;
|
||||||
|
|
||||||
|
let low_clear = sample.conn_pct.is_none_or(|v| v <= low)
|
||||||
|
&& sample.fd_pct.is_none_or(|v| v <= low)
|
||||||
|
&& sample.accept_timeout_delta == 0
|
||||||
|
&& sample.me_queue_pressure_delta == 0;
|
||||||
|
|
||||||
|
if !state.active && high_hit {
|
||||||
|
state.active = true;
|
||||||
|
state.low_streak = 0;
|
||||||
|
shared.set_conntrack_pressure_active(true);
|
||||||
|
stats.set_conntrack_pressure_active(true);
|
||||||
|
info!(
|
||||||
|
conn_pct = ?sample.conn_pct,
|
||||||
|
fd_pct = ?sample.fd_pct,
|
||||||
|
accept_timeout_delta = sample.accept_timeout_delta,
|
||||||
|
me_queue_pressure_delta = sample.me_queue_pressure_delta,
|
||||||
|
"Conntrack pressure mode activated"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if state.active && low_clear {
|
||||||
|
state.low_streak = state.low_streak.saturating_add(1);
|
||||||
|
if state.low_streak >= PRESSURE_RELEASE_TICKS {
|
||||||
|
state.active = false;
|
||||||
|
state.low_streak = 0;
|
||||||
|
shared.set_conntrack_pressure_active(false);
|
||||||
|
stats.set_conntrack_pressure_active(false);
|
||||||
|
info!("Conntrack pressure mode deactivated");
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.low_streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn reconcile_rules(cfg: &ProxyConfig, backend: Option<NetfilterBackend>, stats: &Stats) {
|
||||||
|
if !cfg.server.conntrack_control.inline_conntrack_control {
|
||||||
|
clear_notrack_rules_all_backends().await;
|
||||||
|
stats.set_conntrack_rule_apply_ok(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !has_cap_net_admin() {
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(backend) = backend else {
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let apply_result = match backend {
|
||||||
|
NetfilterBackend::Nftables => apply_nft_rules(cfg).await,
|
||||||
|
NetfilterBackend::Iptables => apply_iptables_rules(cfg).await,
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(error) = apply_result {
|
||||||
|
warn!(error = %error, "Failed to reconcile conntrack/notrack rules");
|
||||||
|
stats.set_conntrack_rule_apply_ok(false);
|
||||||
|
} else {
|
||||||
|
stats.set_conntrack_rule_apply_ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick_backend(configured: ConntrackBackend) -> Option<NetfilterBackend> {
|
||||||
|
match configured {
|
||||||
|
ConntrackBackend::Auto => {
|
||||||
|
if command_exists("nft") {
|
||||||
|
Some(NetfilterBackend::Nftables)
|
||||||
|
} else if command_exists("iptables") {
|
||||||
|
Some(NetfilterBackend::Iptables)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConntrackBackend::Nftables => command_exists("nft").then_some(NetfilterBackend::Nftables),
|
||||||
|
ConntrackBackend::Iptables => {
|
||||||
|
command_exists("iptables").then_some(NetfilterBackend::Iptables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn command_exists(binary: &str) -> bool {
|
||||||
|
let Some(path_var) = std::env::var_os("PATH") else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
std::env::split_paths(&path_var).any(|dir| {
|
||||||
|
let candidate: PathBuf = dir.join(binary);
|
||||||
|
candidate.exists() && candidate.is_file()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn notrack_targets(cfg: &ProxyConfig) -> (Vec<Option<IpAddr>>, Vec<Option<IpAddr>>) {
|
||||||
|
let mode = cfg.server.conntrack_control.mode;
|
||||||
|
let mut v4_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
|
||||||
|
let mut v6_targets: BTreeSet<Option<IpAddr>> = BTreeSet::new();
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
ConntrackMode::Tracked => {}
|
||||||
|
ConntrackMode::Notrack => {
|
||||||
|
if cfg.server.listeners.is_empty() {
|
||||||
|
if let Some(ipv4) = cfg
|
||||||
|
.server
|
||||||
|
.listen_addr_ipv4
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse::<IpAddr>().ok())
|
||||||
|
{
|
||||||
|
if ipv4.is_unspecified() {
|
||||||
|
v4_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v4_targets.insert(Some(ipv4));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ipv6) = cfg
|
||||||
|
.server
|
||||||
|
.listen_addr_ipv6
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|s| s.parse::<IpAddr>().ok())
|
||||||
|
{
|
||||||
|
if ipv6.is_unspecified() {
|
||||||
|
v6_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v6_targets.insert(Some(ipv6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for listener in &cfg.server.listeners {
|
||||||
|
if listener.ip.is_ipv4() {
|
||||||
|
if listener.ip.is_unspecified() {
|
||||||
|
v4_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v4_targets.insert(Some(listener.ip));
|
||||||
|
}
|
||||||
|
} else if listener.ip.is_unspecified() {
|
||||||
|
v6_targets.insert(None);
|
||||||
|
} else {
|
||||||
|
v6_targets.insert(Some(listener.ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ConntrackMode::Hybrid => {
|
||||||
|
for ip in &cfg.server.conntrack_control.hybrid_listener_ips {
|
||||||
|
if ip.is_ipv4() {
|
||||||
|
v4_targets.insert(Some(*ip));
|
||||||
|
} else {
|
||||||
|
v6_targets.insert(Some(*ip));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
v4_targets.into_iter().collect(),
|
||||||
|
v6_targets.into_iter().collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_nft_rules(cfg: &ProxyConfig) -> Result<(), String> {
|
||||||
|
let _ = run_command(
|
||||||
|
"nft",
|
||||||
|
&["delete", "table", "inet", "telemt_conntrack"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if matches!(cfg.server.conntrack_control.mode, ConntrackMode::Tracked) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let (v4_targets, v6_targets) = notrack_targets(cfg);
|
||||||
|
let mut rules = Vec::new();
|
||||||
|
for ip in v4_targets {
|
||||||
|
let rule = if let Some(ip) = ip {
|
||||||
|
format!("tcp dport {} ip daddr {} notrack", cfg.server.port, ip)
|
||||||
|
} else {
|
||||||
|
format!("tcp dport {} notrack", cfg.server.port)
|
||||||
|
};
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
for ip in v6_targets {
|
||||||
|
let rule = if let Some(ip) = ip {
|
||||||
|
format!("tcp dport {} ip6 daddr {} notrack", cfg.server.port, ip)
|
||||||
|
} else {
|
||||||
|
format!("tcp dport {} notrack", cfg.server.port)
|
||||||
|
};
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rule_blob = if rules.is_empty() {
|
||||||
|
String::new()
|
||||||
|
} else {
|
||||||
|
format!(" {}\n", rules.join("\n "))
|
||||||
|
};
|
||||||
|
let script = format!(
|
||||||
|
"table inet telemt_conntrack {{\n chain preraw {{\n type filter hook prerouting priority raw; policy accept;\n{rule_blob} }}\n}}\n"
|
||||||
|
);
|
||||||
|
run_command("nft", &["-f", "-"], Some(script)).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_iptables_rules(cfg: &ProxyConfig) -> Result<(), String> {
|
||||||
|
apply_iptables_rules_for_binary("iptables", cfg, true).await?;
|
||||||
|
apply_iptables_rules_for_binary("ip6tables", cfg, false).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn apply_iptables_rules_for_binary(
|
||||||
|
binary: &str,
|
||||||
|
cfg: &ProxyConfig,
|
||||||
|
ipv4: bool,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
if !command_exists(binary) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let chain = "TELEMT_NOTRACK";
|
||||||
|
let _ = run_command(
|
||||||
|
binary,
|
||||||
|
&["-t", "raw", "-D", "PREROUTING", "-j", chain],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command(binary, &["-t", "raw", "-F", chain], None).await;
|
||||||
|
let _ = run_command(binary, &["-t", "raw", "-X", chain], None).await;
|
||||||
|
|
||||||
|
if matches!(cfg.server.conntrack_control.mode, ConntrackMode::Tracked) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
run_command(binary, &["-t", "raw", "-N", chain], None).await?;
|
||||||
|
run_command(binary, &["-t", "raw", "-F", chain], None).await?;
|
||||||
|
if run_command(
|
||||||
|
binary,
|
||||||
|
&["-t", "raw", "-C", "PREROUTING", "-j", chain],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.is_err()
|
||||||
|
{
|
||||||
|
run_command(
|
||||||
|
binary,
|
||||||
|
&["-t", "raw", "-I", "PREROUTING", "1", "-j", chain],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (v4_targets, v6_targets) = notrack_targets(cfg);
|
||||||
|
let selected = if ipv4 { v4_targets } else { v6_targets };
|
||||||
|
for ip in selected {
|
||||||
|
let mut args = vec![
|
||||||
|
"-t".to_string(),
|
||||||
|
"raw".to_string(),
|
||||||
|
"-A".to_string(),
|
||||||
|
chain.to_string(),
|
||||||
|
"-p".to_string(),
|
||||||
|
"tcp".to_string(),
|
||||||
|
"--dport".to_string(),
|
||||||
|
cfg.server.port.to_string(),
|
||||||
|
];
|
||||||
|
if let Some(ip) = ip {
|
||||||
|
args.push("-d".to_string());
|
||||||
|
args.push(ip.to_string());
|
||||||
|
}
|
||||||
|
args.push("-j".to_string());
|
||||||
|
args.push("CT".to_string());
|
||||||
|
args.push("--notrack".to_string());
|
||||||
|
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||||
|
run_command(binary, &arg_refs, None).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn clear_notrack_rules_all_backends() {
|
||||||
|
let _ = run_command(
|
||||||
|
"nft",
|
||||||
|
&["delete", "table", "inet", "telemt_conntrack"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command(
|
||||||
|
"iptables",
|
||||||
|
&["-t", "raw", "-D", "PREROUTING", "-j", "TELEMT_NOTRACK"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command("iptables", &["-t", "raw", "-F", "TELEMT_NOTRACK"], None).await;
|
||||||
|
let _ = run_command("iptables", &["-t", "raw", "-X", "TELEMT_NOTRACK"], None).await;
|
||||||
|
let _ = run_command(
|
||||||
|
"ip6tables",
|
||||||
|
&["-t", "raw", "-D", "PREROUTING", "-j", "TELEMT_NOTRACK"],
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let _ = run_command("ip6tables", &["-t", "raw", "-F", "TELEMT_NOTRACK"], None).await;
|
||||||
|
let _ = run_command("ip6tables", &["-t", "raw", "-X", "TELEMT_NOTRACK"], None).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DeleteOutcome {
|
||||||
|
Deleted,
|
||||||
|
NotFound,
|
||||||
|
Error,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_conntrack_entry(event: ConntrackCloseEvent) -> DeleteOutcome {
|
||||||
|
if !command_exists("conntrack") {
|
||||||
|
return DeleteOutcome::Error;
|
||||||
|
}
|
||||||
|
let args = vec![
|
||||||
|
"-D".to_string(),
|
||||||
|
"-p".to_string(),
|
||||||
|
"tcp".to_string(),
|
||||||
|
"-s".to_string(),
|
||||||
|
event.src.ip().to_string(),
|
||||||
|
"--sport".to_string(),
|
||||||
|
event.src.port().to_string(),
|
||||||
|
"-d".to_string(),
|
||||||
|
event.dst.ip().to_string(),
|
||||||
|
"--dport".to_string(),
|
||||||
|
event.dst.port().to_string(),
|
||||||
|
];
|
||||||
|
let arg_refs: Vec<&str> = args.iter().map(String::as_str).collect();
|
||||||
|
match run_command("conntrack", &arg_refs, None).await {
|
||||||
|
Ok(()) => DeleteOutcome::Deleted,
|
||||||
|
Err(error) => {
|
||||||
|
if error.contains("0 flow entries have been deleted") {
|
||||||
|
DeleteOutcome::NotFound
|
||||||
|
} else {
|
||||||
|
debug!(error = %error, "conntrack delete failed");
|
||||||
|
DeleteOutcome::Error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn run_command(binary: &str, args: &[&str], stdin: Option<String>) -> Result<(), String> {
|
||||||
|
if !command_exists(binary) {
|
||||||
|
return Err(format!("{binary} is not available"));
|
||||||
|
}
|
||||||
|
let mut command = Command::new(binary);
|
||||||
|
command.args(args);
|
||||||
|
if stdin.is_some() {
|
||||||
|
command.stdin(std::process::Stdio::piped());
|
||||||
|
}
|
||||||
|
command.stdout(std::process::Stdio::null());
|
||||||
|
command.stderr(std::process::Stdio::piped());
|
||||||
|
let mut child = command
|
||||||
|
.spawn()
|
||||||
|
.map_err(|e| format!("spawn {binary} failed: {e}"))?;
|
||||||
|
if let Some(blob) = stdin
|
||||||
|
&& let Some(mut writer) = child.stdin.take()
|
||||||
|
{
|
||||||
|
writer
|
||||||
|
.write_all(blob.as_bytes())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("stdin write {binary} failed: {e}"))?;
|
||||||
|
}
|
||||||
|
let output = child
|
||||||
|
.wait_with_output()
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("wait {binary} failed: {e}"))?;
|
||||||
|
if output.status.success() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
Err(if stderr.is_empty() {
|
||||||
|
format!("{binary} exited with status {}", output.status)
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fd_usage_pct() -> Option<u8> {
|
||||||
|
let soft_limit = nofile_soft_limit()?;
|
||||||
|
if soft_limit == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let fd_count = std::fs::read_dir("/proc/self/fd").ok()?.count() as u64;
|
||||||
|
Some(((fd_count.saturating_mul(100)) / soft_limit).min(100) as u8)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nofile_soft_limit() -> Option<u64> {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let mut lim = libc::rlimit {
|
||||||
|
rlim_cur: 0,
|
||||||
|
rlim_max: 0,
|
||||||
|
};
|
||||||
|
let rc = unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) };
|
||||||
|
if rc != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
return Some(lim.rlim_cur);
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_cap_net_admin() -> bool {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
let Ok(status) = std::fs::read_to_string("/proc/self/status") else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
for line in status.lines() {
|
||||||
|
if let Some(raw) = line.strip_prefix("CapEff:") {
|
||||||
|
let caps = raw.trim();
|
||||||
|
if let Ok(bits) = u64::from_str_radix(caps, 16) {
|
||||||
|
const CAP_NET_ADMIN_BIT: u64 = 12;
|
||||||
|
return (bits & (1u64 << CAP_NET_ADMIN_BIT)) != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::config::ProxyConfig;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressure_activates_on_accept_timeout_spike() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.server.conntrack_control.inline_conntrack_control = true;
|
||||||
|
let mut state = PressureState::new(&stats);
|
||||||
|
let sample = PressureSample {
|
||||||
|
conn_pct: Some(10),
|
||||||
|
fd_pct: Some(10),
|
||||||
|
accept_timeout_delta: 1,
|
||||||
|
me_queue_pressure_delta: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
|
||||||
|
|
||||||
|
assert!(state.active);
|
||||||
|
assert!(shared.conntrack_pressure_active());
|
||||||
|
assert!(stats.get_conntrack_pressure_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressure_releases_after_hysteresis_window() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.server.conntrack_control.inline_conntrack_control = true;
|
||||||
|
let mut state = PressureState::new(&stats);
|
||||||
|
|
||||||
|
let high_sample = PressureSample {
|
||||||
|
conn_pct: Some(95),
|
||||||
|
fd_pct: Some(95),
|
||||||
|
accept_timeout_delta: 0,
|
||||||
|
me_queue_pressure_delta: 0,
|
||||||
|
};
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &high_sample, &mut state);
|
||||||
|
assert!(state.active);
|
||||||
|
|
||||||
|
let low_sample = PressureSample {
|
||||||
|
conn_pct: Some(10),
|
||||||
|
fd_pct: Some(10),
|
||||||
|
accept_timeout_delta: 0,
|
||||||
|
me_queue_pressure_delta: 0,
|
||||||
|
};
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||||
|
assert!(state.active);
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||||
|
assert!(state.active);
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &low_sample, &mut state);
|
||||||
|
|
||||||
|
assert!(!state.active);
|
||||||
|
assert!(!shared.conntrack_pressure_active());
|
||||||
|
assert!(!stats.get_conntrack_pressure_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pressure_does_not_activate_when_disabled() {
|
||||||
|
let stats = Stats::new();
|
||||||
|
let shared = ProxySharedState::new();
|
||||||
|
let mut cfg = ProxyConfig::default();
|
||||||
|
cfg.server.conntrack_control.inline_conntrack_control = false;
|
||||||
|
let mut state = PressureState::new(&stats);
|
||||||
|
let sample = PressureSample {
|
||||||
|
conn_pct: Some(100),
|
||||||
|
fd_pct: Some(100),
|
||||||
|
accept_timeout_delta: 10,
|
||||||
|
me_queue_pressure_delta: 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
update_pressure_state(&stats, shared.as_ref(), &cfg, &sample, &mut state);
|
||||||
|
|
||||||
|
assert!(!state.active);
|
||||||
|
assert!(!shared.conntrack_pressure_active());
|
||||||
|
assert!(!stats.get_conntrack_pressure_active());
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
-21
@@ -88,8 +88,10 @@ pub fn init_logging(
|
|||||||
// Use a custom fmt layer that writes to syslog
|
// Use a custom fmt layer that writes to syslog
|
||||||
let fmt_layer = fmt::Layer::default()
|
let fmt_layer = fmt::Layer::default()
|
||||||
.with_ansi(false)
|
.with_ansi(false)
|
||||||
.with_target(true)
|
.with_target(false)
|
||||||
.with_writer(SyslogWriter::new);
|
.with_level(false)
|
||||||
|
.without_time()
|
||||||
|
.with_writer(SyslogMakeWriter::new());
|
||||||
|
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
.with(filter_layer)
|
.with(filter_layer)
|
||||||
@@ -137,12 +139,17 @@ pub fn init_logging(
|
|||||||
|
|
||||||
/// Syslog writer for tracing.
|
/// Syslog writer for tracing.
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct SyslogMakeWriter;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
struct SyslogWriter {
|
struct SyslogWriter {
|
||||||
_private: (),
|
priority: libc::c_int,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
impl SyslogWriter {
|
impl SyslogMakeWriter {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
// Open syslog connection on first use
|
// Open syslog connection on first use
|
||||||
static INIT: std::sync::Once = std::sync::Once::new();
|
static INIT: std::sync::Once = std::sync::Once::new();
|
||||||
@@ -153,7 +160,18 @@ impl SyslogWriter {
|
|||||||
libc::openlog(ident, libc::LOG_PID | libc::LOG_NDELAY, libc::LOG_DAEMON);
|
libc::openlog(ident, libc::LOG_PID | libc::LOG_NDELAY, libc::LOG_DAEMON);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Self { _private: () }
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn syslog_priority_for_level(level: &tracing::Level) -> libc::c_int {
|
||||||
|
match *level {
|
||||||
|
tracing::Level::ERROR => libc::LOG_ERR,
|
||||||
|
tracing::Level::WARN => libc::LOG_WARNING,
|
||||||
|
tracing::Level::INFO => libc::LOG_INFO,
|
||||||
|
tracing::Level::DEBUG => libc::LOG_DEBUG,
|
||||||
|
tracing::Level::TRACE => libc::LOG_DEBUG,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,26 +186,13 @@ impl std::io::Write for SyslogWriter {
|
|||||||
return Ok(buf.len());
|
return Ok(buf.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine priority based on log level in the message
|
|
||||||
let priority = if msg.contains(" ERROR ") || msg.contains(" error ") {
|
|
||||||
libc::LOG_ERR
|
|
||||||
} else if msg.contains(" WARN ") || msg.contains(" warn ") {
|
|
||||||
libc::LOG_WARNING
|
|
||||||
} else if msg.contains(" INFO ") || msg.contains(" info ") {
|
|
||||||
libc::LOG_INFO
|
|
||||||
} else if msg.contains(" DEBUG ") || msg.contains(" debug ") {
|
|
||||||
libc::LOG_DEBUG
|
|
||||||
} else {
|
|
||||||
libc::LOG_INFO
|
|
||||||
};
|
|
||||||
|
|
||||||
// Write to syslog
|
// Write to syslog
|
||||||
let c_msg = std::ffi::CString::new(msg.as_bytes())
|
let c_msg = std::ffi::CString::new(msg.as_bytes())
|
||||||
.unwrap_or_else(|_| std::ffi::CString::new("(invalid utf8)").unwrap());
|
.unwrap_or_else(|_| std::ffi::CString::new("(invalid utf8)").unwrap());
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
libc::syslog(
|
libc::syslog(
|
||||||
priority,
|
self.priority,
|
||||||
b"%s\0".as_ptr() as *const libc::c_char,
|
b"%s\0".as_ptr() as *const libc::c_char,
|
||||||
c_msg.as_ptr(),
|
c_msg.as_ptr(),
|
||||||
);
|
);
|
||||||
@@ -202,11 +207,19 @@ impl std::io::Write for SyslogWriter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SyslogWriter {
|
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SyslogMakeWriter {
|
||||||
type Writer = SyslogWriter;
|
type Writer = SyslogWriter;
|
||||||
|
|
||||||
fn make_writer(&'a self) -> Self::Writer {
|
fn make_writer(&'a self) -> Self::Writer {
|
||||||
SyslogWriter::new()
|
SyslogWriter {
|
||||||
|
priority: libc::LOG_INFO,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_writer_for(&'a self, meta: &tracing::Metadata<'_>) -> Self::Writer {
|
||||||
|
SyslogWriter {
|
||||||
|
priority: syslog_priority_for_level(meta.level()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,4 +315,29 @@ mod tests {
|
|||||||
LogDestination::Syslog
|
LogDestination::Syslog
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[test]
|
||||||
|
fn test_syslog_priority_for_level_mapping() {
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::ERROR),
|
||||||
|
libc::LOG_ERR
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::WARN),
|
||||||
|
libc::LOG_WARNING
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::INFO),
|
||||||
|
libc::LOG_INFO
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::DEBUG),
|
||||||
|
libc::LOG_DEBUG
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
syslog_priority_for_level(&tracing::Level::TRACE),
|
||||||
|
libc::LOG_DEBUG
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+83
-15
@@ -18,19 +18,38 @@ use crate::transport::middle_proxy::{
|
|||||||
pub(crate) fn resolve_runtime_config_path(
|
pub(crate) fn resolve_runtime_config_path(
|
||||||
config_path_cli: &str,
|
config_path_cli: &str,
|
||||||
startup_cwd: &std::path::Path,
|
startup_cwd: &std::path::Path,
|
||||||
|
config_path_explicit: bool,
|
||||||
) -> PathBuf {
|
) -> PathBuf {
|
||||||
let raw = PathBuf::from(config_path_cli);
|
if config_path_explicit {
|
||||||
let absolute = if raw.is_absolute() {
|
let raw = PathBuf::from(config_path_cli);
|
||||||
raw
|
let absolute = if raw.is_absolute() {
|
||||||
} else {
|
raw
|
||||||
startup_cwd.join(raw)
|
} else {
|
||||||
};
|
startup_cwd.join(raw)
|
||||||
absolute.canonicalize().unwrap_or(absolute)
|
};
|
||||||
|
return absolute.canonicalize().unwrap_or(absolute);
|
||||||
|
}
|
||||||
|
|
||||||
|
let etc_telemt = std::path::Path::new("/etc/telemt");
|
||||||
|
let candidates = [
|
||||||
|
startup_cwd.join("config.toml"),
|
||||||
|
startup_cwd.join("telemt.toml"),
|
||||||
|
etc_telemt.join("telemt.toml"),
|
||||||
|
etc_telemt.join("config.toml"),
|
||||||
|
];
|
||||||
|
for candidate in candidates {
|
||||||
|
if candidate.is_file() {
|
||||||
|
return candidate.canonicalize().unwrap_or(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
startup_cwd.join("config.toml")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parsed CLI arguments.
|
/// Parsed CLI arguments.
|
||||||
pub(crate) struct CliArgs {
|
pub(crate) struct CliArgs {
|
||||||
pub config_path: String,
|
pub config_path: String,
|
||||||
|
pub config_path_explicit: bool,
|
||||||
pub data_path: Option<PathBuf>,
|
pub data_path: Option<PathBuf>,
|
||||||
pub silent: bool,
|
pub silent: bool,
|
||||||
pub log_level: Option<String>,
|
pub log_level: Option<String>,
|
||||||
@@ -39,6 +58,7 @@ pub(crate) struct CliArgs {
|
|||||||
|
|
||||||
pub(crate) fn parse_cli() -> CliArgs {
|
pub(crate) fn parse_cli() -> CliArgs {
|
||||||
let mut config_path = "config.toml".to_string();
|
let mut config_path = "config.toml".to_string();
|
||||||
|
let mut config_path_explicit = false;
|
||||||
let mut data_path: Option<PathBuf> = None;
|
let mut data_path: Option<PathBuf> = None;
|
||||||
let mut silent = false;
|
let mut silent = false;
|
||||||
let mut log_level: Option<String> = None;
|
let mut log_level: Option<String> = None;
|
||||||
@@ -74,6 +94,20 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
s.trim_start_matches("--data-path=").to_string(),
|
s.trim_start_matches("--data-path=").to_string(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
"--working-dir" => {
|
||||||
|
i += 1;
|
||||||
|
if i < args.len() {
|
||||||
|
data_path = Some(PathBuf::from(args[i].clone()));
|
||||||
|
} else {
|
||||||
|
eprintln!("Missing value for --working-dir");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s if s.starts_with("--working-dir=") => {
|
||||||
|
data_path = Some(PathBuf::from(
|
||||||
|
s.trim_start_matches("--working-dir=").to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
"--silent" | "-s" => {
|
"--silent" | "-s" => {
|
||||||
silent = true;
|
silent = true;
|
||||||
}
|
}
|
||||||
@@ -111,13 +145,11 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s if s.starts_with("--working-dir") => {
|
|
||||||
if !s.contains('=') {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s if !s.starts_with('-') => {
|
s if !s.starts_with('-') => {
|
||||||
config_path = s.to_string();
|
if !matches!(s, "run" | "start" | "stop" | "reload" | "status") {
|
||||||
|
config_path = s.to_string();
|
||||||
|
config_path_explicit = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
other => {
|
other => {
|
||||||
eprintln!("Unknown option: {}", other);
|
eprintln!("Unknown option: {}", other);
|
||||||
@@ -128,6 +160,7 @@ pub(crate) fn parse_cli() -> CliArgs {
|
|||||||
|
|
||||||
CliArgs {
|
CliArgs {
|
||||||
config_path,
|
config_path,
|
||||||
|
config_path_explicit,
|
||||||
data_path,
|
data_path,
|
||||||
silent,
|
silent,
|
||||||
log_level,
|
log_level,
|
||||||
@@ -152,6 +185,7 @@ fn print_help() {
|
|||||||
eprintln!(
|
eprintln!(
|
||||||
" --data-path <DIR> Set data directory (absolute path; overrides config value)"
|
" --data-path <DIR> Set data directory (absolute path; overrides config value)"
|
||||||
);
|
);
|
||||||
|
eprintln!(" --working-dir <DIR> Alias for --data-path");
|
||||||
eprintln!(" --silent, -s Suppress info logs");
|
eprintln!(" --silent, -s Suppress info logs");
|
||||||
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
eprintln!(" --log-level <LEVEL> debug|verbose|normal|silent");
|
||||||
eprintln!(" --help, -h Show this help");
|
eprintln!(" --help, -h Show this help");
|
||||||
@@ -210,7 +244,7 @@ mod tests {
|
|||||||
let target = startup_cwd.join("config.toml");
|
let target = startup_cwd.join("config.toml");
|
||||||
std::fs::write(&target, " ").unwrap();
|
std::fs::write(&target, " ").unwrap();
|
||||||
|
|
||||||
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd);
|
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd, true);
|
||||||
assert_eq!(resolved, target.canonicalize().unwrap());
|
assert_eq!(resolved, target.canonicalize().unwrap());
|
||||||
|
|
||||||
let _ = std::fs::remove_file(&target);
|
let _ = std::fs::remove_file(&target);
|
||||||
@@ -226,11 +260,45 @@ mod tests {
|
|||||||
let startup_cwd = std::env::temp_dir().join(format!("telemt_cfg_path_missing_{nonce}"));
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_cfg_path_missing_{nonce}"));
|
||||||
std::fs::create_dir_all(&startup_cwd).unwrap();
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
|
||||||
let resolved = resolve_runtime_config_path("missing.toml", &startup_cwd);
|
let resolved = resolve_runtime_config_path("missing.toml", &startup_cwd, true);
|
||||||
assert_eq!(resolved, startup_cwd.join("missing.toml"));
|
assert_eq!(resolved, startup_cwd.join("missing.toml"));
|
||||||
|
|
||||||
let _ = std::fs::remove_dir(&startup_cwd);
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_config_path_uses_startup_candidates_when_not_explicit() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let startup_cwd =
|
||||||
|
std::env::temp_dir().join(format!("telemt_cfg_startup_candidates_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
let telemt = startup_cwd.join("telemt.toml");
|
||||||
|
std::fs::write(&telemt, " ").unwrap();
|
||||||
|
|
||||||
|
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd, false);
|
||||||
|
assert_eq!(resolved, telemt.canonicalize().unwrap());
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(&telemt);
|
||||||
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_runtime_config_path_defaults_to_startup_config_when_none_found() {
|
||||||
|
let nonce = std::time::SystemTime::now()
|
||||||
|
.duration_since(std::time::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos();
|
||||||
|
let startup_cwd = std::env::temp_dir().join(format!("telemt_cfg_startup_default_{nonce}"));
|
||||||
|
std::fs::create_dir_all(&startup_cwd).unwrap();
|
||||||
|
|
||||||
|
let resolved = resolve_runtime_config_path("config.toml", &startup_cwd, false);
|
||||||
|
assert_eq!(resolved, startup_cwd.join("config.toml"));
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir(&startup_cwd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
pub(crate) fn print_proxy_links(host: &str, port: u16, config: &ProxyConfig) {
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ pub(crate) async fn bind_listeners(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
stats.increment_accept_permit_timeout_total();
|
||||||
debug!(
|
debug!(
|
||||||
timeout_ms = accept_permit_timeout_ms,
|
timeout_ms = accept_permit_timeout_ms,
|
||||||
"Dropping accepted unix connection: permit wait timeout"
|
"Dropping accepted unix connection: permit wait timeout"
|
||||||
@@ -407,6 +408,7 @@ pub(crate) fn spawn_tcp_accept_loops(
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
|
stats.increment_accept_permit_timeout_total();
|
||||||
debug!(
|
debug!(
|
||||||
peer = %peer_addr,
|
peer = %peer_addr,
|
||||||
timeout_ms = accept_permit_timeout_ms,
|
timeout_ms = accept_permit_timeout_ms,
|
||||||
|
|||||||
+102
-6
@@ -29,6 +29,7 @@ use tracing_subscriber::{EnvFilter, fmt, prelude::*, reload};
|
|||||||
|
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use crate::config::{LogLevel, ProxyConfig};
|
use crate::config::{LogLevel, ProxyConfig};
|
||||||
|
use crate::conntrack_control;
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::ip_tracker::UserIpTracker;
|
use crate::ip_tracker::UserIpTracker;
|
||||||
use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe};
|
use crate::network::probe::{decide_network_capabilities, log_probe_result, run_probe};
|
||||||
@@ -111,6 +112,7 @@ async fn run_inner(
|
|||||||
.await;
|
.await;
|
||||||
let cli_args = parse_cli();
|
let cli_args = parse_cli();
|
||||||
let config_path_cli = cli_args.config_path;
|
let config_path_cli = cli_args.config_path;
|
||||||
|
let config_path_explicit = cli_args.config_path_explicit;
|
||||||
let data_path = cli_args.data_path;
|
let data_path = cli_args.data_path;
|
||||||
let cli_silent = cli_args.silent;
|
let cli_silent = cli_args.silent;
|
||||||
let cli_log_level = cli_args.log_level;
|
let cli_log_level = cli_args.log_level;
|
||||||
@@ -122,7 +124,8 @@ async fn run_inner(
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let config_path = resolve_runtime_config_path(&config_path_cli, &startup_cwd);
|
let mut config_path =
|
||||||
|
resolve_runtime_config_path(&config_path_cli, &startup_cwd, config_path_explicit);
|
||||||
|
|
||||||
let mut config = match ProxyConfig::load(&config_path) {
|
let mut config = match ProxyConfig::load(&config_path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
@@ -132,11 +135,99 @@ async fn run_inner(
|
|||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
} else {
|
} else {
|
||||||
let default = ProxyConfig::default();
|
let default = ProxyConfig::default();
|
||||||
std::fs::write(&config_path, toml::to_string_pretty(&default).unwrap()).unwrap();
|
|
||||||
eprintln!(
|
let serialized =
|
||||||
"[telemt] Created default config at {}",
|
match toml::to_string_pretty(&default).or_else(|_| toml::to_string(&default)) {
|
||||||
config_path.display()
|
Ok(value) => Some(value),
|
||||||
);
|
Err(serialize_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to serialize default config: {}",
|
||||||
|
serialize_error
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if config_path_explicit {
|
||||||
|
if let Some(serialized) = serialized.as_ref() {
|
||||||
|
if let Err(write_error) = std::fs::write(&config_path, serialized) {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Error: failed to create explicit config at {}: {}",
|
||||||
|
config_path.display(),
|
||||||
|
write_error
|
||||||
|
);
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Created default config at {}",
|
||||||
|
config_path.display()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: running with in-memory default config without writing to disk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let system_dir = std::path::Path::new("/etc/telemt");
|
||||||
|
let system_config_path = system_dir.join("telemt.toml");
|
||||||
|
let startup_config_path = startup_cwd.join("config.toml");
|
||||||
|
let mut persisted = false;
|
||||||
|
|
||||||
|
if let Some(serialized) = serialized.as_ref() {
|
||||||
|
match std::fs::create_dir_all(system_dir) {
|
||||||
|
Ok(()) => match std::fs::write(&system_config_path, serialized) {
|
||||||
|
Ok(()) => {
|
||||||
|
config_path = system_config_path;
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Created default config at {}",
|
||||||
|
config_path.display()
|
||||||
|
);
|
||||||
|
persisted = true;
|
||||||
|
}
|
||||||
|
Err(write_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to write default config at {}: {}",
|
||||||
|
system_config_path.display(),
|
||||||
|
write_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(create_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to create {}: {}",
|
||||||
|
system_dir.display(),
|
||||||
|
create_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !persisted {
|
||||||
|
match std::fs::write(&startup_config_path, serialized) {
|
||||||
|
Ok(()) => {
|
||||||
|
config_path = startup_config_path;
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Created default config at {}",
|
||||||
|
config_path.display()
|
||||||
|
);
|
||||||
|
persisted = true;
|
||||||
|
}
|
||||||
|
Err(write_error) => {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: failed to write default config at {}: {}",
|
||||||
|
startup_config_path.display(),
|
||||||
|
write_error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !persisted {
|
||||||
|
eprintln!(
|
||||||
|
"[telemt] Warning: running with in-memory default config without writing to disk"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
default
|
default
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -633,6 +724,11 @@ async fn run_inner(
|
|||||||
.await;
|
.await;
|
||||||
let _admission_tx_hold = admission_tx;
|
let _admission_tx_hold = admission_tx;
|
||||||
let shared_state = ProxySharedState::new();
|
let shared_state = ProxySharedState::new();
|
||||||
|
conntrack_control::spawn_conntrack_controller(
|
||||||
|
config_rx.clone(),
|
||||||
|
stats.clone(),
|
||||||
|
shared_state.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let bound = listeners::bind_listeners(
|
let bound = listeners::bind_listeners(
|
||||||
&config,
|
&config,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
mod api;
|
mod api;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod conntrack_control;
|
||||||
mod crypto;
|
mod crypto;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod daemon;
|
mod daemon;
|
||||||
|
|||||||
+128
@@ -359,6 +359,134 @@ async fn render_metrics(stats: &Stats, config: &ProxyConfig, ip_tracker: &UserIp
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_accept_permit_timeout_total Accepted connections dropped due to permit wait timeout"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_accept_permit_timeout_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_accept_permit_timeout_total {}",
|
||||||
|
if core_enabled {
|
||||||
|
stats.get_accept_permit_timeout_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_conntrack_control_state Runtime conntrack control state flags"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_conntrack_control_state gauge");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_control_state{{flag=\"enabled\"}} {}",
|
||||||
|
if stats.get_conntrack_control_enabled() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_control_state{{flag=\"available\"}} {}",
|
||||||
|
if stats.get_conntrack_control_available() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_control_state{{flag=\"pressure_active\"}} {}",
|
||||||
|
if stats.get_conntrack_pressure_active() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_control_state{{flag=\"rule_apply_ok\"}} {}",
|
||||||
|
if stats.get_conntrack_rule_apply_ok() {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_conntrack_event_queue_depth Pending close events in conntrack control queue"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_conntrack_event_queue_depth gauge");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_event_queue_depth {}",
|
||||||
|
stats.get_conntrack_event_queue_depth()
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_conntrack_delete_total Conntrack delete attempts by outcome"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "# TYPE telemt_conntrack_delete_total counter");
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_delete_total{{result=\"attempt\"}} {}",
|
||||||
|
if core_enabled {
|
||||||
|
stats.get_conntrack_delete_attempt_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_delete_total{{result=\"success\"}} {}",
|
||||||
|
if core_enabled {
|
||||||
|
stats.get_conntrack_delete_success_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_delete_total{{result=\"not_found\"}} {}",
|
||||||
|
if core_enabled {
|
||||||
|
stats.get_conntrack_delete_not_found_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_delete_total{{result=\"error\"}} {}",
|
||||||
|
if core_enabled {
|
||||||
|
stats.get_conntrack_delete_error_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# HELP telemt_conntrack_close_event_drop_total Dropped conntrack close events due to queue pressure or unavailable sender"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"# TYPE telemt_conntrack_close_event_drop_total counter"
|
||||||
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"telemt_conntrack_close_event_drop_total {}",
|
||||||
|
if core_enabled {
|
||||||
|
stats.get_conntrack_close_event_drop_total()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
out,
|
out,
|
||||||
"# HELP telemt_upstream_connect_attempt_total Upstream connect attempts across all requests"
|
"# HELP telemt_upstream_connect_attempt_total Upstream connect attempts across all requests"
|
||||||
|
|||||||
@@ -246,7 +246,9 @@ pub fn seed_tier_for_user(user: &str) -> AdaptiveTier {
|
|||||||
if now.saturating_duration_since(value.seen_at) <= PROFILE_TTL {
|
if now.saturating_duration_since(value.seen_at) <= PROFILE_TTL {
|
||||||
return value.tier;
|
return value.tier;
|
||||||
}
|
}
|
||||||
profiles().remove_if(user, |_, v| now.saturating_duration_since(v.seen_at) > PROFILE_TTL);
|
profiles().remove_if(user, |_, v| {
|
||||||
|
now.saturating_duration_since(v.seen_at) > PROFILE_TTL
|
||||||
|
});
|
||||||
}
|
}
|
||||||
AdaptiveTier::Base
|
AdaptiveTier::Base
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-12
@@ -80,7 +80,7 @@ use crate::transport::middle_proxy::MePool;
|
|||||||
use crate::transport::socket::normalize_ip;
|
use crate::transport::socket::normalize_ip;
|
||||||
use crate::transport::{UpstreamManager, configure_client_socket, parse_proxy_protocol};
|
use crate::transport::{UpstreamManager, configure_client_socket, parse_proxy_protocol};
|
||||||
|
|
||||||
use crate::proxy::direct_relay::handle_via_direct;
|
use crate::proxy::direct_relay::handle_via_direct_with_shared;
|
||||||
use crate::proxy::handshake::{
|
use crate::proxy::handshake::{
|
||||||
HandshakeSuccess, handle_mtproto_handshake_with_shared, handle_tls_handshake_with_shared,
|
HandshakeSuccess, handle_mtproto_handshake_with_shared, handle_tls_handshake_with_shared,
|
||||||
};
|
};
|
||||||
@@ -191,6 +191,24 @@ fn handshake_timeout_with_mask_grace(config: &ProxyConfig) -> Duration {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn effective_client_first_byte_idle_secs(config: &ProxyConfig, shared: &ProxySharedState) -> u64 {
|
||||||
|
let idle_secs = config.timeouts.client_first_byte_idle_secs;
|
||||||
|
if idle_secs == 0 {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if shared.conntrack_pressure_active() {
|
||||||
|
idle_secs.min(
|
||||||
|
config
|
||||||
|
.server
|
||||||
|
.conntrack_control
|
||||||
|
.profile
|
||||||
|
.client_first_byte_idle_cap_secs(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
idle_secs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const MASK_CLASSIFIER_PREFETCH_WINDOW: usize = 16;
|
const MASK_CLASSIFIER_PREFETCH_WINDOW: usize = 16;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
const MASK_CLASSIFIER_PREFETCH_TIMEOUT: Duration = Duration::from_millis(5);
|
const MASK_CLASSIFIER_PREFETCH_TIMEOUT: Duration = Duration::from_millis(5);
|
||||||
@@ -463,10 +481,11 @@ where
|
|||||||
|
|
||||||
debug!(peer = %real_peer, "New connection (generic stream)");
|
debug!(peer = %real_peer, "New connection (generic stream)");
|
||||||
|
|
||||||
let first_byte = if config.timeouts.client_first_byte_idle_secs == 0 {
|
let first_byte_idle_secs = effective_client_first_byte_idle_secs(&config, shared.as_ref());
|
||||||
|
let first_byte = if first_byte_idle_secs == 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let idle_timeout = Duration::from_secs(config.timeouts.client_first_byte_idle_secs);
|
let idle_timeout = Duration::from_secs(first_byte_idle_secs);
|
||||||
let mut first_byte = [0u8; 1];
|
let mut first_byte = [0u8; 1];
|
||||||
match timeout(idle_timeout, stream.read(&mut first_byte)).await {
|
match timeout(idle_timeout, stream.read(&mut first_byte)).await {
|
||||||
Ok(Ok(0)) => {
|
Ok(Ok(0)) => {
|
||||||
@@ -502,7 +521,7 @@ where
|
|||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!(
|
debug!(
|
||||||
peer = %real_peer,
|
peer = %real_peer,
|
||||||
idle_secs = config.timeouts.client_first_byte_idle_secs,
|
idle_secs = first_byte_idle_secs,
|
||||||
"Closing idle pooled connection before first client byte"
|
"Closing idle pooled connection before first client byte"
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -968,11 +987,12 @@ impl RunningClientHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let first_byte = if self.config.timeouts.client_first_byte_idle_secs == 0 {
|
let first_byte_idle_secs =
|
||||||
|
effective_client_first_byte_idle_secs(&self.config, self.shared.as_ref());
|
||||||
|
let first_byte = if first_byte_idle_secs == 0 {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
let idle_timeout =
|
let idle_timeout = Duration::from_secs(first_byte_idle_secs);
|
||||||
Duration::from_secs(self.config.timeouts.client_first_byte_idle_secs);
|
|
||||||
let mut first_byte = [0u8; 1];
|
let mut first_byte = [0u8; 1];
|
||||||
match timeout(idle_timeout, self.stream.read(&mut first_byte)).await {
|
match timeout(idle_timeout, self.stream.read(&mut first_byte)).await {
|
||||||
Ok(Ok(0)) => {
|
Ok(Ok(0)) => {
|
||||||
@@ -1008,7 +1028,7 @@ impl RunningClientHandler {
|
|||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!(
|
debug!(
|
||||||
peer = %self.peer,
|
peer = %self.peer,
|
||||||
idle_secs = self.config.timeouts.client_first_byte_idle_secs,
|
idle_secs = first_byte_idle_secs,
|
||||||
"Closing idle pooled connection before first client byte"
|
"Closing idle pooled connection before first client byte"
|
||||||
);
|
);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -1395,7 +1415,7 @@ impl RunningClientHandler {
|
|||||||
local_addr: SocketAddr,
|
local_addr: SocketAddr,
|
||||||
peer_addr: SocketAddr,
|
peer_addr: SocketAddr,
|
||||||
ip_tracker: Arc<UserIpTracker>,
|
ip_tracker: Arc<UserIpTracker>,
|
||||||
_shared: Arc<ProxySharedState>,
|
shared: Arc<ProxySharedState>,
|
||||||
) -> Result<()>
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin + Send + 'static,
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
@@ -1438,12 +1458,12 @@ impl RunningClientHandler {
|
|||||||
route_runtime.subscribe(),
|
route_runtime.subscribe(),
|
||||||
route_snapshot,
|
route_snapshot,
|
||||||
session_id,
|
session_id,
|
||||||
_shared,
|
shared.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
warn!("use_middle_proxy=true but MePool not initialized, falling back to direct");
|
warn!("use_middle_proxy=true but MePool not initialized, falling back to direct");
|
||||||
handle_via_direct(
|
handle_via_direct_with_shared(
|
||||||
client_reader,
|
client_reader,
|
||||||
client_writer,
|
client_writer,
|
||||||
success,
|
success,
|
||||||
@@ -1455,12 +1475,14 @@ impl RunningClientHandler {
|
|||||||
route_runtime.subscribe(),
|
route_runtime.subscribe(),
|
||||||
route_snapshot,
|
route_snapshot,
|
||||||
session_id,
|
session_id,
|
||||||
|
local_addr,
|
||||||
|
shared.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Direct mode (original behavior)
|
// Direct mode (original behavior)
|
||||||
handle_via_direct(
|
handle_via_direct_with_shared(
|
||||||
client_reader,
|
client_reader,
|
||||||
client_writer,
|
client_writer,
|
||||||
success,
|
success,
|
||||||
@@ -1472,6 +1494,8 @@ impl RunningClientHandler {
|
|||||||
route_runtime.subscribe(),
|
route_runtime.subscribe(),
|
||||||
route_snapshot,
|
route_snapshot,
|
||||||
session_id,
|
session_id,
|
||||||
|
local_addr,
|
||||||
|
shared.clone(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use std::net::SocketAddr;
|
|||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
@@ -16,11 +17,13 @@ use crate::crypto::SecureRandom;
|
|||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
use crate::protocol::constants::*;
|
use crate::protocol::constants::*;
|
||||||
use crate::proxy::handshake::{HandshakeSuccess, encrypt_tg_nonce_with_ciphers, generate_tg_nonce};
|
use crate::proxy::handshake::{HandshakeSuccess, encrypt_tg_nonce_with_ciphers, generate_tg_nonce};
|
||||||
use crate::proxy::relay::relay_bidirectional;
|
|
||||||
use crate::proxy::route_mode::{
|
use crate::proxy::route_mode::{
|
||||||
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
||||||
cutover_stagger_delay,
|
cutover_stagger_delay,
|
||||||
};
|
};
|
||||||
|
use crate::proxy::shared_state::{
|
||||||
|
ConntrackCloseEvent, ConntrackClosePublishResult, ConntrackCloseReason, ProxySharedState,
|
||||||
|
};
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
||||||
use crate::transport::UpstreamManager;
|
use crate::transport::UpstreamManager;
|
||||||
@@ -225,7 +228,43 @@ fn unknown_dc_test_lock() -> &'static Mutex<()> {
|
|||||||
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
TEST_LOCK.get_or_init(|| Mutex::new(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub(crate) async fn handle_via_direct<R, W>(
|
pub(crate) async fn handle_via_direct<R, W>(
|
||||||
|
client_reader: CryptoReader<R>,
|
||||||
|
client_writer: CryptoWriter<W>,
|
||||||
|
success: HandshakeSuccess,
|
||||||
|
upstream_manager: Arc<UpstreamManager>,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
config: Arc<ProxyConfig>,
|
||||||
|
buffer_pool: Arc<BufferPool>,
|
||||||
|
rng: Arc<SecureRandom>,
|
||||||
|
route_rx: watch::Receiver<RouteCutoverState>,
|
||||||
|
route_snapshot: RouteCutoverState,
|
||||||
|
session_id: u64,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
|
W: AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
handle_via_direct_with_shared(
|
||||||
|
client_reader,
|
||||||
|
client_writer,
|
||||||
|
success,
|
||||||
|
upstream_manager,
|
||||||
|
stats,
|
||||||
|
config.clone(),
|
||||||
|
buffer_pool,
|
||||||
|
rng,
|
||||||
|
route_rx,
|
||||||
|
route_snapshot,
|
||||||
|
session_id,
|
||||||
|
SocketAddr::from(([0, 0, 0, 0], config.server.port)),
|
||||||
|
ProxySharedState::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn handle_via_direct_with_shared<R, W>(
|
||||||
client_reader: CryptoReader<R>,
|
client_reader: CryptoReader<R>,
|
||||||
client_writer: CryptoWriter<W>,
|
client_writer: CryptoWriter<W>,
|
||||||
success: HandshakeSuccess,
|
success: HandshakeSuccess,
|
||||||
@@ -237,6 +276,8 @@ pub(crate) async fn handle_via_direct<R, W>(
|
|||||||
mut route_rx: watch::Receiver<RouteCutoverState>,
|
mut route_rx: watch::Receiver<RouteCutoverState>,
|
||||||
route_snapshot: RouteCutoverState,
|
route_snapshot: RouteCutoverState,
|
||||||
session_id: u64,
|
session_id: u64,
|
||||||
|
local_addr: SocketAddr,
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
) -> Result<()>
|
) -> Result<()>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin + Send + 'static,
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
@@ -277,7 +318,18 @@ where
|
|||||||
let _direct_connection_lease = stats.acquire_direct_connection_lease();
|
let _direct_connection_lease = stats.acquire_direct_connection_lease();
|
||||||
|
|
||||||
let buffer_pool_trim = Arc::clone(&buffer_pool);
|
let buffer_pool_trim = Arc::clone(&buffer_pool);
|
||||||
let relay_result = relay_bidirectional(
|
let relay_activity_timeout = if shared.conntrack_pressure_active() {
|
||||||
|
Duration::from_secs(
|
||||||
|
config
|
||||||
|
.server
|
||||||
|
.conntrack_control
|
||||||
|
.profile
|
||||||
|
.direct_activity_timeout_secs(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Duration::from_secs(1800)
|
||||||
|
};
|
||||||
|
let relay_result = crate::proxy::relay::relay_bidirectional_with_activity_timeout(
|
||||||
client_reader,
|
client_reader,
|
||||||
client_writer,
|
client_writer,
|
||||||
tg_reader,
|
tg_reader,
|
||||||
@@ -288,6 +340,7 @@ where
|
|||||||
Arc::clone(&stats),
|
Arc::clone(&stats),
|
||||||
config.access.user_data_quota.get(user).copied(),
|
config.access.user_data_quota.get(user).copied(),
|
||||||
buffer_pool,
|
buffer_pool,
|
||||||
|
relay_activity_timeout,
|
||||||
);
|
);
|
||||||
tokio::pin!(relay_result);
|
tokio::pin!(relay_result);
|
||||||
let relay_result = loop {
|
let relay_result = loop {
|
||||||
@@ -329,9 +382,52 @@ where
|
|||||||
pool_snapshot.allocated,
|
pool_snapshot.allocated,
|
||||||
pool_snapshot.allocated.saturating_sub(pool_snapshot.pooled),
|
pool_snapshot.allocated.saturating_sub(pool_snapshot.pooled),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let close_reason = classify_conntrack_close_reason(&relay_result);
|
||||||
|
let publish_result = shared.publish_conntrack_close_event(ConntrackCloseEvent {
|
||||||
|
src: success.peer,
|
||||||
|
dst: local_addr,
|
||||||
|
reason: close_reason,
|
||||||
|
});
|
||||||
|
if !matches!(
|
||||||
|
publish_result,
|
||||||
|
ConntrackClosePublishResult::Sent | ConntrackClosePublishResult::Disabled
|
||||||
|
) {
|
||||||
|
stats.increment_conntrack_close_event_drop_total();
|
||||||
|
}
|
||||||
|
|
||||||
relay_result
|
relay_result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn classify_conntrack_close_reason(result: &Result<()>) -> ConntrackCloseReason {
|
||||||
|
match result {
|
||||||
|
Ok(()) => ConntrackCloseReason::NormalEof,
|
||||||
|
Err(crate::error::ProxyError::Io(error))
|
||||||
|
if matches!(error.kind(), std::io::ErrorKind::TimedOut) =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Timeout
|
||||||
|
}
|
||||||
|
Err(crate::error::ProxyError::Io(error))
|
||||||
|
if matches!(
|
||||||
|
error.kind(),
|
||||||
|
std::io::ErrorKind::ConnectionReset
|
||||||
|
| std::io::ErrorKind::ConnectionAborted
|
||||||
|
| std::io::ErrorKind::BrokenPipe
|
||||||
|
| std::io::ErrorKind::NotConnected
|
||||||
|
| std::io::ErrorKind::UnexpectedEof
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Reset
|
||||||
|
}
|
||||||
|
Err(crate::error::ProxyError::Proxy(message))
|
||||||
|
if message.contains("pressure") || message.contains("evicted") =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Pressure
|
||||||
|
}
|
||||||
|
Err(_) => ConntrackCloseReason::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
fn get_dc_addr_static(dc_idx: i16, config: &ProxyConfig) -> Result<SocketAddr> {
|
||||||
let prefer_v6 = config.network.prefer == 6 && config.network.ipv6.unwrap_or(true);
|
let prefer_v6 = config.network.prefer == 6 && config.network.ipv6.unwrap_or(true);
|
||||||
let datacenters = if prefer_v6 {
|
let datacenters = if prefer_v6 {
|
||||||
|
|||||||
@@ -118,7 +118,11 @@ fn auth_probe_state_expired(state: &AuthProbeState, now: Instant) -> bool {
|
|||||||
now.duration_since(state.last_seen) > retention
|
now.duration_since(state.last_seen) > retention
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auth_probe_eviction_offset_in(shared: &ProxySharedState, peer_ip: IpAddr, now: Instant) -> usize {
|
fn auth_probe_eviction_offset_in(
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
peer_ip: IpAddr,
|
||||||
|
now: Instant,
|
||||||
|
) -> usize {
|
||||||
let hasher_state = &shared.handshake.auth_probe_eviction_hasher;
|
let hasher_state = &shared.handshake.auth_probe_eviction_hasher;
|
||||||
let mut hasher = hasher_state.build_hasher();
|
let mut hasher = hasher_state.build_hasher();
|
||||||
peer_ip.hash(&mut hasher);
|
peer_ip.hash(&mut hasher);
|
||||||
|
|||||||
@@ -255,7 +255,11 @@ async fn wait_mask_connect_budget(started: Instant) {
|
|||||||
// sigma is chosen so ~99% of raw samples land inside [floor, ceiling] before clamp.
|
// sigma is chosen so ~99% of raw samples land inside [floor, ceiling] before clamp.
|
||||||
// When floor > ceiling (misconfiguration), returns ceiling (the smaller value).
|
// When floor > ceiling (misconfiguration), returns ceiling (the smaller value).
|
||||||
// When floor == ceiling, returns that value. When both are 0, returns 0.
|
// When floor == ceiling, returns that value. When both are 0, returns 0.
|
||||||
pub(crate) fn sample_lognormal_percentile_bounded(floor: u64, ceiling: u64, rng: &mut impl Rng) -> u64 {
|
pub(crate) fn sample_lognormal_percentile_bounded(
|
||||||
|
floor: u64,
|
||||||
|
ceiling: u64,
|
||||||
|
rng: &mut impl Rng,
|
||||||
|
) -> u64 {
|
||||||
if ceiling == 0 && floor == 0 {
|
if ceiling == 0 && floor == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -296,7 +300,9 @@ fn mask_outcome_target_budget(config: &ProxyConfig) -> Duration {
|
|||||||
}
|
}
|
||||||
if ceiling > floor {
|
if ceiling > floor {
|
||||||
let mut rng = rand::rng();
|
let mut rng = rand::rng();
|
||||||
return Duration::from_millis(sample_lognormal_percentile_bounded(floor, ceiling, &mut rng));
|
return Duration::from_millis(sample_lognormal_percentile_bounded(
|
||||||
|
floor, ceiling, &mut rng,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// ceiling <= floor: use the larger value (fail-closed: preserve longer delay)
|
// ceiling <= floor: use the larger value (fail-closed: preserve longer delay)
|
||||||
return Duration::from_millis(floor.max(ceiling));
|
return Duration::from_millis(floor.max(ceiling));
|
||||||
|
|||||||
+98
-18
@@ -3,12 +3,12 @@ use std::collections::hash_map::DefaultHasher;
|
|||||||
use std::collections::{BTreeSet, HashMap};
|
use std::collections::{BTreeSet, HashMap};
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
use std::hash::{BuildHasher, Hash};
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use std::hash::Hasher;
|
use std::hash::Hasher;
|
||||||
|
use std::hash::{BuildHasher, Hash};
|
||||||
use std::net::{IpAddr, SocketAddr};
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||||
@@ -16,16 +16,18 @@ use tokio::sync::{mpsc, oneshot, watch};
|
|||||||
use tokio::time::timeout;
|
use tokio::time::timeout;
|
||||||
use tracing::{debug, info, trace, warn};
|
use tracing::{debug, info, trace, warn};
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::{ConntrackPressureProfile, ProxyConfig};
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
use crate::protocol::constants::{secure_padding_len, *};
|
use crate::protocol::constants::{secure_padding_len, *};
|
||||||
use crate::proxy::handshake::HandshakeSuccess;
|
use crate::proxy::handshake::HandshakeSuccess;
|
||||||
use crate::proxy::shared_state::ProxySharedState;
|
|
||||||
use crate::proxy::route_mode::{
|
use crate::proxy::route_mode::{
|
||||||
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
ROUTE_SWITCH_ERROR_MSG, RelayRouteMode, RouteCutoverState, affected_cutover_state,
|
||||||
cutover_stagger_delay,
|
cutover_stagger_delay,
|
||||||
};
|
};
|
||||||
|
use crate::proxy::shared_state::{
|
||||||
|
ConntrackCloseEvent, ConntrackClosePublishResult, ConntrackCloseReason, ProxySharedState,
|
||||||
|
};
|
||||||
use crate::stats::{
|
use crate::stats::{
|
||||||
MeD2cFlushReason, MeD2cQuotaRejectStage, MeD2cWriteMode, QuotaReserveError, Stats, UserStats,
|
MeD2cFlushReason, MeD2cQuotaRejectStage, MeD2cWriteMode, QuotaReserveError, Stats, UserStats,
|
||||||
};
|
};
|
||||||
@@ -135,6 +137,10 @@ fn note_relay_pressure_event_in(shared: &ProxySharedState) {
|
|||||||
guard.pressure_event_seq = guard.pressure_event_seq.wrapping_add(1);
|
guard.pressure_event_seq = guard.pressure_event_seq.wrapping_add(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn note_global_relay_pressure(shared: &ProxySharedState) {
|
||||||
|
note_relay_pressure_event_in(shared);
|
||||||
|
}
|
||||||
|
|
||||||
fn relay_pressure_event_seq_in(shared: &ProxySharedState) -> u64 {
|
fn relay_pressure_event_seq_in(shared: &ProxySharedState) -> u64 {
|
||||||
let guard = relay_idle_candidate_registry_lock_in(shared);
|
let guard = relay_idle_candidate_registry_lock_in(shared);
|
||||||
guard.pressure_event_seq
|
guard.pressure_event_seq
|
||||||
@@ -241,6 +247,21 @@ impl RelayClientIdlePolicy {
|
|||||||
legacy_frame_read_timeout: frame_read_timeout,
|
legacy_frame_read_timeout: frame_read_timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_pressure_caps(&mut self, profile: ConntrackPressureProfile) {
|
||||||
|
let pressure_soft_idle_cap = Duration::from_secs(profile.middle_soft_idle_cap_secs());
|
||||||
|
let pressure_hard_idle_cap = Duration::from_secs(profile.middle_hard_idle_cap_secs());
|
||||||
|
|
||||||
|
self.soft_idle = self.soft_idle.min(pressure_soft_idle_cap);
|
||||||
|
self.hard_idle = self.hard_idle.min(pressure_hard_idle_cap);
|
||||||
|
if self.soft_idle > self.hard_idle {
|
||||||
|
self.soft_idle = self.hard_idle;
|
||||||
|
}
|
||||||
|
self.legacy_frame_read_timeout = self.legacy_frame_read_timeout.min(pressure_hard_idle_cap);
|
||||||
|
if self.grace_after_downstream_activity > self.hard_idle {
|
||||||
|
self.grace_after_downstream_activity = self.hard_idle;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
@@ -438,12 +459,15 @@ fn report_desync_frame_too_large_in(
|
|||||||
.map(|b| matches!(b[0], b'G' | b'P' | b'H' | b'C' | b'D'))
|
.map(|b| matches!(b[0], b'G' | b'P' | b'H' | b'C' | b'D'))
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let dedup_key = hash_value_in(shared, &(
|
let dedup_key = hash_value_in(
|
||||||
state.user.as_str(),
|
shared,
|
||||||
state.peer_hash,
|
&(
|
||||||
proto_tag,
|
state.user.as_str(),
|
||||||
DESYNC_ERROR_CLASS,
|
state.peer_hash,
|
||||||
));
|
proto_tag,
|
||||||
|
DESYNC_ERROR_CLASS,
|
||||||
|
),
|
||||||
|
);
|
||||||
let emit_full = should_emit_full_desync_in(shared, dedup_key, state.desync_all_full, now);
|
let emit_full = should_emit_full_desync_in(shared, dedup_key, state.desync_all_full, now);
|
||||||
let duration_ms = state.started_at.elapsed().as_millis() as u64;
|
let duration_ms = state.started_at.elapsed().as_millis() as u64;
|
||||||
let bytes_me2c = state.bytes_me2c.load(Ordering::Relaxed);
|
let bytes_me2c = state.bytes_me2c.load(Ordering::Relaxed);
|
||||||
@@ -608,7 +632,10 @@ fn observe_me_d2c_flush_event(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn mark_relay_idle_candidate_for_testing(shared: &ProxySharedState, conn_id: u64) -> bool {
|
pub(crate) fn mark_relay_idle_candidate_for_testing(
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
conn_id: u64,
|
||||||
|
) -> bool {
|
||||||
let registry = &shared.middle_relay.relay_idle_registry;
|
let registry = &shared.middle_relay.relay_idle_registry;
|
||||||
let mut guard = match registry.lock() {
|
let mut guard = match registry.lock() {
|
||||||
Ok(guard) => guard,
|
Ok(guard) => guard,
|
||||||
@@ -693,7 +720,10 @@ pub(crate) fn relay_pressure_event_seq_for_testing(shared: &ProxySharedState) ->
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn relay_idle_mark_seq_for_testing(shared: &ProxySharedState) -> u64 {
|
pub(crate) fn relay_idle_mark_seq_for_testing(shared: &ProxySharedState) -> u64 {
|
||||||
shared.middle_relay.relay_idle_mark_seq.load(Ordering::Relaxed)
|
shared
|
||||||
|
.middle_relay
|
||||||
|
.relay_idle_mark_seq
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -842,10 +872,7 @@ pub(crate) fn desync_dedup_insert_for_testing(shared: &ProxySharedState, key: u6
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn desync_dedup_get_for_testing(
|
pub(crate) fn desync_dedup_get_for_testing(shared: &ProxySharedState, key: u64) -> Option<Instant> {
|
||||||
shared: &ProxySharedState,
|
|
||||||
key: u64,
|
|
||||||
) -> Option<Instant> {
|
|
||||||
shared
|
shared
|
||||||
.middle_relay
|
.middle_relay
|
||||||
.desync_dedup
|
.desync_dedup
|
||||||
@@ -854,7 +881,9 @@ pub(crate) fn desync_dedup_get_for_testing(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) fn desync_dedup_keys_for_testing(shared: &ProxySharedState) -> std::collections::HashSet<u64> {
|
pub(crate) fn desync_dedup_keys_for_testing(
|
||||||
|
shared: &ProxySharedState,
|
||||||
|
) -> std::collections::HashSet<u64> {
|
||||||
shared
|
shared
|
||||||
.middle_relay
|
.middle_relay
|
||||||
.desync_dedup
|
.desync_dedup
|
||||||
@@ -1027,7 +1056,12 @@ where
|
|||||||
let translated_local_addr = me_pool.translate_our_addr(local_addr);
|
let translated_local_addr = me_pool.translate_our_addr(local_addr);
|
||||||
|
|
||||||
let frame_limit = config.general.max_client_frame;
|
let frame_limit = config.general.max_client_frame;
|
||||||
let relay_idle_policy = RelayClientIdlePolicy::from_config(&config);
|
let mut relay_idle_policy = RelayClientIdlePolicy::from_config(&config);
|
||||||
|
let mut pressure_caps_applied = false;
|
||||||
|
if shared.conntrack_pressure_active() {
|
||||||
|
relay_idle_policy.apply_pressure_caps(config.server.conntrack_control.profile);
|
||||||
|
pressure_caps_applied = true;
|
||||||
|
}
|
||||||
let session_started_at = forensics.started_at;
|
let session_started_at = forensics.started_at;
|
||||||
let mut relay_idle_state = RelayClientIdleState::new(session_started_at);
|
let mut relay_idle_state = RelayClientIdleState::new(session_started_at);
|
||||||
let last_downstream_activity_ms = Arc::new(AtomicU64::new(0));
|
let last_downstream_activity_ms = Arc::new(AtomicU64::new(0));
|
||||||
@@ -1421,6 +1455,11 @@ where
|
|||||||
let mut route_watch_open = true;
|
let mut route_watch_open = true;
|
||||||
let mut seen_pressure_seq = relay_pressure_event_seq_in(shared.as_ref());
|
let mut seen_pressure_seq = relay_pressure_event_seq_in(shared.as_ref());
|
||||||
loop {
|
loop {
|
||||||
|
if shared.conntrack_pressure_active() && !pressure_caps_applied {
|
||||||
|
relay_idle_policy.apply_pressure_caps(config.server.conntrack_control.profile);
|
||||||
|
pressure_caps_applied = true;
|
||||||
|
}
|
||||||
|
|
||||||
if relay_idle_policy.enabled
|
if relay_idle_policy.enabled
|
||||||
&& maybe_evict_idle_candidate_on_pressure_in(
|
&& maybe_evict_idle_candidate_on_pressure_in(
|
||||||
shared.as_ref(),
|
shared.as_ref(),
|
||||||
@@ -1600,6 +1639,20 @@ where
|
|||||||
frames_ok = frame_counter,
|
frames_ok = frame_counter,
|
||||||
"ME relay cleanup"
|
"ME relay cleanup"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let close_reason = classify_conntrack_close_reason(&result);
|
||||||
|
let publish_result = shared.publish_conntrack_close_event(ConntrackCloseEvent {
|
||||||
|
src: peer,
|
||||||
|
dst: local_addr,
|
||||||
|
reason: close_reason,
|
||||||
|
});
|
||||||
|
if !matches!(
|
||||||
|
publish_result,
|
||||||
|
ConntrackClosePublishResult::Sent | ConntrackClosePublishResult::Disabled
|
||||||
|
) {
|
||||||
|
stats.increment_conntrack_close_event_drop_total();
|
||||||
|
}
|
||||||
|
|
||||||
clear_relay_idle_candidate_in(shared.as_ref(), conn_id);
|
clear_relay_idle_candidate_in(shared.as_ref(), conn_id);
|
||||||
me_pool.registry().unregister(conn_id).await;
|
me_pool.registry().unregister(conn_id).await;
|
||||||
buffer_pool.trim_to(buffer_pool.max_buffers().min(64));
|
buffer_pool.trim_to(buffer_pool.max_buffers().min(64));
|
||||||
@@ -1612,6 +1665,33 @@ where
|
|||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn classify_conntrack_close_reason(result: &Result<()>) -> ConntrackCloseReason {
|
||||||
|
match result {
|
||||||
|
Ok(()) => ConntrackCloseReason::NormalEof,
|
||||||
|
Err(ProxyError::Io(error)) if matches!(error.kind(), std::io::ErrorKind::TimedOut) => {
|
||||||
|
ConntrackCloseReason::Timeout
|
||||||
|
}
|
||||||
|
Err(ProxyError::Io(error))
|
||||||
|
if matches!(
|
||||||
|
error.kind(),
|
||||||
|
std::io::ErrorKind::ConnectionReset
|
||||||
|
| std::io::ErrorKind::ConnectionAborted
|
||||||
|
| std::io::ErrorKind::BrokenPipe
|
||||||
|
| std::io::ErrorKind::NotConnected
|
||||||
|
| std::io::ErrorKind::UnexpectedEof
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Reset
|
||||||
|
}
|
||||||
|
Err(ProxyError::Proxy(message))
|
||||||
|
if message.contains("pressure") || message.contains("evicted") =>
|
||||||
|
{
|
||||||
|
ConntrackCloseReason::Pressure
|
||||||
|
}
|
||||||
|
Err(_) => ConntrackCloseReason::Other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn read_client_payload_with_idle_policy_in<R>(
|
async fn read_client_payload_with_idle_policy_in<R>(
|
||||||
client_reader: &mut CryptoReader<R>,
|
client_reader: &mut CryptoReader<R>,
|
||||||
proto_tag: ProtoTag,
|
proto_tag: ProtoTag,
|
||||||
|
|||||||
+39
-1
@@ -70,6 +70,7 @@ use tracing::{debug, trace, warn};
|
|||||||
///
|
///
|
||||||
/// iOS keeps Telegram connections alive in background for up to 30 minutes.
|
/// iOS keeps Telegram connections alive in background for up to 30 minutes.
|
||||||
/// Closing earlier causes unnecessary reconnects and handshake overhead.
|
/// Closing earlier causes unnecessary reconnects and handshake overhead.
|
||||||
|
#[allow(dead_code)]
|
||||||
const ACTIVITY_TIMEOUT: Duration = Duration::from_secs(1800);
|
const ACTIVITY_TIMEOUT: Duration = Duration::from_secs(1800);
|
||||||
|
|
||||||
/// Watchdog check interval — also used for periodic rate logging.
|
/// Watchdog check interval — also used for periodic rate logging.
|
||||||
@@ -453,6 +454,7 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
|
|||||||
/// - Clean shutdown: both write sides are shut down on exit
|
/// - Clean shutdown: both write sides are shut down on exit
|
||||||
/// - Error propagation: quota exits return `ProxyError::DataQuotaExceeded`,
|
/// - Error propagation: quota exits return `ProxyError::DataQuotaExceeded`,
|
||||||
/// other I/O failures are returned as `ProxyError::Io`
|
/// other I/O failures are returned as `ProxyError::Io`
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn relay_bidirectional<CR, CW, SR, SW>(
|
pub async fn relay_bidirectional<CR, CW, SR, SW>(
|
||||||
client_reader: CR,
|
client_reader: CR,
|
||||||
client_writer: CW,
|
client_writer: CW,
|
||||||
@@ -471,6 +473,42 @@ where
|
|||||||
SR: AsyncRead + Unpin + Send + 'static,
|
SR: AsyncRead + Unpin + Send + 'static,
|
||||||
SW: AsyncWrite + Unpin + Send + 'static,
|
SW: AsyncWrite + Unpin + Send + 'static,
|
||||||
{
|
{
|
||||||
|
relay_bidirectional_with_activity_timeout(
|
||||||
|
client_reader,
|
||||||
|
client_writer,
|
||||||
|
server_reader,
|
||||||
|
server_writer,
|
||||||
|
c2s_buf_size,
|
||||||
|
s2c_buf_size,
|
||||||
|
user,
|
||||||
|
stats,
|
||||||
|
quota_limit,
|
||||||
|
_buffer_pool,
|
||||||
|
ACTIVITY_TIMEOUT,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn relay_bidirectional_with_activity_timeout<CR, CW, SR, SW>(
|
||||||
|
client_reader: CR,
|
||||||
|
client_writer: CW,
|
||||||
|
server_reader: SR,
|
||||||
|
server_writer: SW,
|
||||||
|
c2s_buf_size: usize,
|
||||||
|
s2c_buf_size: usize,
|
||||||
|
user: &str,
|
||||||
|
stats: Arc<Stats>,
|
||||||
|
quota_limit: Option<u64>,
|
||||||
|
_buffer_pool: Arc<BufferPool>,
|
||||||
|
activity_timeout: Duration,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
CR: AsyncRead + Unpin + Send + 'static,
|
||||||
|
CW: AsyncWrite + Unpin + Send + 'static,
|
||||||
|
SR: AsyncRead + Unpin + Send + 'static,
|
||||||
|
SW: AsyncWrite + Unpin + Send + 'static,
|
||||||
|
{
|
||||||
|
let activity_timeout = activity_timeout.max(Duration::from_secs(1));
|
||||||
let epoch = Instant::now();
|
let epoch = Instant::now();
|
||||||
let counters = Arc::new(SharedCounters::new());
|
let counters = Arc::new(SharedCounters::new());
|
||||||
let quota_exceeded = Arc::new(AtomicBool::new(false));
|
let quota_exceeded = Arc::new(AtomicBool::new(false));
|
||||||
@@ -512,7 +550,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Activity timeout ────────────────────────────────────
|
// ── Activity timeout ────────────────────────────────────
|
||||||
if idle >= ACTIVITY_TIMEOUT {
|
if idle >= activity_timeout {
|
||||||
let c2s = wd_counters.c2s_bytes.load(Ordering::Relaxed);
|
let c2s = wd_counters.c2s_bytes.load(Ordering::Relaxed);
|
||||||
let s2c = wd_counters.s2c_bytes.load(Ordering::Relaxed);
|
let s2c = wd_counters.s2c_bytes.load(Ordering::Relaxed);
|
||||||
warn!(
|
warn!(
|
||||||
|
|||||||
@@ -1,15 +1,40 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::collections::hash_map::RandomState;
|
use std::collections::hash_map::RandomState;
|
||||||
use std::net::IpAddr;
|
use std::net::{IpAddr, SocketAddr};
|
||||||
use std::sync::atomic::AtomicU64;
|
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use dashmap::DashMap;
|
use dashmap::DashMap;
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::proxy::handshake::{AuthProbeState, AuthProbeSaturationState};
|
use crate::proxy::handshake::{AuthProbeSaturationState, AuthProbeState};
|
||||||
use crate::proxy::middle_relay::{DesyncDedupRotationState, RelayIdleCandidateRegistry};
|
use crate::proxy::middle_relay::{DesyncDedupRotationState, RelayIdleCandidateRegistry};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ConntrackCloseReason {
|
||||||
|
NormalEof,
|
||||||
|
Timeout,
|
||||||
|
Pressure,
|
||||||
|
Reset,
|
||||||
|
Other,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub(crate) struct ConntrackCloseEvent {
|
||||||
|
pub(crate) src: SocketAddr,
|
||||||
|
pub(crate) dst: SocketAddr,
|
||||||
|
pub(crate) reason: ConntrackCloseReason,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub(crate) enum ConntrackClosePublishResult {
|
||||||
|
Sent,
|
||||||
|
Disabled,
|
||||||
|
QueueFull,
|
||||||
|
QueueClosed,
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) struct HandshakeSharedState {
|
pub(crate) struct HandshakeSharedState {
|
||||||
pub(crate) auth_probe: DashMap<IpAddr, AuthProbeState>,
|
pub(crate) auth_probe: DashMap<IpAddr, AuthProbeState>,
|
||||||
pub(crate) auth_probe_saturation: Mutex<Option<AuthProbeSaturationState>>,
|
pub(crate) auth_probe_saturation: Mutex<Option<AuthProbeSaturationState>>,
|
||||||
@@ -31,6 +56,8 @@ pub(crate) struct MiddleRelaySharedState {
|
|||||||
pub(crate) struct ProxySharedState {
|
pub(crate) struct ProxySharedState {
|
||||||
pub(crate) handshake: HandshakeSharedState,
|
pub(crate) handshake: HandshakeSharedState,
|
||||||
pub(crate) middle_relay: MiddleRelaySharedState,
|
pub(crate) middle_relay: MiddleRelaySharedState,
|
||||||
|
pub(crate) conntrack_pressure_active: AtomicBool,
|
||||||
|
pub(crate) conntrack_close_tx: Mutex<Option<mpsc::Sender<ConntrackCloseEvent>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProxySharedState {
|
impl ProxySharedState {
|
||||||
@@ -52,6 +79,68 @@ impl ProxySharedState {
|
|||||||
relay_idle_registry: Mutex::new(RelayIdleCandidateRegistry::default()),
|
relay_idle_registry: Mutex::new(RelayIdleCandidateRegistry::default()),
|
||||||
relay_idle_mark_seq: AtomicU64::new(0),
|
relay_idle_mark_seq: AtomicU64::new(0),
|
||||||
},
|
},
|
||||||
|
conntrack_pressure_active: AtomicBool::new(false),
|
||||||
|
conntrack_close_tx: Mutex::new(None),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_conntrack_close_sender(&self, tx: mpsc::Sender<ConntrackCloseEvent>) {
|
||||||
|
match self.conntrack_close_tx.lock() {
|
||||||
|
Ok(mut guard) => {
|
||||||
|
*guard = Some(tx);
|
||||||
|
}
|
||||||
|
Err(poisoned) => {
|
||||||
|
let mut guard = poisoned.into_inner();
|
||||||
|
*guard = Some(tx);
|
||||||
|
self.conntrack_close_tx.clear_poison();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn disable_conntrack_close_sender(&self) {
|
||||||
|
match self.conntrack_close_tx.lock() {
|
||||||
|
Ok(mut guard) => {
|
||||||
|
*guard = None;
|
||||||
|
}
|
||||||
|
Err(poisoned) => {
|
||||||
|
let mut guard = poisoned.into_inner();
|
||||||
|
*guard = None;
|
||||||
|
self.conntrack_close_tx.clear_poison();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn publish_conntrack_close_event(
|
||||||
|
&self,
|
||||||
|
event: ConntrackCloseEvent,
|
||||||
|
) -> ConntrackClosePublishResult {
|
||||||
|
let tx = match self.conntrack_close_tx.lock() {
|
||||||
|
Ok(guard) => guard.clone(),
|
||||||
|
Err(poisoned) => {
|
||||||
|
let guard = poisoned.into_inner();
|
||||||
|
let cloned = guard.clone();
|
||||||
|
self.conntrack_close_tx.clear_poison();
|
||||||
|
cloned
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(tx) = tx else {
|
||||||
|
return ConntrackClosePublishResult::Disabled;
|
||||||
|
};
|
||||||
|
|
||||||
|
match tx.try_send(event) {
|
||||||
|
Ok(()) => ConntrackClosePublishResult::Sent,
|
||||||
|
Err(mpsc::error::TrySendError::Full(_)) => ConntrackClosePublishResult::QueueFull,
|
||||||
|
Err(mpsc::error::TrySendError::Closed(_)) => ConntrackClosePublishResult::QueueClosed,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_conntrack_pressure_active(&self, active: bool) {
|
||||||
|
self.conntrack_pressure_active
|
||||||
|
.store(active, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn conntrack_pressure_active(&self) -> bool {
|
||||||
|
self.conntrack_pressure_active.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
static RACE_TEST_KEY_COUNTER: AtomicUsize = AtomicUsize::new(1_000_000);
|
static RACE_TEST_KEY_COUNTER: AtomicUsize = AtomicUsize::new(1_000_000);
|
||||||
|
|||||||
@@ -65,9 +65,15 @@ fn adaptive_base_tier_buffers_unchanged() {
|
|||||||
fn adaptive_tier1_buffers_within_caps() {
|
fn adaptive_tier1_buffers_within_caps() {
|
||||||
let (c2s, s2c) = direct_copy_buffers_for_tier(AdaptiveTier::Tier1, 65536, 262144);
|
let (c2s, s2c) = direct_copy_buffers_for_tier(AdaptiveTier::Tier1, 65536, 262144);
|
||||||
assert!(c2s > 65536, "Tier1 c2s should exceed Base");
|
assert!(c2s > 65536, "Tier1 c2s should exceed Base");
|
||||||
assert!(c2s <= 128 * 1024, "Tier1 c2s should not exceed DIRECT_C2S_CAP_BYTES");
|
assert!(
|
||||||
|
c2s <= 128 * 1024,
|
||||||
|
"Tier1 c2s should not exceed DIRECT_C2S_CAP_BYTES"
|
||||||
|
);
|
||||||
assert!(s2c > 262144, "Tier1 s2c should exceed Base");
|
assert!(s2c > 262144, "Tier1 s2c should exceed Base");
|
||||||
assert!(s2c <= 512 * 1024, "Tier1 s2c should not exceed DIRECT_S2C_CAP_BYTES");
|
assert!(
|
||||||
|
s2c <= 512 * 1024,
|
||||||
|
"Tier1 s2c should not exceed DIRECT_S2C_CAP_BYTES"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ fn adversarial_large_state_offsets_escape_first_scan_window() {
|
|||||||
((i.wrapping_mul(131)) & 0xff) as u8,
|
((i.wrapping_mul(131)) & 0xff) as u8,
|
||||||
));
|
));
|
||||||
let now = base + Duration::from_nanos(i);
|
let now = base + Duration::from_nanos(i);
|
||||||
let start = auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
let start =
|
||||||
|
auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
||||||
if start >= scan_limit {
|
if start >= scan_limit {
|
||||||
saw_offset_outside_first_window = true;
|
saw_offset_outside_first_window = true;
|
||||||
break;
|
break;
|
||||||
@@ -48,7 +49,8 @@ fn stress_large_state_offsets_cover_many_scan_windows() {
|
|||||||
((i.wrapping_mul(17)) & 0xff) as u8,
|
((i.wrapping_mul(17)) & 0xff) as u8,
|
||||||
));
|
));
|
||||||
let now = base + Duration::from_micros(i);
|
let now = base + Duration::from_micros(i);
|
||||||
let start = auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
let start =
|
||||||
|
auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
||||||
covered_windows.insert(start / scan_limit);
|
covered_windows.insert(start / scan_limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +82,8 @@ fn light_fuzz_offset_always_stays_inside_state_len() {
|
|||||||
let state_len = ((seed >> 16) as usize % 200_000).saturating_add(1);
|
let state_len = ((seed >> 16) as usize % 200_000).saturating_add(1);
|
||||||
let scan_limit = ((seed >> 40) as usize % 2_048).saturating_add(1);
|
let scan_limit = ((seed >> 40) as usize % 2_048).saturating_add(1);
|
||||||
let now = base + Duration::from_nanos(seed & 0x0fff);
|
let now = base + Duration::from_nanos(seed & 0x0fff);
|
||||||
let start = auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
let start =
|
||||||
|
auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
start < state_len,
|
start < state_len,
|
||||||
|
|||||||
@@ -87,7 +87,11 @@ fn adversarial_saturation_grace_requires_extra_failures_before_preauth_throttle(
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
auth_probe_should_apply_preauth_throttle_in(shared.as_ref(), ip, now + Duration::from_millis(1)),
|
auth_probe_should_apply_preauth_throttle_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
ip,
|
||||||
|
now + Duration::from_millis(1)
|
||||||
|
),
|
||||||
"after grace failures are exhausted, preauth throttle must activate"
|
"after grace failures are exhausted, preauth throttle must activate"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -134,7 +138,11 @@ fn light_fuzz_randomized_failures_preserve_cap_and_nonzero_streaks() {
|
|||||||
(seed >> 8) as u8,
|
(seed >> 8) as u8,
|
||||||
seed as u8,
|
seed as u8,
|
||||||
));
|
));
|
||||||
auth_probe_record_failure_in(shared.as_ref(), ip, now + Duration::from_millis((seed & 0x3f) as u64));
|
auth_probe_record_failure_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
ip,
|
||||||
|
now + Duration::from_millis((seed & 0x3f) as u64),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let state = auth_probe_state_for_testing_in_shared(shared.as_ref());
|
let state = auth_probe_state_for_testing_in_shared(shared.as_ref());
|
||||||
@@ -162,7 +170,11 @@ async fn stress_parallel_failure_flood_keeps_state_hard_capped() {
|
|||||||
((i >> 8) & 0xff) as u8,
|
((i >> 8) & 0xff) as u8,
|
||||||
(i & 0xff) as u8,
|
(i & 0xff) as u8,
|
||||||
));
|
));
|
||||||
auth_probe_record_failure_in(shared.as_ref(), ip, start + Duration::from_millis((i % 4) as u64));
|
auth_probe_record_failure_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
ip,
|
||||||
|
start + Duration::from_millis((i % 4) as u64),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ fn adversarial_large_state_must_allow_start_offset_outside_scan_budget_window()
|
|||||||
(i & 0xff) as u8,
|
(i & 0xff) as u8,
|
||||||
));
|
));
|
||||||
let now = base + Duration::from_micros(i as u64);
|
let now = base + Duration::from_micros(i as u64);
|
||||||
let start = auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
let start =
|
||||||
|
auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
||||||
assert!(
|
assert!(
|
||||||
start < state_len,
|
start < state_len,
|
||||||
"start offset must stay within state length; start={start}, len={state_len}"
|
"start offset must stay within state length; start={start}, len={state_len}"
|
||||||
@@ -83,7 +84,8 @@ fn light_fuzz_scan_offset_budget_never_exceeds_effective_window() {
|
|||||||
let state_len = ((seed >> 8) as usize % 131_072).saturating_add(1);
|
let state_len = ((seed >> 8) as usize % 131_072).saturating_add(1);
|
||||||
let scan_limit = ((seed >> 32) as usize % 512).saturating_add(1);
|
let scan_limit = ((seed >> 32) as usize % 512).saturating_add(1);
|
||||||
let now = base + Duration::from_nanos(seed & 0xffff);
|
let now = base + Duration::from_nanos(seed & 0xffff);
|
||||||
let start = auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
let start =
|
||||||
|
auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
start < state_len,
|
start < state_len,
|
||||||
|
|||||||
@@ -36,7 +36,13 @@ fn adversarial_many_ips_same_time_spreads_offsets_without_bias_collapse() {
|
|||||||
i as u8,
|
i as u8,
|
||||||
(255 - (i as u8)),
|
(255 - (i as u8)),
|
||||||
));
|
));
|
||||||
uniq.insert(auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, 65_536, 16));
|
uniq.insert(auth_probe_scan_start_offset_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
ip,
|
||||||
|
now,
|
||||||
|
65_536,
|
||||||
|
16,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
@@ -63,7 +69,11 @@ async fn stress_parallel_failure_churn_under_saturation_remains_capped_and_live(
|
|||||||
((i >> 8) & 0xff) as u8,
|
((i >> 8) & 0xff) as u8,
|
||||||
(i & 0xff) as u8,
|
(i & 0xff) as u8,
|
||||||
));
|
));
|
||||||
auth_probe_record_failure_in(shared.as_ref(), ip, start + Duration::from_micros((i % 128) as u64));
|
auth_probe_record_failure_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
ip,
|
||||||
|
start + Duration::from_micros((i % 128) as u64),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -73,12 +83,17 @@ async fn stress_parallel_failure_churn_under_saturation_remains_capped_and_live(
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
auth_probe_state_for_testing_in_shared(shared.as_ref()).len() <= AUTH_PROBE_TRACK_MAX_ENTRIES,
|
auth_probe_state_for_testing_in_shared(shared.as_ref()).len()
|
||||||
|
<= AUTH_PROBE_TRACK_MAX_ENTRIES,
|
||||||
"state must remain hard-capped under parallel saturation churn"
|
"state must remain hard-capped under parallel saturation churn"
|
||||||
);
|
);
|
||||||
|
|
||||||
let probe = IpAddr::V4(Ipv4Addr::new(10, 4, 1, 1));
|
let probe = IpAddr::V4(Ipv4Addr::new(10, 4, 1, 1));
|
||||||
let _ = auth_probe_should_apply_preauth_throttle_in(shared.as_ref(), probe, start + Duration::from_millis(1));
|
let _ = auth_probe_should_apply_preauth_throttle_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
probe,
|
||||||
|
start + Duration::from_millis(1),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -102,7 +117,8 @@ fn light_fuzz_scan_offset_stays_within_window_for_randomized_inputs() {
|
|||||||
let scan_limit = ((seed >> 40) as usize % 1024).saturating_add(1);
|
let scan_limit = ((seed >> 40) as usize % 1024).saturating_add(1);
|
||||||
let now = base + Duration::from_nanos(seed & 0x1fff);
|
let now = base + Duration::from_nanos(seed & 0x1fff);
|
||||||
|
|
||||||
let offset = auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
let offset =
|
||||||
|
auth_probe_scan_start_offset_in(shared.as_ref(), ip, now, state_len, scan_limit);
|
||||||
assert!(
|
assert!(
|
||||||
offset < state_len,
|
offset < state_len,
|
||||||
"scan offset must always remain inside state length"
|
"scan offset must always remain inside state length"
|
||||||
|
|||||||
@@ -116,8 +116,14 @@ async fn handshake_baseline_auth_probe_streak_increments_per_ip() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(matches!(res, HandshakeResult::BadClient { .. }));
|
assert!(matches!(res, HandshakeResult::BadClient { .. }));
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), peer.ip()), Some(expected));
|
assert_eq!(
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), untouched_ip), None);
|
auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), peer.ip()),
|
||||||
|
Some(expected)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), untouched_ip),
|
||||||
|
None
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,7 +155,8 @@ fn handshake_baseline_repeated_probes_streak_monotonic() {
|
|||||||
|
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
auth_probe_record_failure_in(shared.as_ref(), ip, now);
|
auth_probe_record_failure_in(shared.as_ref(), ip, now);
|
||||||
let current = auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), ip).unwrap_or(0);
|
let current =
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), ip).unwrap_or(0);
|
||||||
assert!(current >= prev, "streak must be monotonic");
|
assert!(current >= prev, "streak must be monotonic");
|
||||||
prev = current;
|
prev = current;
|
||||||
}
|
}
|
||||||
@@ -173,8 +180,16 @@ fn handshake_baseline_throttled_ip_incurs_backoff_delay() {
|
|||||||
let before_expiry = now + delay.saturating_sub(Duration::from_millis(1));
|
let before_expiry = now + delay.saturating_sub(Duration::from_millis(1));
|
||||||
let after_expiry = now + delay + Duration::from_millis(1);
|
let after_expiry = now + delay + Duration::from_millis(1);
|
||||||
|
|
||||||
assert!(auth_probe_is_throttled_in(shared.as_ref(), ip, before_expiry));
|
assert!(auth_probe_is_throttled_in(
|
||||||
assert!(!auth_probe_is_throttled_in(shared.as_ref(), ip, after_expiry));
|
shared.as_ref(),
|
||||||
|
ip,
|
||||||
|
before_expiry
|
||||||
|
));
|
||||||
|
assert!(!auth_probe_is_throttled_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
ip,
|
||||||
|
after_expiry
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -212,7 +227,10 @@ async fn handshake_baseline_malformed_probe_frames_fail_closed_to_masking() {
|
|||||||
.expect("malformed probe handling must complete in bounded time");
|
.expect("malformed probe handling must complete in bounded time");
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
matches!(res, HandshakeResult::BadClient { .. } | HandshakeResult::Error(_)),
|
matches!(
|
||||||
|
res,
|
||||||
|
HandshakeResult::BadClient { .. } | HandshakeResult::Error(_)
|
||||||
|
),
|
||||||
"malformed probe must fail closed"
|
"malformed probe must fail closed"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,7 +332,13 @@ async fn invalid_secret_warning_lock_contention_and_bound() {
|
|||||||
b.wait().await;
|
b.wait().await;
|
||||||
for i in 0..iterations_per_task {
|
for i in 0..iterations_per_task {
|
||||||
let user_name = format!("contention_user_{}_{}", t, i);
|
let user_name = format!("contention_user_{}_{}", t, i);
|
||||||
warn_invalid_secret_once_in(shared.as_ref(), &user_name, "invalid_hex", ACCESS_SECRET_BYTES, None);
|
warn_invalid_secret_once_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
&user_name,
|
||||||
|
"invalid_hex",
|
||||||
|
ACCESS_SECRET_BYTES,
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -629,7 +635,8 @@ fn auth_probe_saturation_note_resets_retention_window() {
|
|||||||
|
|
||||||
// This call may return false if backoff has elapsed, but it must not clear
|
// This call may return false if backoff has elapsed, but it must not clear
|
||||||
// the saturation state because `later` refreshed last_seen.
|
// the saturation state because `later` refreshed last_seen.
|
||||||
let _ = auth_probe_saturation_is_throttled_at_for_testing_in_shared(shared.as_ref(), check_time);
|
let _ =
|
||||||
|
auth_probe_saturation_is_throttled_at_for_testing_in_shared(shared.as_ref(), check_time);
|
||||||
let guard = auth_probe_saturation_state_lock_for_testing_in_shared(shared.as_ref());
|
let guard = auth_probe_saturation_state_lock_for_testing_in_shared(shared.as_ref());
|
||||||
assert!(
|
assert!(
|
||||||
guard.is_some(),
|
guard.is_some(),
|
||||||
|
|||||||
@@ -206,7 +206,12 @@ fn auth_probe_eviction_identical_timestamps_keeps_map_bounded() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let new_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 21, 21));
|
let new_ip = IpAddr::V4(Ipv4Addr::new(192, 168, 21, 21));
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(), state, new_ip, same + Duration::from_millis(1));
|
auth_probe_record_failure_with_state_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
state,
|
||||||
|
new_ip,
|
||||||
|
same + Duration::from_millis(1),
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(state.len(), AUTH_PROBE_TRACK_MAX_ENTRIES);
|
assert_eq!(state.len(), AUTH_PROBE_TRACK_MAX_ENTRIES);
|
||||||
assert!(state.contains_key(&new_ip));
|
assert!(state.contains_key(&new_ip));
|
||||||
@@ -325,7 +330,8 @@ async fn saturation_grace_exhaustion_under_concurrency_keeps_peer_throttled() {
|
|||||||
final_state.fail_streak
|
final_state.fail_streak
|
||||||
>= AUTH_PROBE_BACKOFF_START_FAILS + AUTH_PROBE_SATURATION_GRACE_FAILS
|
>= AUTH_PROBE_BACKOFF_START_FAILS + AUTH_PROBE_SATURATION_GRACE_FAILS
|
||||||
);
|
);
|
||||||
assert!(auth_probe_should_apply_preauth_throttle_in(shared.as_ref(),
|
assert!(auth_probe_should_apply_preauth_throttle_in(
|
||||||
|
shared.as_ref(),
|
||||||
peer_ip,
|
peer_ip,
|
||||||
Instant::now()
|
Instant::now()
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ fn clear_auth_probe_state_clears_saturation_even_if_poisoned() {
|
|||||||
poison_saturation_mutex(shared.as_ref());
|
poison_saturation_mutex(shared.as_ref());
|
||||||
|
|
||||||
auth_probe_note_saturation_in(shared.as_ref(), Instant::now());
|
auth_probe_note_saturation_in(shared.as_ref(), Instant::now());
|
||||||
assert!(auth_probe_saturation_is_throttled_for_testing_in_shared(shared.as_ref()));
|
assert!(auth_probe_saturation_is_throttled_for_testing_in_shared(
|
||||||
|
shared.as_ref()
|
||||||
|
));
|
||||||
|
|
||||||
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -1427,7 +1427,13 @@ fn invalid_secret_warning_cache_is_bounded() {
|
|||||||
|
|
||||||
for idx in 0..(WARNED_SECRET_MAX_ENTRIES + 32) {
|
for idx in 0..(WARNED_SECRET_MAX_ENTRIES + 32) {
|
||||||
let user = format!("warned_user_{idx}");
|
let user = format!("warned_user_{idx}");
|
||||||
warn_invalid_secret_once_in(shared.as_ref(), &user, "invalid_length", ACCESS_SECRET_BYTES, Some(idx));
|
warn_invalid_secret_once_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
&user,
|
||||||
|
"invalid_length",
|
||||||
|
ACCESS_SECRET_BYTES,
|
||||||
|
Some(idx),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let warned = warned_secrets_for_testing_in_shared(shared.as_ref());
|
let warned = warned_secrets_for_testing_in_shared(shared.as_ref());
|
||||||
@@ -1640,11 +1646,15 @@ fn unknown_sni_warn_cooldown_first_event_is_warn_and_repeated_events_are_info_un
|
|||||||
"first unknown SNI event must be eligible for WARN emission"
|
"first unknown SNI event must be eligible for WARN emission"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
!should_emit_unknown_sni_warn_for_testing_in_shared(shared.as_ref(), now + Duration::from_secs(1)),
|
!should_emit_unknown_sni_warn_for_testing_in_shared(
|
||||||
|
shared.as_ref(),
|
||||||
|
now + Duration::from_secs(1)
|
||||||
|
),
|
||||||
"events inside cooldown window must be demoted from WARN to INFO"
|
"events inside cooldown window must be demoted from WARN to INFO"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
should_emit_unknown_sni_warn_for_testing_in_shared(shared.as_ref(),
|
should_emit_unknown_sni_warn_for_testing_in_shared(
|
||||||
|
shared.as_ref(),
|
||||||
now + Duration::from_secs(UNKNOWN_SNI_WARN_COOLDOWN_SECS)
|
now + Duration::from_secs(UNKNOWN_SNI_WARN_COOLDOWN_SECS)
|
||||||
),
|
),
|
||||||
"once cooldown expires, next unknown SNI event must be WARN-eligible again"
|
"once cooldown expires, next unknown SNI event must be WARN-eligible again"
|
||||||
@@ -1725,7 +1735,12 @@ fn auth_probe_over_cap_churn_still_tracks_newcomer_after_round_limit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newcomer = IpAddr::V4(Ipv4Addr::new(203, 0, 114, 77));
|
let newcomer = IpAddr::V4(Ipv4Addr::new(203, 0, 114, 77));
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(), &state, newcomer, now + Duration::from_secs(1));
|
auth_probe_record_failure_with_state_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
&state,
|
||||||
|
newcomer,
|
||||||
|
now + Duration::from_secs(1),
|
||||||
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
state.get(&newcomer).is_some(),
|
state.get(&newcomer).is_some(),
|
||||||
@@ -1931,8 +1946,18 @@ fn auth_probe_ipv6_is_bucketed_by_prefix_64() {
|
|||||||
let ip_a = IpAddr::V6("2001:db8:abcd:1234:1:2:3:4".parse().unwrap());
|
let ip_a = IpAddr::V6("2001:db8:abcd:1234:1:2:3:4".parse().unwrap());
|
||||||
let ip_b = IpAddr::V6("2001:db8:abcd:1234:ffff:eeee:dddd:cccc".parse().unwrap());
|
let ip_b = IpAddr::V6("2001:db8:abcd:1234:ffff:eeee:dddd:cccc".parse().unwrap());
|
||||||
|
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(), &state, normalize_auth_probe_ip(ip_a), now);
|
auth_probe_record_failure_with_state_in(
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(), &state, normalize_auth_probe_ip(ip_b), now);
|
shared.as_ref(),
|
||||||
|
&state,
|
||||||
|
normalize_auth_probe_ip(ip_a),
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
auth_probe_record_failure_with_state_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
&state,
|
||||||
|
normalize_auth_probe_ip(ip_b),
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
|
||||||
let normalized = normalize_auth_probe_ip(ip_a);
|
let normalized = normalize_auth_probe_ip(ip_a);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -1956,8 +1981,18 @@ fn auth_probe_ipv6_different_prefixes_use_distinct_buckets() {
|
|||||||
let ip_a = IpAddr::V6("2001:db8:1111:2222:1:2:3:4".parse().unwrap());
|
let ip_a = IpAddr::V6("2001:db8:1111:2222:1:2:3:4".parse().unwrap());
|
||||||
let ip_b = IpAddr::V6("2001:db8:1111:3333:1:2:3:4".parse().unwrap());
|
let ip_b = IpAddr::V6("2001:db8:1111:3333:1:2:3:4".parse().unwrap());
|
||||||
|
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(), &state, normalize_auth_probe_ip(ip_a), now);
|
auth_probe_record_failure_with_state_in(
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(), &state, normalize_auth_probe_ip(ip_b), now);
|
shared.as_ref(),
|
||||||
|
&state,
|
||||||
|
normalize_auth_probe_ip(ip_a),
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
auth_probe_record_failure_with_state_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
&state,
|
||||||
|
normalize_auth_probe_ip(ip_b),
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
state.len(),
|
state.len(),
|
||||||
@@ -2070,7 +2105,12 @@ fn auth_probe_round_limited_overcap_eviction_marks_saturation_and_keeps_newcomer
|
|||||||
}
|
}
|
||||||
|
|
||||||
let newcomer = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 40));
|
let newcomer = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 40));
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(), &state, newcomer, now + Duration::from_millis(1));
|
auth_probe_record_failure_with_state_in(
|
||||||
|
shared.as_ref(),
|
||||||
|
&state,
|
||||||
|
newcomer,
|
||||||
|
now + Duration::from_millis(1),
|
||||||
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
state.get(&newcomer).is_some(),
|
state.get(&newcomer).is_some(),
|
||||||
@@ -2081,7 +2121,10 @@ fn auth_probe_round_limited_overcap_eviction_marks_saturation_and_keeps_newcomer
|
|||||||
"high fail-streak sentinel must survive round-limited eviction"
|
"high fail-streak sentinel must survive round-limited eviction"
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
auth_probe_saturation_is_throttled_at_for_testing_in_shared(shared.as_ref(), now + Duration::from_millis(1)),
|
auth_probe_saturation_is_throttled_at_for_testing_in_shared(
|
||||||
|
shared.as_ref(),
|
||||||
|
now + Duration::from_millis(1)
|
||||||
|
),
|
||||||
"round-limited over-cap path must activate saturation throttle marker"
|
"round-limited over-cap path must activate saturation throttle marker"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2163,7 +2206,8 @@ fn stress_auth_probe_overcap_churn_does_not_starve_high_threat_sentinel_bucket()
|
|||||||
((step >> 8) & 0xff) as u8,
|
((step >> 8) & 0xff) as u8,
|
||||||
(step & 0xff) as u8,
|
(step & 0xff) as u8,
|
||||||
));
|
));
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(),
|
auth_probe_record_failure_with_state_in(
|
||||||
|
shared.as_ref(),
|
||||||
&state,
|
&state,
|
||||||
newcomer,
|
newcomer,
|
||||||
base_now + Duration::from_millis(step as u64 + 1),
|
base_now + Duration::from_millis(step as u64 + 1),
|
||||||
@@ -2226,7 +2270,8 @@ fn light_fuzz_auth_probe_overcap_eviction_prefers_less_threatening_entries() {
|
|||||||
((round >> 8) & 0xff) as u8,
|
((round >> 8) & 0xff) as u8,
|
||||||
(round & 0xff) as u8,
|
(round & 0xff) as u8,
|
||||||
));
|
));
|
||||||
auth_probe_record_failure_with_state_in(shared.as_ref(),
|
auth_probe_record_failure_with_state_in(
|
||||||
|
shared.as_ref(),
|
||||||
&state,
|
&state,
|
||||||
newcomer,
|
newcomer,
|
||||||
now + Duration::from_millis(round as u64 + 1),
|
now + Duration::from_millis(round as u64 + 1),
|
||||||
@@ -3105,7 +3150,10 @@ async fn saturation_grace_boundary_still_admits_valid_tls_before_exhaustion() {
|
|||||||
matches!(result, HandshakeResult::Success(_)),
|
matches!(result, HandshakeResult::Success(_)),
|
||||||
"valid TLS should still pass while peer remains within saturation grace budget"
|
"valid TLS should still pass while peer remains within saturation grace budget"
|
||||||
);
|
);
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), peer.ip()), None);
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), peer.ip()),
|
||||||
|
None
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -3171,7 +3219,10 @@ async fn saturation_grace_exhaustion_blocks_valid_tls_until_backoff_expires() {
|
|||||||
matches!(allowed, HandshakeResult::Success(_)),
|
matches!(allowed, HandshakeResult::Success(_)),
|
||||||
"valid TLS should recover after peer-specific pre-auth backoff has elapsed"
|
"valid TLS should recover after peer-specific pre-auth backoff has elapsed"
|
||||||
);
|
);
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), peer.ip()), None);
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(shared.as_ref(), peer.ip()),
|
||||||
|
None
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
|
use rand::rngs::StdRng;
|
||||||
|
|
||||||
fn seeded_rng(seed: u64) -> StdRng {
|
fn seeded_rng(seed: u64) -> StdRng {
|
||||||
StdRng::seed_from_u64(seed)
|
StdRng::seed_from_u64(seed)
|
||||||
@@ -57,7 +57,10 @@ fn masking_lognormal_degenerate_floor_eq_ceiling_returns_floor() {
|
|||||||
let mut rng = seeded_rng(99);
|
let mut rng = seeded_rng(99);
|
||||||
for _ in 0..100 {
|
for _ in 0..100 {
|
||||||
let val = sample_lognormal_percentile_bounded(1000, 1000, &mut rng);
|
let val = sample_lognormal_percentile_bounded(1000, 1000, &mut rng);
|
||||||
assert_eq!(val, 1000, "floor == ceiling must always return exactly that value");
|
assert_eq!(
|
||||||
|
val, 1000,
|
||||||
|
"floor == ceiling must always return exactly that value"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,13 +7,22 @@ fn middle_relay_baseline_public_api_idle_roundtrip_contract() {
|
|||||||
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
||||||
|
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 7001));
|
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 7001));
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(7001));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(7001)
|
||||||
|
);
|
||||||
|
|
||||||
clear_relay_idle_candidate_for_testing(shared.as_ref(), 7001);
|
clear_relay_idle_candidate_for_testing(shared.as_ref(), 7001);
|
||||||
assert_ne!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(7001));
|
assert_ne!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(7001)
|
||||||
|
);
|
||||||
|
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 7001));
|
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 7001));
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(7001));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(7001)
|
||||||
|
);
|
||||||
|
|
||||||
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
||||||
}
|
}
|
||||||
@@ -26,7 +35,12 @@ fn middle_relay_baseline_public_api_desync_window_contract() {
|
|||||||
let key = 0xDEAD_BEEF_0000_0001u64;
|
let key = 0xDEAD_BEEF_0000_0001u64;
|
||||||
let t0 = Instant::now();
|
let t0 = Instant::now();
|
||||||
|
|
||||||
assert!(should_emit_full_desync_for_testing(shared.as_ref(), key, false, t0));
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
t0
|
||||||
|
));
|
||||||
assert!(!should_emit_full_desync_for_testing(
|
assert!(!should_emit_full_desync_for_testing(
|
||||||
shared.as_ref(),
|
shared.as_ref(),
|
||||||
key,
|
key,
|
||||||
@@ -35,7 +49,12 @@ fn middle_relay_baseline_public_api_desync_window_contract() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
let t1 = t0 + DESYNC_DEDUP_WINDOW + Duration::from_millis(10);
|
let t1 = t0 + DESYNC_DEDUP_WINDOW + Duration::from_millis(10);
|
||||||
assert!(should_emit_full_desync_for_testing(shared.as_ref(), key, false, t1));
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
t1
|
||||||
|
));
|
||||||
|
|
||||||
clear_desync_dedup_for_testing_in_shared(shared.as_ref());
|
clear_desync_dedup_for_testing_in_shared(shared.as_ref());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,12 @@ fn desync_all_full_bypass_does_not_initialize_or_grow_dedup_cache() {
|
|||||||
|
|
||||||
for i in 0..20_000u64 {
|
for i in 0..20_000u64 {
|
||||||
assert!(
|
assert!(
|
||||||
should_emit_full_desync_for_testing(shared.as_ref(), 0xD35E_D000_0000_0000u64 ^ i, true, now),
|
should_emit_full_desync_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
0xD35E_D000_0000_0000u64 ^ i,
|
||||||
|
true,
|
||||||
|
now
|
||||||
|
),
|
||||||
"desync_all_full path must always emit"
|
"desync_all_full path must always emit"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -37,7 +42,12 @@ fn desync_all_full_bypass_keeps_existing_dedup_entries_unchanged() {
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
for i in 0..2048u64 {
|
for i in 0..2048u64 {
|
||||||
assert!(
|
assert!(
|
||||||
should_emit_full_desync_for_testing(shared.as_ref(), 0xF011_F000_0000_0000u64 ^ i, true, now),
|
should_emit_full_desync_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
0xF011_F000_0000_0000u64 ^ i,
|
||||||
|
true,
|
||||||
|
now
|
||||||
|
),
|
||||||
"desync_all_full must bypass suppression and dedup refresh"
|
"desync_all_full must bypass suppression and dedup refresh"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -68,7 +78,8 @@ fn edge_all_full_burst_does_not_poison_later_false_path_tracking() {
|
|||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
for i in 0..8192u64 {
|
for i in 0..8192u64 {
|
||||||
assert!(should_emit_full_desync_for_testing(shared.as_ref(),
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
0xABCD_0000_0000_0000 ^ i,
|
0xABCD_0000_0000_0000 ^ i,
|
||||||
true,
|
true,
|
||||||
now
|
now
|
||||||
@@ -102,7 +113,12 @@ fn adversarial_mixed_sequence_true_steps_never_change_cache_len() {
|
|||||||
let flag_all_full = (seed & 0x1) == 1;
|
let flag_all_full = (seed & 0x1) == 1;
|
||||||
let key = 0x7000_0000_0000_0000u64 ^ i ^ seed;
|
let key = 0x7000_0000_0000_0000u64 ^ i ^ seed;
|
||||||
let before = desync_dedup_len_for_testing(shared.as_ref());
|
let before = desync_dedup_len_for_testing(shared.as_ref());
|
||||||
let _ = should_emit_full_desync_for_testing(shared.as_ref(), key, flag_all_full, Instant::now());
|
let _ = should_emit_full_desync_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
key,
|
||||||
|
flag_all_full,
|
||||||
|
Instant::now(),
|
||||||
|
);
|
||||||
let after = desync_dedup_len_for_testing(shared.as_ref());
|
let after = desync_dedup_len_for_testing(shared.as_ref());
|
||||||
|
|
||||||
if flag_all_full {
|
if flag_all_full {
|
||||||
@@ -124,7 +140,12 @@ fn light_fuzz_all_full_mode_always_emits_and_stays_bounded() {
|
|||||||
seed ^= seed >> 9;
|
seed ^= seed >> 9;
|
||||||
seed ^= seed << 8;
|
seed ^= seed << 8;
|
||||||
let key = seed ^ 0x55AA_55AA_55AA_55AAu64;
|
let key = seed ^ 0x55AA_55AA_55AA_55AAu64;
|
||||||
assert!(should_emit_full_desync_for_testing(shared.as_ref(), key, true, Instant::now()));
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
key,
|
||||||
|
true,
|
||||||
|
Instant::now()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let after = desync_dedup_len_for_testing(shared.as_ref());
|
let after = desync_dedup_len_for_testing(shared.as_ref());
|
||||||
|
|||||||
@@ -366,23 +366,42 @@ fn pressure_evicts_oldest_idle_candidate_with_deterministic_ordering() {
|
|||||||
|
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 10));
|
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 10));
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 11));
|
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 11));
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(10));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(10)
|
||||||
|
);
|
||||||
|
|
||||||
note_relay_pressure_event_for_testing(shared.as_ref());
|
note_relay_pressure_event_for_testing(shared.as_ref());
|
||||||
|
|
||||||
let mut seen_for_newer = 0u64;
|
let mut seen_for_newer = 0u64;
|
||||||
assert!(
|
assert!(
|
||||||
!maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 11, &mut seen_for_newer, &stats),
|
!maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
11,
|
||||||
|
&mut seen_for_newer,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"newer idle candidate must not be evicted while older candidate exists"
|
"newer idle candidate must not be evicted while older candidate exists"
|
||||||
);
|
);
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(10));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(10)
|
||||||
|
);
|
||||||
|
|
||||||
let mut seen_for_oldest = 0u64;
|
let mut seen_for_oldest = 0u64;
|
||||||
assert!(
|
assert!(
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 10, &mut seen_for_oldest, &stats),
|
maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
10,
|
||||||
|
&mut seen_for_oldest,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"oldest idle candidate must be evicted first under pressure"
|
"oldest idle candidate must be evicted first under pressure"
|
||||||
);
|
);
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(11));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(11)
|
||||||
|
);
|
||||||
assert_eq!(stats.get_relay_pressure_evict_total(), 1);
|
assert_eq!(stats.get_relay_pressure_evict_total(), 1);
|
||||||
|
|
||||||
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
||||||
@@ -402,7 +421,10 @@ fn pressure_does_not_evict_without_new_pressure_signal() {
|
|||||||
"without new pressure signal, candidate must stay"
|
"without new pressure signal, candidate must stay"
|
||||||
);
|
);
|
||||||
assert_eq!(stats.get_relay_pressure_evict_total(), 0);
|
assert_eq!(stats.get_relay_pressure_evict_total(), 0);
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(21));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(21)
|
||||||
|
);
|
||||||
|
|
||||||
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
||||||
}
|
}
|
||||||
@@ -415,7 +437,10 @@ fn stress_pressure_eviction_preserves_fifo_across_many_candidates() {
|
|||||||
|
|
||||||
let mut seen_per_conn = std::collections::HashMap::new();
|
let mut seen_per_conn = std::collections::HashMap::new();
|
||||||
for conn_id in 1000u64..1064u64 {
|
for conn_id in 1000u64..1064u64 {
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), conn_id));
|
assert!(mark_relay_idle_candidate_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
conn_id
|
||||||
|
));
|
||||||
seen_per_conn.insert(conn_id, 0u64);
|
seen_per_conn.insert(conn_id, 0u64);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,7 +451,12 @@ fn stress_pressure_eviction_preserves_fifo_across_many_candidates() {
|
|||||||
.get(&expected)
|
.get(&expected)
|
||||||
.expect("per-conn pressure cursor must exist");
|
.expect("per-conn pressure cursor must exist");
|
||||||
assert!(
|
assert!(
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), expected, &mut seen, &stats),
|
maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
expected,
|
||||||
|
&mut seen,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"expected conn_id {expected} must be evicted next by deterministic FIFO ordering"
|
"expected conn_id {expected} must be evicted next by deterministic FIFO ordering"
|
||||||
);
|
);
|
||||||
seen_per_conn.insert(expected, seen);
|
seen_per_conn.insert(expected, seen);
|
||||||
@@ -436,7 +466,10 @@ fn stress_pressure_eviction_preserves_fifo_across_many_candidates() {
|
|||||||
} else {
|
} else {
|
||||||
Some(expected + 1)
|
Some(expected + 1)
|
||||||
};
|
};
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), next);
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
next
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(stats.get_relay_pressure_evict_total(), 64);
|
assert_eq!(stats.get_relay_pressure_evict_total(), 64);
|
||||||
@@ -460,9 +493,24 @@ fn blackhat_single_pressure_event_must_not_evict_more_than_one_candidate() {
|
|||||||
// Single pressure event should authorize at most one eviction globally.
|
// Single pressure event should authorize at most one eviction globally.
|
||||||
note_relay_pressure_event_for_testing(shared.as_ref());
|
note_relay_pressure_event_for_testing(shared.as_ref());
|
||||||
|
|
||||||
let evicted_301 = maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 301, &mut seen_301, &stats);
|
let evicted_301 = maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
let evicted_302 = maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 302, &mut seen_302, &stats);
|
shared.as_ref(),
|
||||||
let evicted_303 = maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 303, &mut seen_303, &stats);
|
301,
|
||||||
|
&mut seen_301,
|
||||||
|
&stats,
|
||||||
|
);
|
||||||
|
let evicted_302 = maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
302,
|
||||||
|
&mut seen_302,
|
||||||
|
&stats,
|
||||||
|
);
|
||||||
|
let evicted_303 = maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
303,
|
||||||
|
&mut seen_303,
|
||||||
|
&stats,
|
||||||
|
);
|
||||||
|
|
||||||
let evicted_total = [evicted_301, evicted_302, evicted_303]
|
let evicted_total = [evicted_301, evicted_302, evicted_303]
|
||||||
.iter()
|
.iter()
|
||||||
@@ -492,12 +540,22 @@ fn blackhat_pressure_counter_must_track_global_budget_not_per_session_cursor() {
|
|||||||
note_relay_pressure_event_for_testing(shared.as_ref());
|
note_relay_pressure_event_for_testing(shared.as_ref());
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 401, &mut seen_oldest, &stats),
|
maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
401,
|
||||||
|
&mut seen_oldest,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"oldest candidate must consume pressure budget first"
|
"oldest candidate must consume pressure budget first"
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(
|
assert!(
|
||||||
!maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 402, &mut seen_next, &stats),
|
!maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
402,
|
||||||
|
&mut seen_next,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"next candidate must not consume the same pressure budget"
|
"next candidate must not consume the same pressure budget"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -522,7 +580,12 @@ fn blackhat_stale_pressure_before_idle_mark_must_not_trigger_eviction() {
|
|||||||
|
|
||||||
let mut seen = 0u64;
|
let mut seen = 0u64;
|
||||||
assert!(
|
assert!(
|
||||||
!maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 501, &mut seen, &stats),
|
!maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
501,
|
||||||
|
&mut seen,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"stale pressure (before soft-idle mark) must not evict newly marked candidate"
|
"stale pressure (before soft-idle mark) must not evict newly marked candidate"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -545,9 +608,24 @@ fn blackhat_stale_pressure_must_not_evict_any_of_newly_marked_batch() {
|
|||||||
let mut seen_513 = 0u64;
|
let mut seen_513 = 0u64;
|
||||||
|
|
||||||
let evicted = [
|
let evicted = [
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 511, &mut seen_511, &stats),
|
maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 512, &mut seen_512, &stats),
|
shared.as_ref(),
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 513, &mut seen_513, &stats),
|
511,
|
||||||
|
&mut seen_511,
|
||||||
|
&stats,
|
||||||
|
),
|
||||||
|
maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
512,
|
||||||
|
&mut seen_512,
|
||||||
|
&stats,
|
||||||
|
),
|
||||||
|
maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
513,
|
||||||
|
&mut seen_513,
|
||||||
|
&stats,
|
||||||
|
),
|
||||||
]
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|value| **value)
|
.filter(|value| **value)
|
||||||
@@ -572,7 +650,12 @@ fn blackhat_stale_pressure_seen_without_candidates_must_be_globally_invalidated(
|
|||||||
// Session A observed pressure while there were no candidates.
|
// Session A observed pressure while there were no candidates.
|
||||||
let mut seen_a = 0u64;
|
let mut seen_a = 0u64;
|
||||||
assert!(
|
assert!(
|
||||||
!maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 999_001, &mut seen_a, &stats),
|
!maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
999_001,
|
||||||
|
&mut seen_a,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"no candidate existed, so no eviction is possible"
|
"no candidate existed, so no eviction is possible"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -580,7 +663,12 @@ fn blackhat_stale_pressure_seen_without_candidates_must_be_globally_invalidated(
|
|||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 521));
|
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 521));
|
||||||
let mut seen_b = 0u64;
|
let mut seen_b = 0u64;
|
||||||
assert!(
|
assert!(
|
||||||
!maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 521, &mut seen_b, &stats),
|
!maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
521,
|
||||||
|
&mut seen_b,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"once pressure is observed with empty candidate set, it must not be replayed later"
|
"once pressure is observed with empty candidate set, it must not be replayed later"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -600,7 +688,12 @@ fn blackhat_stale_pressure_must_not_survive_candidate_churn() {
|
|||||||
|
|
||||||
let mut seen = 0u64;
|
let mut seen = 0u64;
|
||||||
assert!(
|
assert!(
|
||||||
!maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), 532, &mut seen, &stats),
|
!maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
532,
|
||||||
|
&mut seen,
|
||||||
|
&stats
|
||||||
|
),
|
||||||
"stale pressure must not survive clear+remark churn cycles"
|
"stale pressure must not survive clear+remark churn cycles"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -663,7 +756,10 @@ async fn integration_race_single_pressure_event_allows_at_most_one_eviction_unde
|
|||||||
let mut seen_per_session = vec![0u64; sessions];
|
let mut seen_per_session = vec![0u64; sessions];
|
||||||
|
|
||||||
for conn_id in &conn_ids {
|
for conn_id in &conn_ids {
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), *conn_id));
|
assert!(mark_relay_idle_candidate_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
*conn_id
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
for round in 0..rounds {
|
for round in 0..rounds {
|
||||||
@@ -676,8 +772,12 @@ async fn integration_race_single_pressure_event_allows_at_most_one_eviction_unde
|
|||||||
let stats = stats.clone();
|
let stats = stats.clone();
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
joins.push(tokio::spawn(async move {
|
joins.push(tokio::spawn(async move {
|
||||||
let evicted =
|
let evicted = maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), conn_id, &mut seen, stats.as_ref());
|
shared.as_ref(),
|
||||||
|
conn_id,
|
||||||
|
&mut seen,
|
||||||
|
stats.as_ref(),
|
||||||
|
);
|
||||||
(idx, conn_id, seen, evicted)
|
(idx, conn_id, seen, evicted)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -729,7 +829,10 @@ async fn integration_race_burst_pressure_with_churn_preserves_empty_set_invalida
|
|||||||
let mut seen_per_session = vec![0u64; sessions];
|
let mut seen_per_session = vec![0u64; sessions];
|
||||||
|
|
||||||
for conn_id in &conn_ids {
|
for conn_id in &conn_ids {
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), *conn_id));
|
assert!(mark_relay_idle_candidate_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
*conn_id
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut expected_total_evictions = 0u64;
|
let mut expected_total_evictions = 0u64;
|
||||||
@@ -751,8 +854,12 @@ async fn integration_race_burst_pressure_with_churn_preserves_empty_set_invalida
|
|||||||
let stats = stats.clone();
|
let stats = stats.clone();
|
||||||
let shared = shared.clone();
|
let shared = shared.clone();
|
||||||
joins.push(tokio::spawn(async move {
|
joins.push(tokio::spawn(async move {
|
||||||
let evicted =
|
let evicted = maybe_evict_idle_candidate_on_pressure_for_testing(
|
||||||
maybe_evict_idle_candidate_on_pressure_for_testing(shared.as_ref(), conn_id, &mut seen, stats.as_ref());
|
shared.as_ref(),
|
||||||
|
conn_id,
|
||||||
|
&mut seen,
|
||||||
|
stats.as_ref(),
|
||||||
|
);
|
||||||
(idx, conn_id, seen, evicted)
|
(idx, conn_id, seen, evicted)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -774,7 +881,10 @@ async fn integration_race_burst_pressure_with_churn_preserves_empty_set_invalida
|
|||||||
"round {round}: empty candidate phase must not allow stale-pressure eviction"
|
"round {round}: empty candidate phase must not allow stale-pressure eviction"
|
||||||
);
|
);
|
||||||
for conn_id in &conn_ids {
|
for conn_id in &conn_ids {
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), *conn_id));
|
assert!(mark_relay_idle_candidate_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
*conn_id
|
||||||
|
));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert!(
|
assert!(
|
||||||
@@ -783,7 +893,10 @@ async fn integration_race_burst_pressure_with_churn_preserves_empty_set_invalida
|
|||||||
);
|
);
|
||||||
if let Some(conn_id) = evicted_conn {
|
if let Some(conn_id) = evicted_conn {
|
||||||
expected_total_evictions = expected_total_evictions.saturating_add(1);
|
expected_total_evictions = expected_total_evictions.saturating_add(1);
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), conn_id));
|
assert!(mark_relay_idle_candidate_for_testing(
|
||||||
|
shared.as_ref(),
|
||||||
|
conn_id
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ fn blackhat_registry_poison_recovers_with_fail_closed_reset_and_pressure_account
|
|||||||
|
|
||||||
// Helper lock must recover from poison, reset stale state, and continue.
|
// Helper lock must recover from poison, reset stale state, and continue.
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 42));
|
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 42));
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(42));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(42)
|
||||||
|
);
|
||||||
|
|
||||||
let before = relay_pressure_event_seq_for_testing(shared.as_ref());
|
let before = relay_pressure_event_seq_for_testing(shared.as_ref());
|
||||||
note_relay_pressure_event_for_testing(shared.as_ref());
|
note_relay_pressure_event_for_testing(shared.as_ref());
|
||||||
@@ -54,11 +57,17 @@ fn clear_state_helper_must_reset_poisoned_registry_for_deterministic_fifo_tests(
|
|||||||
|
|
||||||
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
||||||
|
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), None);
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
None
|
||||||
|
);
|
||||||
assert_eq!(relay_pressure_event_seq_for_testing(shared.as_ref()), 0);
|
assert_eq!(relay_pressure_event_seq_for_testing(shared.as_ref()), 0);
|
||||||
|
|
||||||
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 7));
|
assert!(mark_relay_idle_candidate_for_testing(shared.as_ref(), 7));
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(shared.as_ref()), Some(7));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(shared.as_ref()),
|
||||||
|
Some(7)
|
||||||
|
);
|
||||||
|
|
||||||
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
clear_relay_idle_pressure_state_for_testing_in_shared(shared.as_ref());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
use crate::proxy::client::handle_client_stream_with_shared;
|
||||||
use crate::proxy::handshake::{
|
use crate::proxy::handshake::{
|
||||||
auth_probe_fail_streak_for_testing_in_shared, auth_probe_is_throttled_for_testing_in_shared,
|
auth_probe_fail_streak_for_testing_in_shared, auth_probe_is_throttled_for_testing_in_shared,
|
||||||
auth_probe_record_failure_for_testing, clear_auth_probe_state_for_testing_in_shared,
|
auth_probe_record_failure_for_testing, clear_auth_probe_state_for_testing_in_shared,
|
||||||
clear_unknown_sni_warn_state_for_testing_in_shared, clear_warned_secrets_for_testing_in_shared,
|
clear_unknown_sni_warn_state_for_testing_in_shared, clear_warned_secrets_for_testing_in_shared,
|
||||||
should_emit_unknown_sni_warn_for_testing_in_shared, warned_secrets_for_testing_in_shared,
|
should_emit_unknown_sni_warn_for_testing_in_shared, warned_secrets_for_testing_in_shared,
|
||||||
};
|
};
|
||||||
use crate::proxy::client::handle_client_stream_with_shared;
|
|
||||||
use crate::proxy::middle_relay::{
|
use crate::proxy::middle_relay::{
|
||||||
clear_desync_dedup_for_testing_in_shared, clear_relay_idle_candidate_for_testing,
|
clear_desync_dedup_for_testing_in_shared, clear_relay_idle_candidate_for_testing,
|
||||||
clear_relay_idle_pressure_state_for_testing_in_shared, mark_relay_idle_candidate_for_testing,
|
clear_relay_idle_pressure_state_for_testing_in_shared, mark_relay_idle_candidate_for_testing,
|
||||||
@@ -81,7 +81,10 @@ fn new_client_harness() -> ClientHarness {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn drive_invalid_mtproto_handshake(shared: Arc<ProxySharedState>, peer: std::net::SocketAddr) {
|
async fn drive_invalid_mtproto_handshake(
|
||||||
|
shared: Arc<ProxySharedState>,
|
||||||
|
peer: std::net::SocketAddr,
|
||||||
|
) {
|
||||||
let harness = new_client_harness();
|
let harness = new_client_harness();
|
||||||
let (server_side, mut client_side) = duplex(4096);
|
let (server_side, mut client_side) = duplex(4096);
|
||||||
let invalid = [0u8; 64];
|
let invalid = [0u8; 64];
|
||||||
@@ -108,7 +111,10 @@ async fn drive_invalid_mtproto_handshake(shared: Arc<ProxySharedState>, peer: st
|
|||||||
.write_all(&invalid)
|
.write_all(&invalid)
|
||||||
.await
|
.await
|
||||||
.expect("failed to write invalid handshake");
|
.expect("failed to write invalid handshake");
|
||||||
client_side.shutdown().await.expect("failed to shutdown client");
|
client_side
|
||||||
|
.shutdown()
|
||||||
|
.await
|
||||||
|
.expect("failed to shutdown client");
|
||||||
let _ = tokio::time::timeout(Duration::from_secs(3), task)
|
let _ = tokio::time::timeout(Duration::from_secs(3), task)
|
||||||
.await
|
.await
|
||||||
.expect("client task timed out")
|
.expect("client task timed out")
|
||||||
@@ -128,7 +134,10 @@ fn proxy_shared_state_two_instances_do_not_share_auth_probe_state() {
|
|||||||
auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip),
|
auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip),
|
||||||
Some(1)
|
Some(1)
|
||||||
);
|
);
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip), None);
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip),
|
||||||
|
None
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -139,8 +148,18 @@ fn proxy_shared_state_two_instances_do_not_share_desync_dedup() {
|
|||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let key = 0xA5A5_u64;
|
let key = 0xA5A5_u64;
|
||||||
assert!(should_emit_full_desync_for_testing(a.as_ref(), key, false, now));
|
assert!(should_emit_full_desync_for_testing(
|
||||||
assert!(should_emit_full_desync_for_testing(b.as_ref(), key, false, now));
|
a.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
now
|
||||||
|
));
|
||||||
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
b.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
now
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -150,7 +169,10 @@ fn proxy_shared_state_two_instances_do_not_share_idle_registry() {
|
|||||||
clear_relay_idle_pressure_state_for_testing_in_shared(a.as_ref());
|
clear_relay_idle_pressure_state_for_testing_in_shared(a.as_ref());
|
||||||
|
|
||||||
assert!(mark_relay_idle_candidate_for_testing(a.as_ref(), 111));
|
assert!(mark_relay_idle_candidate_for_testing(a.as_ref(), 111));
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(a.as_ref()), Some(111));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(a.as_ref()),
|
||||||
|
Some(111)
|
||||||
|
);
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(b.as_ref()), None);
|
assert_eq!(oldest_relay_idle_candidate_for_testing(b.as_ref()), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +190,10 @@ fn proxy_shared_state_reset_in_one_instance_does_not_affect_another() {
|
|||||||
auth_probe_record_failure_for_testing(b.as_ref(), ip_b, now);
|
auth_probe_record_failure_for_testing(b.as_ref(), ip_b, now);
|
||||||
clear_auth_probe_state_for_testing_in_shared(a.as_ref());
|
clear_auth_probe_state_for_testing_in_shared(a.as_ref());
|
||||||
|
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip_a), None);
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip_a),
|
||||||
|
None
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip_b),
|
auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip_b),
|
||||||
Some(1)
|
Some(1)
|
||||||
@@ -191,8 +216,14 @@ fn proxy_shared_state_parallel_auth_probe_updates_stay_per_instance() {
|
|||||||
auth_probe_record_failure_for_testing(b.as_ref(), ip, now + Duration::from_millis(1));
|
auth_probe_record_failure_for_testing(b.as_ref(), ip, now + Duration::from_millis(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip), Some(5));
|
assert_eq!(
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip), Some(3));
|
auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip),
|
||||||
|
Some(5)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip),
|
||||||
|
Some(3)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -317,8 +348,14 @@ fn proxy_shared_state_auth_saturation_does_not_bleed_across_instances() {
|
|||||||
auth_probe_record_failure_for_testing(a.as_ref(), ip, future_now);
|
auth_probe_record_failure_for_testing(a.as_ref(), ip, future_now);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!(auth_probe_is_throttled_for_testing_in_shared(a.as_ref(), ip));
|
assert!(auth_probe_is_throttled_for_testing_in_shared(
|
||||||
assert!(!auth_probe_is_throttled_for_testing_in_shared(b.as_ref(), ip));
|
a.as_ref(),
|
||||||
|
ip
|
||||||
|
));
|
||||||
|
assert!(!auth_probe_is_throttled_for_testing_in_shared(
|
||||||
|
b.as_ref(),
|
||||||
|
ip
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -348,7 +385,10 @@ fn proxy_shared_state_poison_clear_in_one_instance_does_not_affect_other_instanc
|
|||||||
|
|
||||||
clear_auth_probe_state_for_testing_in_shared(a.as_ref());
|
clear_auth_probe_state_for_testing_in_shared(a.as_ref());
|
||||||
|
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip_a), None);
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip_a),
|
||||||
|
None
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip_b),
|
auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip_b),
|
||||||
Some(1),
|
Some(1),
|
||||||
@@ -463,7 +503,10 @@ fn proxy_shared_state_warned_secret_clear_in_one_instance_does_not_clear_other()
|
|||||||
clear_warned_secrets_for_testing_in_shared(a.as_ref());
|
clear_warned_secrets_for_testing_in_shared(a.as_ref());
|
||||||
clear_warned_secrets_for_testing_in_shared(b.as_ref());
|
clear_warned_secrets_for_testing_in_shared(b.as_ref());
|
||||||
|
|
||||||
let key = ("clear-isolation-user".to_string(), "invalid_length".to_string());
|
let key = (
|
||||||
|
"clear-isolation-user".to_string(),
|
||||||
|
"invalid_length".to_string(),
|
||||||
|
);
|
||||||
{
|
{
|
||||||
let warned_a = warned_secrets_for_testing_in_shared(a.as_ref());
|
let warned_a = warned_secrets_for_testing_in_shared(a.as_ref());
|
||||||
let mut guard_a = warned_a
|
let mut guard_a = warned_a
|
||||||
@@ -508,14 +551,24 @@ fn proxy_shared_state_desync_duplicate_suppression_is_instance_scoped() {
|
|||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let key = 0xBEEF_0000_0000_0001u64;
|
let key = 0xBEEF_0000_0000_0001u64;
|
||||||
assert!(should_emit_full_desync_for_testing(a.as_ref(), key, false, now));
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
a.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
now
|
||||||
|
));
|
||||||
assert!(!should_emit_full_desync_for_testing(
|
assert!(!should_emit_full_desync_for_testing(
|
||||||
a.as_ref(),
|
a.as_ref(),
|
||||||
key,
|
key,
|
||||||
false,
|
false,
|
||||||
now + Duration::from_millis(1)
|
now + Duration::from_millis(1)
|
||||||
));
|
));
|
||||||
assert!(should_emit_full_desync_for_testing(b.as_ref(), key, false, now));
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
b.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
now
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -527,8 +580,18 @@ fn proxy_shared_state_desync_clear_in_one_instance_does_not_clear_other() {
|
|||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let key = 0xCAFE_0000_0000_0001u64;
|
let key = 0xCAFE_0000_0000_0001u64;
|
||||||
assert!(should_emit_full_desync_for_testing(a.as_ref(), key, false, now));
|
assert!(should_emit_full_desync_for_testing(
|
||||||
assert!(should_emit_full_desync_for_testing(b.as_ref(), key, false, now));
|
a.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
now
|
||||||
|
));
|
||||||
|
assert!(should_emit_full_desync_for_testing(
|
||||||
|
b.as_ref(),
|
||||||
|
key,
|
||||||
|
false,
|
||||||
|
now
|
||||||
|
));
|
||||||
|
|
||||||
clear_desync_dedup_for_testing_in_shared(a.as_ref());
|
clear_desync_dedup_for_testing_in_shared(a.as_ref());
|
||||||
|
|
||||||
@@ -558,7 +621,10 @@ fn proxy_shared_state_idle_candidate_clear_in_one_instance_does_not_affect_other
|
|||||||
clear_relay_idle_candidate_for_testing(a.as_ref(), 1001);
|
clear_relay_idle_candidate_for_testing(a.as_ref(), 1001);
|
||||||
|
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(a.as_ref()), None);
|
assert_eq!(oldest_relay_idle_candidate_for_testing(a.as_ref()), None);
|
||||||
assert_eq!(oldest_relay_idle_candidate_for_testing(b.as_ref()), Some(2002));
|
assert_eq!(
|
||||||
|
oldest_relay_idle_candidate_for_testing(b.as_ref()),
|
||||||
|
Some(2002)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
use crate::proxy::handshake::{
|
use crate::proxy::handshake::{
|
||||||
auth_probe_fail_streak_for_testing_in_shared, auth_probe_record_failure_for_testing,
|
auth_probe_fail_streak_for_testing_in_shared, auth_probe_record_failure_for_testing,
|
||||||
clear_auth_probe_state_for_testing_in_shared, clear_unknown_sni_warn_state_for_testing_in_shared,
|
clear_auth_probe_state_for_testing_in_shared,
|
||||||
|
clear_unknown_sni_warn_state_for_testing_in_shared,
|
||||||
should_emit_unknown_sni_warn_for_testing_in_shared,
|
should_emit_unknown_sni_warn_for_testing_in_shared,
|
||||||
};
|
};
|
||||||
use crate::proxy::middle_relay::{
|
use crate::proxy::middle_relay::{
|
||||||
clear_desync_dedup_for_testing_in_shared, clear_relay_idle_pressure_state_for_testing_in_shared,
|
clear_desync_dedup_for_testing_in_shared,
|
||||||
mark_relay_idle_candidate_for_testing, oldest_relay_idle_candidate_for_testing,
|
clear_relay_idle_pressure_state_for_testing_in_shared, mark_relay_idle_candidate_for_testing,
|
||||||
should_emit_full_desync_for_testing,
|
oldest_relay_idle_candidate_for_testing, should_emit_full_desync_for_testing,
|
||||||
};
|
};
|
||||||
use crate::proxy::shared_state::ProxySharedState;
|
use crate::proxy::shared_state::ProxySharedState;
|
||||||
use rand::SeedableRng;
|
|
||||||
use rand::RngExt;
|
use rand::RngExt;
|
||||||
|
use rand::SeedableRng;
|
||||||
use rand::rngs::StdRng;
|
use rand::rngs::StdRng;
|
||||||
use std::net::{IpAddr, Ipv4Addr};
|
use std::net::{IpAddr, Ipv4Addr};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -99,8 +100,14 @@ async fn proxy_shared_state_dual_instance_same_ip_high_contention_no_counter_ble
|
|||||||
handle.await.expect("task join failed");
|
handle.await.expect("task join failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip), Some(64));
|
assert_eq!(
|
||||||
assert_eq!(auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip), Some(64));
|
auth_probe_fail_streak_for_testing_in_shared(a.as_ref(), ip),
|
||||||
|
Some(64)
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
auth_probe_fail_streak_for_testing_in_shared(b.as_ref(), ip),
|
||||||
|
Some(64)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
||||||
@@ -183,12 +190,7 @@ async fn proxy_shared_state_seed_matrix_concurrency_isolation_no_counter_bleed()
|
|||||||
clear_auth_probe_state_for_testing_in_shared(shared_a.as_ref());
|
clear_auth_probe_state_for_testing_in_shared(shared_a.as_ref());
|
||||||
clear_auth_probe_state_for_testing_in_shared(shared_b.as_ref());
|
clear_auth_probe_state_for_testing_in_shared(shared_b.as_ref());
|
||||||
|
|
||||||
let ip = IpAddr::V4(Ipv4Addr::new(
|
let ip = IpAddr::V4(Ipv4Addr::new(198, 51, 100, rng.random_range(1_u8..=250_u8)));
|
||||||
198,
|
|
||||||
51,
|
|
||||||
100,
|
|
||||||
rng.random_range(1_u8..=250_u8),
|
|
||||||
));
|
|
||||||
let workers = rng.random_range(16_usize..=48_usize);
|
let workers = rng.random_range(16_usize..=48_usize);
|
||||||
let rounds = rng.random_range(4_usize..=10_usize);
|
let rounds = rng.random_range(4_usize..=10_usize);
|
||||||
|
|
||||||
@@ -210,7 +212,11 @@ async fn proxy_shared_state_seed_matrix_concurrency_isolation_no_counter_bleed()
|
|||||||
handles.push(tokio::spawn(async move {
|
handles.push(tokio::spawn(async move {
|
||||||
start_a.wait().await;
|
start_a.wait().await;
|
||||||
for _ in 0..a_ops {
|
for _ in 0..a_ops {
|
||||||
auth_probe_record_failure_for_testing(shared_a.as_ref(), ip, Instant::now());
|
auth_probe_record_failure_for_testing(
|
||||||
|
shared_a.as_ref(),
|
||||||
|
ip,
|
||||||
|
Instant::now(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -219,7 +225,11 @@ async fn proxy_shared_state_seed_matrix_concurrency_isolation_no_counter_bleed()
|
|||||||
handles.push(tokio::spawn(async move {
|
handles.push(tokio::spawn(async move {
|
||||||
start_b.wait().await;
|
start_b.wait().await;
|
||||||
for _ in 0..b_ops {
|
for _ in 0..b_ops {
|
||||||
auth_probe_record_failure_for_testing(shared_b.as_ref(), ip, Instant::now());
|
auth_probe_record_failure_for_testing(
|
||||||
|
shared_b.as_ref(),
|
||||||
|
ip,
|
||||||
|
Instant::now(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,7 +69,10 @@ async fn relay_baseline_activity_timeout_fires_after_inactivity() {
|
|||||||
.expect("relay must complete after inactivity timeout")
|
.expect("relay must complete after inactivity timeout")
|
||||||
.expect("relay task must not panic");
|
.expect("relay task must not panic");
|
||||||
|
|
||||||
assert!(done.is_ok(), "relay must return Ok(()) after inactivity timeout");
|
assert!(
|
||||||
|
done.is_ok(),
|
||||||
|
"relay must return Ok(()) after inactivity timeout"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -155,7 +158,10 @@ async fn relay_baseline_bidirectional_bytes_counted_symmetrically() {
|
|||||||
.expect("relay task must not panic");
|
.expect("relay task must not panic");
|
||||||
assert!(done.is_ok());
|
assert!(done.is_ok());
|
||||||
|
|
||||||
assert_eq!(stats.get_user_total_octets(user), (c2s.len() + s2c.len()) as u64);
|
assert_eq!(
|
||||||
|
stats.get_user_total_octets(user),
|
||||||
|
(c2s.len() + s2c.len()) as u64
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -222,7 +228,10 @@ async fn relay_baseline_broken_pipe_midtransfer_returns_error() {
|
|||||||
match done {
|
match done {
|
||||||
Err(ProxyError::Io(err)) => {
|
Err(ProxyError::Io(err)) => {
|
||||||
assert!(
|
assert!(
|
||||||
matches!(err.kind(), io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset),
|
matches!(
|
||||||
|
err.kind(),
|
||||||
|
io::ErrorKind::BrokenPipe | io::ErrorKind::ConnectionReset
|
||||||
|
),
|
||||||
"expected BrokenPipe/ConnectionReset, got {:?}",
|
"expected BrokenPipe/ConnectionReset, got {:?}",
|
||||||
err.kind()
|
err.kind()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::config::ProxyConfig;
|
use crate::config::ProxyConfig;
|
||||||
use rand::rngs::StdRng;
|
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
|
use rand::rngs::StdRng;
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -18,7 +18,10 @@ mod tests {
|
|||||||
let arc = Arc::<AtomicUsize>::from_raw(data.cast::<AtomicUsize>());
|
let arc = Arc::<AtomicUsize>::from_raw(data.cast::<AtomicUsize>());
|
||||||
let cloned = Arc::clone(&arc);
|
let cloned = Arc::clone(&arc);
|
||||||
let _ = Arc::into_raw(arc);
|
let _ = Arc::into_raw(arc);
|
||||||
RawWaker::new(Arc::into_raw(cloned).cast::<()>(), &WAKE_COUNTER_WAKER_VTABLE)
|
RawWaker::new(
|
||||||
|
Arc::into_raw(cloned).cast::<()>(),
|
||||||
|
&WAKE_COUNTER_WAKER_VTABLE,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafe fn wake_counter_wake(data: *const ()) {
|
unsafe fn wake_counter_wake(data: *const ()) {
|
||||||
|
|||||||
+2
-2
@@ -159,8 +159,8 @@ MemoryDenyWriteExecute=true
|
|||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
|
|
||||||
# Allow binding to privileged ports and writing to specific paths
|
# Allow binding to privileged ports and writing to specific paths
|
||||||
AmbientCapabilities=CAP_NET_BIND_SERVICE
|
AmbientCapabilities=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
|
||||||
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
|
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_NET_ADMIN
|
||||||
ReadWritePaths=/etc/telemt /var/run /var/lib/telemt
|
ReadWritePaths=/etc/telemt /var/run /var/lib/telemt
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -91,6 +91,17 @@ pub struct Stats {
|
|||||||
current_connections_direct: AtomicU64,
|
current_connections_direct: AtomicU64,
|
||||||
current_connections_me: AtomicU64,
|
current_connections_me: AtomicU64,
|
||||||
handshake_timeouts: AtomicU64,
|
handshake_timeouts: AtomicU64,
|
||||||
|
accept_permit_timeout_total: AtomicU64,
|
||||||
|
conntrack_control_enabled_gauge: AtomicBool,
|
||||||
|
conntrack_control_available_gauge: AtomicBool,
|
||||||
|
conntrack_pressure_active_gauge: AtomicBool,
|
||||||
|
conntrack_event_queue_depth_gauge: AtomicU64,
|
||||||
|
conntrack_rule_apply_ok_gauge: AtomicBool,
|
||||||
|
conntrack_delete_attempt_total: AtomicU64,
|
||||||
|
conntrack_delete_success_total: AtomicU64,
|
||||||
|
conntrack_delete_not_found_total: AtomicU64,
|
||||||
|
conntrack_delete_error_total: AtomicU64,
|
||||||
|
conntrack_close_event_drop_total: AtomicU64,
|
||||||
upstream_connect_attempt_total: AtomicU64,
|
upstream_connect_attempt_total: AtomicU64,
|
||||||
upstream_connect_success_total: AtomicU64,
|
upstream_connect_success_total: AtomicU64,
|
||||||
upstream_connect_fail_total: AtomicU64,
|
upstream_connect_fail_total: AtomicU64,
|
||||||
@@ -528,6 +539,74 @@ impl Stats {
|
|||||||
self.handshake_timeouts.fetch_add(1, Ordering::Relaxed);
|
self.handshake_timeouts.fetch_add(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn increment_accept_permit_timeout_total(&self) {
|
||||||
|
if self.telemetry_core_enabled() {
|
||||||
|
self.accept_permit_timeout_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_conntrack_control_enabled(&self, enabled: bool) {
|
||||||
|
self.conntrack_control_enabled_gauge
|
||||||
|
.store(enabled, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_conntrack_control_available(&self, available: bool) {
|
||||||
|
self.conntrack_control_available_gauge
|
||||||
|
.store(available, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_conntrack_pressure_active(&self, active: bool) {
|
||||||
|
self.conntrack_pressure_active_gauge
|
||||||
|
.store(active, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_conntrack_event_queue_depth(&self, depth: u64) {
|
||||||
|
self.conntrack_event_queue_depth_gauge
|
||||||
|
.store(depth, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_conntrack_rule_apply_ok(&self, ok: bool) {
|
||||||
|
self.conntrack_rule_apply_ok_gauge
|
||||||
|
.store(ok, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_conntrack_delete_attempt_total(&self) {
|
||||||
|
if self.telemetry_core_enabled() {
|
||||||
|
self.conntrack_delete_attempt_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_conntrack_delete_success_total(&self) {
|
||||||
|
if self.telemetry_core_enabled() {
|
||||||
|
self.conntrack_delete_success_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_conntrack_delete_not_found_total(&self) {
|
||||||
|
if self.telemetry_core_enabled() {
|
||||||
|
self.conntrack_delete_not_found_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_conntrack_delete_error_total(&self) {
|
||||||
|
if self.telemetry_core_enabled() {
|
||||||
|
self.conntrack_delete_error_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_conntrack_close_event_drop_total(&self) {
|
||||||
|
if self.telemetry_core_enabled() {
|
||||||
|
self.conntrack_close_event_drop_total
|
||||||
|
.fetch_add(1, Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn increment_upstream_connect_attempt_total(&self) {
|
pub fn increment_upstream_connect_attempt_total(&self) {
|
||||||
if self.telemetry_core_enabled() {
|
if self.telemetry_core_enabled() {
|
||||||
self.upstream_connect_attempt_total
|
self.upstream_connect_attempt_total
|
||||||
@@ -1477,6 +1556,9 @@ impl Stats {
|
|||||||
pub fn get_connects_bad(&self) -> u64 {
|
pub fn get_connects_bad(&self) -> u64 {
|
||||||
self.connects_bad.load(Ordering::Relaxed)
|
self.connects_bad.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
pub fn get_accept_permit_timeout_total(&self) -> u64 {
|
||||||
|
self.accept_permit_timeout_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
pub fn get_current_connections_direct(&self) -> u64 {
|
pub fn get_current_connections_direct(&self) -> u64 {
|
||||||
self.current_connections_direct.load(Ordering::Relaxed)
|
self.current_connections_direct.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
@@ -1487,6 +1569,40 @@ impl Stats {
|
|||||||
self.get_current_connections_direct()
|
self.get_current_connections_direct()
|
||||||
.saturating_add(self.get_current_connections_me())
|
.saturating_add(self.get_current_connections_me())
|
||||||
}
|
}
|
||||||
|
pub fn get_conntrack_control_enabled(&self) -> bool {
|
||||||
|
self.conntrack_control_enabled_gauge.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_control_available(&self) -> bool {
|
||||||
|
self.conntrack_control_available_gauge
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_pressure_active(&self) -> bool {
|
||||||
|
self.conntrack_pressure_active_gauge.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_event_queue_depth(&self) -> u64 {
|
||||||
|
self.conntrack_event_queue_depth_gauge
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_rule_apply_ok(&self) -> bool {
|
||||||
|
self.conntrack_rule_apply_ok_gauge.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_delete_attempt_total(&self) -> u64 {
|
||||||
|
self.conntrack_delete_attempt_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_delete_success_total(&self) -> u64 {
|
||||||
|
self.conntrack_delete_success_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_delete_not_found_total(&self) -> u64 {
|
||||||
|
self.conntrack_delete_not_found_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_delete_error_total(&self) -> u64 {
|
||||||
|
self.conntrack_delete_error_total.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
|
pub fn get_conntrack_close_event_drop_total(&self) -> u64 {
|
||||||
|
self.conntrack_close_event_drop_total
|
||||||
|
.load(Ordering::Relaxed)
|
||||||
|
}
|
||||||
pub fn get_me_keepalive_sent(&self) -> u64 {
|
pub fn get_me_keepalive_sent(&self) -> u64 {
|
||||||
self.me_keepalive_sent.load(Ordering::Relaxed)
|
self.me_keepalive_sent.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user