Compare commits

...

14 Commits

Author SHA1 Message Date
Alexey
cd0771eee4 Merge pull request #715 from telemt/flow
Fixes in TLS-F
2026-04-17 13:00:30 +03:00
Alexey
a858dd799e Bump 2026-04-17 12:43:41 +03:00
Alexey
947ef2beb7 Fixes in TLS-F 2026-04-17 12:38:22 +03:00
Alexey
376f9b42fb Traffic Control + Fairness + Evaluating hard-idle timeout + Improve FakeTLS server-flight fidelity + PROXY Protocol V2 UNKNOWN/LOCAL misuse fixes: merge pull request #714 from telemt/flow
Traffic Control + Fairness + Evaluating hard-idle timeout + Improve FakeTLS server-flight fidelity + PROXY Protocol V2 UNKNOWN/LOCAL misuse fixes
2026-04-17 11:54:18 +03:00
Alexey
191ca35076 Update scheduler.rs 2026-04-17 11:20:58 +03:00
Alexey
44485a545e Fixes for unused imports 2026-04-17 11:06:42 +03:00
Alexey
17a966b822 Rustfmt 2026-04-17 10:48:01 +03:00
Alexey
073eacbb37 PROXY Protocol V2 UNKNOWN/LOCAL misuse fixes for TLS-Fetcher by #713
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
2026-04-17 10:43:49 +03:00
Alexey
7494cb3092 Merge pull request #692 from ne4sp/patch-1
FIx XRAY_DOUBLE_HOP.md files.
2026-04-16 18:39:36 +03:00
Alexey
d25aa5a1e9 Merge pull request #709 from groozchique/main
[docs] add hyperlinks to README
2026-04-16 16:12:48 +03:00
Nick Parfyonov
f1b7b9aa08 [docs] add hyperlinks to README 2026-04-16 09:40:55 +03:00
ne4sp
3f69b54f5d Update XRAY_DOUBLE_HOP.ru.md 2026-04-12 16:15:52 +03:00
ne4sp
62a90e05a0 Update XRAY_DOUBLE_HOP.en.md 2026-04-12 15:59:59 +03:00
ne4sp
1b3d2d8bc5 Update XRAY_DOUBLE_HOP.ru.md
При инициализации xray на сервере не запускался из-за длинного shortID.
2026-04-12 15:45:01 +03:00
20 changed files with 231 additions and 115 deletions

2
Cargo.lock generated
View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# Telemt - MTProxy on Rust + Tokio # Telemt - MTProxy on Rust + Tokio
![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon) ![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social) ![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs) [![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon)](https://github.com/telemt/telemt/releases/latest) [![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social)](https://github.com/telemt/telemt/stargazers) [![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social)](https://github.com/telemt/telemt/network/members) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs)
[🇷🇺 README на русском](https://github.com/telemt/telemt/blob/main/README.ru.md) [🇷🇺 README на русском](https://github.com/telemt/telemt/blob/main/README.ru.md)

View File

@@ -1,6 +1,6 @@
# Telemt — MTProxy на Rust + Tokio # Telemt — MTProxy на Rust + Tokio
![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon) ![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social) ![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs) [![Latest Release](https://img.shields.io/github/v/release/telemt/telemt?color=neon)](https://github.com/telemt/telemt/releases/latest) [![Stars](https://img.shields.io/github/stars/telemt/telemt?style=social)](https://github.com/telemt/telemt/stargazers) [![Forks](https://img.shields.io/github/forks/telemt/telemt?style=social)](https://github.com/telemt/telemt/network/members) [![Telegram](https://img.shields.io/badge/Telegram-Chat-24a1de?logo=telegram&logoColor=24a1de)](https://t.me/telemtrs)
***Решает проблемы раньше, чем другие узнают об их существовании*** ***Решает проблемы раньше, чем другие узнают об их существовании***

View File

@@ -37,13 +37,13 @@ xray x25519
``` ```
3. **Short ID (Reality identifier):** 3. **Short ID (Reality identifier):**
```bash ```bash
openssl rand -hex 16 openssl rand -hex 8
# Save the output (e.g.: 0123456789abcdef0123456789abcdef) — this is <SHORT_ID> # Save the output (e.g.: abc123def456) — this is <SHORT_ID>
``` ```
4. **Random Path (for xhttp):** 4. **Random Path (for xhttp):**
```bash ```bash
openssl rand -hex 8 openssl rand -hex 16
# Save the output (e.g., abc123def456) to replace <YOUR_RANDOM_PATH> in configs # Save the output (e.g., 0123456789abcdef0123456789abcdef) to replace <YOUR_RANDOM_PATH> in configs
``` ```
--- ---

View File

@@ -37,13 +37,13 @@ xray x25519
``` ```
3. **Short ID (идентификатор Reality):** 3. **Short ID (идентификатор Reality):**
```bash ```bash
openssl rand -hex 16 openssl rand -hex 8
# Сохраните вывод (например: 0123456789abcdef0123456789abcdef) — это <SHORT_ID> # Сохраните вывод (например: abc123def456) — это <SHORT_ID>
``` ```
4. **Random Path (путь для xhttp):** 4. **Random Path (путь для xhttp):**
```bash ```bash
openssl rand -hex 8 openssl rand -hex 16
# Сохраните вывод (например, abc123def456), чтобы заменить <YOUR_RANDOM_PATH> в конфигах # Сохраните вывод (например, 0123456789abcdef0123456789abcdef), чтобы заменить <YOUR_RANDOM_PATH> в конфигах
``` ```
--- ---

View File

@@ -122,7 +122,8 @@ pub struct HotFields {
pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>, pub user_expirations: std::collections::HashMap<String, chrono::DateTime<chrono::Utc>>,
pub user_data_quota: std::collections::HashMap<String, u64>, pub user_data_quota: std::collections::HashMap<String, u64>,
pub user_rate_limits: std::collections::HashMap<String, crate::config::RateLimitBps>, pub user_rate_limits: std::collections::HashMap<String, crate::config::RateLimitBps>,
pub cidr_rate_limits: std::collections::HashMap<ipnetwork::IpNetwork, crate::config::RateLimitBps>, pub cidr_rate_limits:
std::collections::HashMap<ipnetwork::IpNetwork, crate::config::RateLimitBps>,
pub user_max_unique_ips: std::collections::HashMap<String, usize>, pub user_max_unique_ips: std::collections::HashMap<String, usize>,
pub user_max_unique_ips_global_each: usize, pub user_max_unique_ips_global_each: usize,
pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode, pub user_max_unique_ips_mode: crate::config::UserMaxUniqueIpsMode,

View File

@@ -8,8 +8,8 @@ use std::io::{self, Read, Write};
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use nix::fcntl::{Flock, FlockArg};
use nix::errno::Errno; use nix::errno::Errno;
use nix::fcntl::{Flock, FlockArg};
use nix::unistd::{self, ForkResult, Gid, Pid, Uid, chdir, close, fork, getpid, setsid}; use nix::unistd::{self, ForkResult, Gid, Pid, Uid, chdir, close, fork, getpid, setsid};
use tracing::{debug, info, warn}; use tracing::{debug, info, warn};
@@ -158,15 +158,15 @@ fn redirect_stdio_to_devnull() -> Result<(), DaemonError> {
unsafe { unsafe {
// Redirect stdin (fd 0) // Redirect stdin (fd 0)
if libc::dup2(devnull_fd, 0) < 0 { if libc::dup2(devnull_fd, 0) < 0 {
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last())); return Err(DaemonError::RedirectFailed(Errno::last()));
} }
// Redirect stdout (fd 1) // Redirect stdout (fd 1)
if libc::dup2(devnull_fd, 1) < 0 { if libc::dup2(devnull_fd, 1) < 0 {
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last())); return Err(DaemonError::RedirectFailed(Errno::last()));
} }
// Redirect stderr (fd 2) // Redirect stderr (fd 2)
if libc::dup2(devnull_fd, 2) < 0 { if libc::dup2(devnull_fd, 2) < 0 {
return Err(DaemonError::RedirectFailed(nix::errno::Errno::last())); return Err(DaemonError::RedirectFailed(Errno::last()));
} }
} }
@@ -350,11 +350,7 @@ fn set_supplementary_groups(gid: Gid) -> Result<(), nix::Error> {
groups.as_ptr(), groups.as_ptr(),
) )
}; };
if rc == 0 { if rc == 0 { Ok(()) } else { Err(Errno::last()) }
Ok(())
} else {
Err(Errno::last())
}
} }
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]

View File

@@ -190,8 +190,16 @@ pub(crate) async fn spawn_runtime_tasks(
); );
let mut config_rx_rate_limits = config_rx.clone(); let mut config_rx_rate_limits = config_rx.clone();
tokio::spawn(async move { tokio::spawn(async move {
let mut prev_user_limits = config_rx_rate_limits.borrow().access.user_rate_limits.clone(); let mut prev_user_limits = config_rx_rate_limits
let mut prev_cidr_limits = config_rx_rate_limits.borrow().access.cidr_rate_limits.clone(); .borrow()
.access
.user_rate_limits
.clone();
let mut prev_cidr_limits = config_rx_rate_limits
.borrow()
.access
.cidr_rate_limits
.clone();
loop { loop {
if config_rx_rate_limits.changed().await.is_err() { if config_rx_rate_limits.changed().await.is_err() {
break; break;

View File

@@ -316,7 +316,9 @@ where
stats.increment_user_connects(user); stats.increment_user_connects(user);
let _direct_connection_lease = stats.acquire_direct_connection_lease(); let _direct_connection_lease = stats.acquire_direct_connection_lease();
let traffic_lease = shared.traffic_limiter.acquire_lease(user, success.peer.ip()); let traffic_lease = shared
.traffic_limiter
.acquire_lease(user, success.peer.ip());
let buffer_pool_trim = Arc::clone(&buffer_pool); let buffer_pool_trim = Arc::clone(&buffer_pool);
let relay_activity_timeout = if shared.conntrack_pressure_active() { let relay_activity_timeout = if shared.conntrack_pressure_active() {

View File

@@ -289,17 +289,9 @@ impl<S> StatsIo<S> {
let Some(started_at) = wait.started_at.take() else { let Some(started_at) = wait.started_at.take() else {
return; return;
}; };
let wait_ms = started_at let wait_ms = started_at.elapsed().as_millis().min(u128::from(u64::MAX)) as u64;
.elapsed()
.as_millis()
.min(u128::from(u64::MAX)) as u64;
if let Some(lease) = lease { if let Some(lease) = lease {
lease.observe_wait_ms( lease.observe_wait_ms(direction, wait.blocked_user, wait.blocked_cidr, wait_ms);
direction,
wait.blocked_user,
wait.blocked_cidr,
wait_ms,
);
} }
wait.blocked_user = false; wait.blocked_user = false;
wait.blocked_cidr = false; wait.blocked_cidr = false;
@@ -340,8 +332,7 @@ impl<S> StatsIo<S> {
while self.c2s_rate_debt_bytes > 0 { while self.c2s_rate_debt_bytes > 0 {
let consume = lease.try_consume(RateDirection::Up, self.c2s_rate_debt_bytes); let consume = lease.try_consume(RateDirection::Up, self.c2s_rate_debt_bytes);
if consume.granted > 0 { if consume.granted > 0 {
self.c2s_rate_debt_bytes = self.c2s_rate_debt_bytes = self.c2s_rate_debt_bytes.saturating_sub(consume.granted);
self.c2s_rate_debt_bytes.saturating_sub(consume.granted);
continue; continue;
} }
Self::arm_wait( Self::arm_wait(
@@ -647,7 +638,10 @@ impl<S: AsyncWrite + Unpin> AsyncWrite for StatsIo<S> {
match Pin::new(&mut this.inner).poll_write(cx, write_buf) { match Pin::new(&mut this.inner).poll_write(cx, write_buf) {
Poll::Ready(Ok(n)) => { Poll::Ready(Ok(n)) => {
if reserved_bytes > n as u64 { if reserved_bytes > n as u64 {
refund_reserved_quota_bytes(this.user_stats.as_ref(), reserved_bytes - n as u64); refund_reserved_quota_bytes(
this.user_stats.as_ref(),
reserved_bytes - n as u64,
);
} }
if shaper_reserved_bytes > n as u64 if shaper_reserved_bytes > n as u64
&& let Some(lease) = this.traffic_lease.as_ref() && let Some(lease) = this.traffic_lease.as_ref()

View File

@@ -74,7 +74,8 @@ impl ScopeMetrics {
self.wait_up_ms_total.fetch_add(wait_ms, Ordering::Relaxed); self.wait_up_ms_total.fetch_add(wait_ms, Ordering::Relaxed);
} }
RateDirection::Down => { RateDirection::Down => {
self.wait_down_ms_total.fetch_add(wait_ms, Ordering::Relaxed); self.wait_down_ms_total
.fetch_add(wait_ms, Ordering::Relaxed);
} }
} }
} }
@@ -254,9 +255,7 @@ impl CidrDirectionBucket {
let grant = if guaranteed_remaining > 0 { let grant = if guaranteed_remaining > 0 {
requested.min(guaranteed_remaining).min(total_remaining) requested.min(guaranteed_remaining).min(total_remaining)
} else { } else {
requested requested.min(total_remaining).min(MAX_BORROW_CHUNK_BYTES)
.min(total_remaining)
.min(MAX_BORROW_CHUNK_BYTES)
}; };
if grant == 0 { if grant == 0 {
@@ -266,12 +265,7 @@ impl CidrDirectionBucket {
let next_total = total_used.saturating_add(grant); let next_total = total_used.saturating_add(grant);
if self if self
.used .used
.compare_exchange_weak( .compare_exchange_weak(total_used, next_total, Ordering::Relaxed, Ordering::Relaxed)
total_used,
next_total,
Ordering::Relaxed,
Ordering::Relaxed,
)
.is_ok() .is_ok()
{ {
user_state.used.fetch_add(grant, Ordering::Relaxed); user_state.used.fetch_add(grant, Ordering::Relaxed);
@@ -430,8 +424,14 @@ struct PolicySnapshot {
impl PolicySnapshot { impl PolicySnapshot {
fn match_cidr(&self, ip: IpAddr) -> Option<&CidrRule> { fn match_cidr(&self, ip: IpAddr) -> Option<&CidrRule> {
match ip { match ip {
IpAddr::V4(_) => self.cidr_rules_v4.iter().find(|rule| rule.cidr.contains(ip)), IpAddr::V4(_) => self
IpAddr::V6(_) => self.cidr_rules_v6.iter().find(|rule| rule.cidr.contains(ip)), .cidr_rules_v4
.iter()
.find(|rule| rule.cidr.contains(ip)),
IpAddr::V6(_) => self
.cidr_rules_v6
.iter()
.find(|rule| rule.cidr.contains(ip)),
} }
} }
} }
@@ -535,7 +535,8 @@ impl TrafficLease {
if let (Some(cidr_bucket), Some(cidr_user_share)) = if let (Some(cidr_bucket), Some(cidr_user_share)) =
(self.cidr_bucket.as_ref(), self.cidr_user_share.as_ref()) (self.cidr_bucket.as_ref(), self.cidr_user_share.as_ref())
{ {
let cidr_granted = cidr_bucket.try_consume_for_user(direction, cidr_user_share, granted); let cidr_granted =
cidr_bucket.try_consume_for_user(direction, cidr_user_share, granted);
if cidr_granted < granted if cidr_granted < granted
&& let Some(user_bucket) = self.user_bucket.as_ref() && let Some(user_bucket) = self.user_bucket.as_ref()
{ {
@@ -693,7 +694,9 @@ impl TrafficLimiter {
.get_or_insert_with(user, || UserBucket::new(limit)); .get_or_insert_with(user, || UserBucket::new(limit));
bucket.set_rates(limit); bucket.set_rates(limit);
bucket.active_leases.fetch_add(1, Ordering::Relaxed); bucket.active_leases.fetch_add(1, Ordering::Relaxed);
self.user_scope.active_leases.fetch_add(1, Ordering::Relaxed); self.user_scope
.active_leases
.fetch_add(1, Ordering::Relaxed);
user_bucket = Some(bucket); user_bucket = Some(bucket);
} }
@@ -706,7 +709,9 @@ impl TrafficLimiter {
.get_or_insert_with(rule.key.as_str(), || CidrBucket::new(rule.limits)); .get_or_insert_with(rule.key.as_str(), || CidrBucket::new(rule.limits));
bucket.set_rates(rule.limits); bucket.set_rates(rule.limits);
bucket.active_leases.fetch_add(1, Ordering::Relaxed); bucket.active_leases.fetch_add(1, Ordering::Relaxed);
self.cidr_scope.active_leases.fetch_add(1, Ordering::Relaxed); self.cidr_scope
.active_leases
.fetch_add(1, Ordering::Relaxed);
let share = bucket.acquire_user_share(user); let share = bucket.acquire_user_share(user);
cidr_user_key = Some(user.to_string()); cidr_user_key = Some(user.to_string());
cidr_user_share = Some(share); cidr_user_share = Some(share);
@@ -784,7 +789,8 @@ impl TrafficLimiter {
let policy = self.policy.load_full(); let policy = self.policy.load_full();
self.user_buckets.retain(|user, bucket| { self.user_buckets.retain(|user, bucket| {
bucket.active_leases.load(Ordering::Relaxed) > 0 || policy.user_limits.contains_key(user) bucket.active_leases.load(Ordering::Relaxed) > 0
|| policy.user_limits.contains_key(user)
}); });
self.cidr_buckets.retain(|cidr_key, bucket| { self.cidr_buckets.retain(|cidr_key, bucket| {
bucket.cleanup_idle_users(); bucket.cleanup_idle_users();

View File

@@ -18,6 +18,9 @@ fn jitter_and_clamp_sizes(sizes: &[usize], rng: &SecureRandom) -> Vec<usize> {
.iter() .iter()
.map(|&size| { .map(|&size| {
let base = size.clamp(MIN_APP_DATA, MAX_APP_DATA); let base = size.clamp(MIN_APP_DATA, MAX_APP_DATA);
if base == MIN_APP_DATA || base == MAX_APP_DATA {
return base;
}
let jitter_range = ((base as f64) * 0.03).round() as i64; let jitter_range = ((base as f64) * 0.03).round() as i64;
if jitter_range == 0 { if jitter_range == 0 {
return base; return base;
@@ -98,7 +101,9 @@ fn emulated_ticket_record_sizes(
let target_count = sizes let target_count = sizes
.len() .len()
.max(usize::from(new_session_tickets.min(MAX_TICKET_RECORDS as u8))) .max(usize::from(
new_session_tickets.min(MAX_TICKET_RECORDS as u8),
))
.min(MAX_TICKET_RECORDS); .min(MAX_TICKET_RECORDS);
while sizes.len() < target_count { while sizes.len() < target_count {
@@ -329,11 +334,11 @@ pub fn build_emulated_server_hello(
let mut tickets = Vec::new(); let mut tickets = Vec::new();
for ticket_len in emulated_ticket_record_sizes(cached, new_session_tickets, rng) { for ticket_len in emulated_ticket_record_sizes(cached, new_session_tickets, rng) {
let mut rec = Vec::with_capacity(5 + ticket_len); let mut rec = Vec::with_capacity(5 + ticket_len);
rec.push(TLS_RECORD_APPLICATION); rec.push(TLS_RECORD_APPLICATION);
rec.extend_from_slice(&TLS_VERSION); rec.extend_from_slice(&TLS_VERSION);
rec.extend_from_slice(&(ticket_len as u16).to_be_bytes()); rec.extend_from_slice(&(ticket_len as u16).to_be_bytes());
rec.extend_from_slice(&rng.bytes(ticket_len)); rec.extend_from_slice(&rng.bytes(ticket_len));
tickets.extend_from_slice(&rec); tickets.extend_from_slice(&rec);
} }
let mut response = Vec::with_capacity( let mut response = Vec::with_capacity(

View File

@@ -1,6 +1,7 @@
#![allow(clippy::too_many_arguments)] #![allow(clippy::too_many_arguments)]
use dashmap::DashMap; use dashmap::DashMap;
use std::net::SocketAddr;
use std::sync::Arc; use std::sync::Arc;
use std::sync::OnceLock; use std::sync::OnceLock;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -793,6 +794,51 @@ async fn connect_tcp_with_upstream(
)) ))
} }
fn socket_addrs_from_upstream_stream(
stream: &UpstreamStream,
) -> (Option<SocketAddr>, Option<SocketAddr>) {
match stream {
UpstreamStream::Tcp(tcp) => (tcp.local_addr().ok(), tcp.peer_addr().ok()),
UpstreamStream::Shadowsocks(_) => (None, None),
}
}
fn build_tls_fetch_proxy_header(
proxy_protocol: u8,
src_addr: Option<SocketAddr>,
dst_addr: Option<SocketAddr>,
) -> Option<Vec<u8>> {
match proxy_protocol {
0 => None,
2 => {
let header = match (src_addr, dst_addr) {
(Some(src @ SocketAddr::V4(_)), Some(dst @ SocketAddr::V4(_)))
| (Some(src @ SocketAddr::V6(_)), Some(dst @ SocketAddr::V6(_))) => {
ProxyProtocolV2Builder::new().with_addrs(src, dst).build()
}
_ => ProxyProtocolV2Builder::new().build(),
};
Some(header)
}
_ => {
let header = match (src_addr, dst_addr) {
(Some(SocketAddr::V4(src)), Some(SocketAddr::V4(dst))) => {
ProxyProtocolV1Builder::new()
.tcp4(src.into(), dst.into())
.build()
}
(Some(SocketAddr::V6(src)), Some(SocketAddr::V6(dst))) => {
ProxyProtocolV1Builder::new()
.tcp6(src.into(), dst.into())
.build()
}
_ => ProxyProtocolV1Builder::new().build(),
};
Some(header)
}
}
}
fn encode_tls13_certificate_message(cert_chain_der: &[Vec<u8>]) -> Option<Vec<u8>> { fn encode_tls13_certificate_message(cert_chain_der: &[Vec<u8>]) -> Option<Vec<u8>> {
if cert_chain_der.is_empty() { if cert_chain_der.is_empty() {
return None; return None;
@@ -824,7 +870,7 @@ async fn fetch_via_raw_tls_stream<S>(
mut stream: S, mut stream: S,
sni: &str, sni: &str,
connect_timeout: Duration, connect_timeout: Duration,
proxy_protocol: u8, proxy_header: Option<Vec<u8>>,
profile: TlsFetchProfile, profile: TlsFetchProfile,
grease_enabled: bool, grease_enabled: bool,
deterministic: bool, deterministic: bool,
@@ -835,11 +881,7 @@ where
let rng = SecureRandom::new(); let rng = SecureRandom::new();
let client_hello = build_client_hello(sni, &rng, profile, grease_enabled, deterministic); let client_hello = build_client_hello(sni, &rng, profile, grease_enabled, deterministic);
timeout(connect_timeout, async { timeout(connect_timeout, async {
if proxy_protocol > 0 { if let Some(header) = proxy_header.as_ref() {
let header = match proxy_protocol {
2 => ProxyProtocolV2Builder::new().build(),
_ => ProxyProtocolV1Builder::new().build(),
};
stream.write_all(&header).await?; stream.write_all(&header).await?;
} }
stream.write_all(&client_hello).await?; stream.write_all(&client_hello).await?;
@@ -921,11 +963,12 @@ async fn fetch_via_raw_tls(
sock = %sock_path, sock = %sock_path,
"Raw TLS fetch using mask unix socket" "Raw TLS fetch using mask unix socket"
); );
let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, None, None);
return fetch_via_raw_tls_stream( return fetch_via_raw_tls_stream(
stream, stream,
sni, sni,
connect_timeout, connect_timeout,
proxy_protocol, proxy_header,
profile, profile,
grease_enabled, grease_enabled,
deterministic, deterministic,
@@ -956,11 +999,13 @@ async fn fetch_via_raw_tls(
let stream = let stream =
connect_tcp_with_upstream(host, port, connect_timeout, upstream, scope, strict_route) connect_tcp_with_upstream(host, port, connect_timeout, upstream, scope, strict_route)
.await?; .await?;
let (src_addr, dst_addr) = socket_addrs_from_upstream_stream(&stream);
let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, src_addr, dst_addr);
fetch_via_raw_tls_stream( fetch_via_raw_tls_stream(
stream, stream,
sni, sni,
connect_timeout, connect_timeout,
proxy_protocol, proxy_header,
profile, profile,
grease_enabled, grease_enabled,
deterministic, deterministic,
@@ -972,17 +1017,13 @@ async fn fetch_via_rustls_stream<S>(
mut stream: S, mut stream: S,
host: &str, host: &str,
sni: &str, sni: &str,
proxy_protocol: u8, proxy_header: Option<Vec<u8>>,
) -> Result<TlsFetchResult> ) -> Result<TlsFetchResult>
where where
S: AsyncRead + AsyncWrite + Unpin, S: AsyncRead + AsyncWrite + Unpin,
{ {
// rustls handshake path for certificate and basic negotiated metadata. // rustls handshake path for certificate and basic negotiated metadata.
if proxy_protocol > 0 { if let Some(header) = proxy_header.as_ref() {
let header = match proxy_protocol {
2 => ProxyProtocolV2Builder::new().build(),
_ => ProxyProtocolV1Builder::new().build(),
};
stream.write_all(&header).await?; stream.write_all(&header).await?;
stream.flush().await?; stream.flush().await?;
} }
@@ -1082,7 +1123,8 @@ async fn fetch_via_rustls(
sock = %sock_path, sock = %sock_path,
"Rustls fetch using mask unix socket" "Rustls fetch using mask unix socket"
); );
return fetch_via_rustls_stream(stream, host, sni, proxy_protocol).await; let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, None, None);
return fetch_via_rustls_stream(stream, host, sni, proxy_header).await;
} }
Ok(Err(e)) => { Ok(Err(e)) => {
warn!( warn!(
@@ -1108,7 +1150,9 @@ async fn fetch_via_rustls(
let stream = let stream =
connect_tcp_with_upstream(host, port, connect_timeout, upstream, scope, strict_route) connect_tcp_with_upstream(host, port, connect_timeout, upstream, scope, strict_route)
.await?; .await?;
fetch_via_rustls_stream(stream, host, sni, proxy_protocol).await let (src_addr, dst_addr) = socket_addrs_from_upstream_stream(&stream);
let proxy_header = build_tls_fetch_proxy_header(proxy_protocol, src_addr, dst_addr);
fetch_via_rustls_stream(stream, host, sni, proxy_header).await
} }
/// Fetch real TLS metadata with an adaptive multi-profile strategy. /// Fetch real TLS metadata with an adaptive multi-profile strategy.
@@ -1278,11 +1322,13 @@ pub async fn fetch_real_tls(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::net::SocketAddr;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use super::{ use super::{
ProfileCacheValue, TlsFetchStrategy, build_client_hello, derive_behavior_profile, ProfileCacheValue, TlsFetchStrategy, build_client_hello, build_tls_fetch_proxy_header,
encode_tls13_certificate_message, order_profiles, profile_cache, profile_cache_key, derive_behavior_profile, encode_tls13_certificate_message, order_profiles, profile_cache,
profile_cache_key,
}; };
use crate::config::TlsFetchProfile; use crate::config::TlsFetchProfile;
use crate::crypto::SecureRandom; use crate::crypto::SecureRandom;
@@ -1423,4 +1469,48 @@ mod tests {
assert_eq!(first, second); assert_eq!(first, second);
} }
#[test]
fn test_build_tls_fetch_proxy_header_v2_with_tcp_addrs() {
let src: SocketAddr = "198.51.100.10:42000".parse().expect("valid src");
let dst: SocketAddr = "203.0.113.20:443".parse().expect("valid dst");
let header = build_tls_fetch_proxy_header(2, Some(src), Some(dst)).expect("header");
assert_eq!(
&header[..12],
&[
0x0d, 0x0a, 0x0d, 0x0a, 0x00, 0x0d, 0x0a, 0x51, 0x55, 0x49, 0x54, 0x0a
]
);
assert_eq!(header[12], 0x21);
assert_eq!(header[13], 0x11);
assert_eq!(u16::from_be_bytes([header[14], header[15]]), 12);
assert_eq!(&header[16..20], &[198, 51, 100, 10]);
assert_eq!(&header[20..24], &[203, 0, 113, 20]);
assert_eq!(u16::from_be_bytes([header[24], header[25]]), 42000);
assert_eq!(u16::from_be_bytes([header[26], header[27]]), 443);
}
#[test]
fn test_build_tls_fetch_proxy_header_v2_mixed_family_falls_back_to_local_command() {
let src: SocketAddr = "198.51.100.10:42000".parse().expect("valid src");
let dst: SocketAddr = "[2001:db8::20]:443".parse().expect("valid dst");
let header = build_tls_fetch_proxy_header(2, Some(src), Some(dst)).expect("header");
assert_eq!(header[12], 0x20);
assert_eq!(header[13], 0x00);
assert_eq!(u16::from_be_bytes([header[14], header[15]]), 0);
}
#[test]
fn test_build_tls_fetch_proxy_header_v1_with_tcp_addrs() {
let src: SocketAddr = "198.51.100.10:42000".parse().expect("valid src");
let dst: SocketAddr = "203.0.113.20:443".parse().expect("valid dst");
let header = build_tls_fetch_proxy_header(1, Some(src), Some(dst)).expect("header");
assert_eq!(
header,
b"PROXY TCP4 198.51.100.10 203.0.113.20 42000 443\r\n"
);
}
} }

View File

@@ -7,7 +7,7 @@ mod model;
mod pressure; mod pressure;
mod scheduler; mod scheduler;
pub(crate) use model::{ #[cfg(test)]
AdmissionDecision, DispatchAction, DispatchFeedback, PressureState, SchedulerDecision, pub(crate) use model::PressureState;
}; pub(crate) use model::{AdmissionDecision, DispatchAction, DispatchFeedback, SchedulerDecision};
pub(crate) use scheduler::{WorkerFairnessConfig, WorkerFairnessSnapshot, WorkerFairnessState}; pub(crate) use scheduler::{WorkerFairnessConfig, WorkerFairnessSnapshot, WorkerFairnessState};

View File

@@ -136,8 +136,8 @@ impl PressureEvaluator {
let queue_ratio_pct = if max_total_queued_bytes == 0 { let queue_ratio_pct = if max_total_queued_bytes == 0 {
100 100
} else { } else {
((signals.total_queued_bytes.saturating_mul(100)) / max_total_queued_bytes) ((signals.total_queued_bytes.saturating_mul(100)) / max_total_queued_bytes).min(100)
.min(100) as u8 as u8
}; };
let standing_ratio_pct = if signals.active_flows == 0 { let standing_ratio_pct = if signals.active_flows == 0 {

View File

@@ -166,7 +166,8 @@ impl WorkerFairnessState {
return AdmissionDecision::RejectSaturated; return AdmissionDecision::RejectSaturated;
} }
if self.total_queued_bytes.saturating_add(frame_bytes) > self.config.max_total_queued_bytes { if self.total_queued_bytes.saturating_add(frame_bytes) > self.config.max_total_queued_bytes
{
self.pressure self.pressure
.note_admission_reject(now, &self.config.pressure); .note_admission_reject(now, &self.config.pressure);
self.enqueue_rejects = self.enqueue_rejects.saturating_add(1); self.enqueue_rejects = self.enqueue_rejects.saturating_add(1);
@@ -211,7 +212,8 @@ impl WorkerFairnessState {
.expect("flow inserted must be retrievable") .expect("flow inserted must be retrievable")
}; };
if entry.fairness.pending_bytes.saturating_add(frame_bytes) > self.config.max_flow_queued_bytes if entry.fairness.pending_bytes.saturating_add(frame_bytes)
> self.config.max_flow_queued_bytes
{ {
self.pressure self.pressure
.note_admission_reject(now, &self.config.pressure); .note_admission_reject(now, &self.config.pressure);
@@ -237,7 +239,8 @@ impl WorkerFairnessState {
entry.queue.push_back(frame); entry.queue.push_back(frame);
self.total_queued_bytes = self.total_queued_bytes.saturating_add(frame_bytes); self.total_queued_bytes = self.total_queued_bytes.saturating_add(frame_bytes);
self.bucket_queued_bytes[bucket_id] = self.bucket_queued_bytes[bucket_id].saturating_add(frame_bytes); self.bucket_queued_bytes[bucket_id] =
self.bucket_queued_bytes[bucket_id].saturating_add(frame_bytes);
if !entry.fairness.in_active_ring { if !entry.fairness.in_active_ring {
entry.fairness.in_active_ring = true; entry.fairness.in_active_ring = true;
@@ -277,23 +280,31 @@ impl WorkerFairnessState {
Self::classify_flow(&self.config, pressure_state, now, &mut flow.fairness); Self::classify_flow(&self.config, pressure_state, now, &mut flow.fairness);
let quantum = Self::effective_quantum_bytes(&self.config, pressure_state, &flow.fairness); let quantum =
flow.fairness.deficit_bytes = Self::effective_quantum_bytes(&self.config, pressure_state, &flow.fairness);
flow.fairness.deficit_bytes.saturating_add(i64::from(quantum)); flow.fairness.deficit_bytes = flow
.fairness
.deficit_bytes
.saturating_add(i64::from(quantum));
self.deficit_grants = self.deficit_grants.saturating_add(1); self.deficit_grants = self.deficit_grants.saturating_add(1);
let front_len = flow.queue.front().map_or(0, |front| front.queued_bytes()); let front_len = flow.queue.front().map_or(0, |front| front.queued_bytes());
if flow.fairness.deficit_bytes < front_len as i64 { if flow.fairness.deficit_bytes < front_len as i64 {
flow.fairness.consecutive_skips = flow.fairness.consecutive_skips.saturating_add(1); flow.fairness.consecutive_skips =
flow.fairness.consecutive_skips.saturating_add(1);
self.deficit_skips = self.deficit_skips.saturating_add(1); self.deficit_skips = self.deficit_skips.saturating_add(1);
requeue_active = true; requeue_active = true;
} else if let Some(frame) = flow.queue.pop_front() { } else if let Some(frame) = flow.queue.pop_front() {
drained_bytes = frame.queued_bytes(); drained_bytes = frame.queued_bytes();
flow.fairness.pending_bytes = flow.fairness.pending_bytes.saturating_sub(drained_bytes); flow.fairness.pending_bytes =
flow.fairness.deficit_bytes = flow.fairness.pending_bytes.saturating_sub(drained_bytes);
flow.fairness.deficit_bytes.saturating_sub(drained_bytes as i64); flow.fairness.deficit_bytes = flow
.fairness
.deficit_bytes
.saturating_sub(drained_bytes as i64);
flow.fairness.consecutive_skips = 0; flow.fairness.consecutive_skips = 0;
flow.fairness.queue_started_at = flow.queue.front().map(|front| front.enqueued_at); flow.fairness.queue_started_at =
flow.queue.front().map(|front| front.enqueued_at);
requeue_active = !flow.queue.is_empty(); requeue_active = !flow.queue.is_empty();
if !requeue_active { if !requeue_active {
flow.fairness.scheduler_state = FlowSchedulerState::Idle; flow.fairness.scheduler_state = FlowSchedulerState::Idle;
@@ -359,7 +370,8 @@ impl WorkerFairnessState {
return DispatchAction::Continue; return DispatchAction::Continue;
}; };
flow.fairness.consecutive_stalls = flow.fairness.consecutive_stalls.saturating_add(1); flow.fairness.consecutive_stalls =
flow.fairness.consecutive_stalls.saturating_add(1);
flow.fairness.scheduler_state = FlowSchedulerState::Backpressured; flow.fairness.scheduler_state = FlowSchedulerState::Backpressured;
flow.fairness.pressure_class = FlowPressureClass::Backpressured; flow.fairness.pressure_class = FlowPressureClass::Backpressured;
@@ -376,10 +388,10 @@ impl WorkerFairnessState {
} else { } else {
let frame_bytes = candidate.frame.queued_bytes(); let frame_bytes = candidate.frame.queued_bytes();
flow.queue.push_front(candidate.frame); flow.queue.push_front(candidate.frame);
flow.fairness.pending_bytes = flow.fairness.pending_bytes.saturating_add(frame_bytes); flow.fairness.pending_bytes =
if flow.fairness.queue_started_at.is_none() { flow.fairness.pending_bytes.saturating_add(frame_bytes);
flow.fairness.queue_started_at = Some(now); flow.fairness.queue_started_at =
} flow.queue.front().map(|front| front.enqueued_at);
self.total_queued_bytes = self.total_queued_bytes.saturating_add(frame_bytes); self.total_queued_bytes = self.total_queued_bytes.saturating_add(frame_bytes);
self.bucket_queued_bytes[flow.fairness.bucket_id] = self.bucket_queued_bytes self.bucket_queued_bytes[flow.fairness.bucket_id] = self.bucket_queued_bytes
[flow.fairness.bucket_id] [flow.fairness.bucket_id]
@@ -390,7 +402,8 @@ impl WorkerFairnessState {
} }
} }
if flow.fairness.consecutive_stalls >= self.config.max_consecutive_stalls_before_close if flow.fairness.consecutive_stalls
>= self.config.max_consecutive_stalls_before_close
&& self.pressure.state() == PressureState::Saturated && self.pressure.state() == PressureState::Saturated
{ {
self.remove_flow(conn_id); self.remove_flow(conn_id);
@@ -414,18 +427,16 @@ impl WorkerFairnessState {
return; return;
}; };
self.bucket_active_flows[entry.fairness.bucket_id] = self.bucket_active_flows self.bucket_active_flows[entry.fairness.bucket_id] =
[entry.fairness.bucket_id] self.bucket_active_flows[entry.fairness.bucket_id].saturating_sub(1);
.saturating_sub(1);
let mut reclaimed = 0u64; let mut reclaimed = 0u64;
for frame in entry.queue { for frame in entry.queue {
reclaimed = reclaimed.saturating_add(frame.queued_bytes()); reclaimed = reclaimed.saturating_add(frame.queued_bytes());
} }
self.total_queued_bytes = self.total_queued_bytes.saturating_sub(reclaimed); self.total_queued_bytes = self.total_queued_bytes.saturating_sub(reclaimed);
self.bucket_queued_bytes[entry.fairness.bucket_id] = self.bucket_queued_bytes self.bucket_queued_bytes[entry.fairness.bucket_id] =
[entry.fairness.bucket_id] self.bucket_queued_bytes[entry.fairness.bucket_id].saturating_sub(reclaimed);
.saturating_sub(reclaimed);
} }
fn evaluate_pressure(&mut self, now: Instant, force: bool) { fn evaluate_pressure(&mut self, now: Instant, force: bool) {

View File

@@ -3,6 +3,9 @@
mod codec; mod codec;
mod config_updater; mod config_updater;
mod fairness; mod fairness;
#[cfg(test)]
#[path = "tests/fairness_security_tests.rs"]
mod fairness_security_tests;
mod handshake; mod handshake;
mod health; mod health;
#[cfg(test)] #[cfg(test)]
@@ -31,9 +34,6 @@ mod pool_writer;
#[cfg(test)] #[cfg(test)]
#[path = "tests/pool_writer_security_tests.rs"] #[path = "tests/pool_writer_security_tests.rs"]
mod pool_writer_security_tests; mod pool_writer_security_tests;
#[cfg(test)]
#[path = "tests/fairness_security_tests.rs"]
mod fairness_security_tests;
mod reader; mod reader;
mod registry; mod registry;
mod rotation; mod rotation;

View File

@@ -118,20 +118,22 @@ fn apply_fairness_metrics_delta(
stats.set_me_fair_backpressured_flows_gauge(current.backpressured_flows as u64); stats.set_me_fair_backpressured_flows_gauge(current.backpressured_flows as u64);
stats.set_me_fair_pressure_state_gauge(current.pressure_state.as_u8() as u64); stats.set_me_fair_pressure_state_gauge(current.pressure_state.as_u8() as u64);
stats.add_me_fair_scheduler_rounds_total( stats.add_me_fair_scheduler_rounds_total(
current.scheduler_rounds.saturating_sub(prev.scheduler_rounds), current
.scheduler_rounds
.saturating_sub(prev.scheduler_rounds),
); );
stats.add_me_fair_deficit_grants_total( stats.add_me_fair_deficit_grants_total(
current.deficit_grants.saturating_sub(prev.deficit_grants), current.deficit_grants.saturating_sub(prev.deficit_grants),
); );
stats.add_me_fair_deficit_skips_total( stats.add_me_fair_deficit_skips_total(current.deficit_skips.saturating_sub(prev.deficit_skips));
current.deficit_skips.saturating_sub(prev.deficit_skips),
);
stats.add_me_fair_enqueue_rejects_total( stats.add_me_fair_enqueue_rejects_total(
current.enqueue_rejects.saturating_sub(prev.enqueue_rejects), current.enqueue_rejects.saturating_sub(prev.enqueue_rejects),
); );
stats.add_me_fair_shed_drops_total(current.shed_drops.saturating_sub(prev.shed_drops)); stats.add_me_fair_shed_drops_total(current.shed_drops.saturating_sub(prev.shed_drops));
stats.add_me_fair_penalties_total( stats.add_me_fair_penalties_total(
current.fairness_penalties.saturating_sub(prev.fairness_penalties), current
.fairness_penalties
.saturating_sub(prev.fairness_penalties),
); );
stats.add_me_fair_downstream_stalls_total( stats.add_me_fair_downstream_stalls_total(
current current

View File

@@ -175,7 +175,8 @@ fn fairness_randomized_sequence_preserves_memory_bounds() {
} else { } else {
DispatchFeedback::QueueFull DispatchFeedback::QueueFull
}; };
let _ = fairness.apply_dispatch_feedback(candidate.frame.conn_id, candidate, feedback, now); let _ =
fairness.apply_dispatch_feedback(candidate.frame.conn_id, candidate, feedback, now);
} }
let snapshot = fairness.snapshot(); let snapshot = fairness.snapshot();