mirror of
https://github.com/telemt/telemt.git
synced 2026-04-17 18:44:10 +03:00
Security hardening, concurrency fixes, and expanded test coverage
This commit introduces a comprehensive set of improvements to enhance the security, reliability, and configurability of the proxy server, specifically targeting adversarial resilience and high-load concurrency. Security & Cryptography: - Zeroize MTProto cryptographic key material (`dec_key`, `enc_key`) immediately after use to prevent memory leakage on early returns. - Move TLS handshake replay tracking after full policy/ALPN validation to prevent cache poisoning by unauthenticated probes. - Add `proxy_protocol_trusted_cidrs` configuration to restrict PROXY protocol headers to trusted networks, rejecting spoofed IPs. Adversarial Resilience & DoS Mitigation: - Implement "Tiny Frame Debt" tracking in the middle-relay to prevent CPU exhaustion from malicious 0-byte or 1-byte frame floods. - Add `mask_relay_max_bytes` to strictly bound unauthenticated fallback connections, preventing the proxy from being abused as an open relay. - Add a 5ms prefetch window (`mask_classifier_prefetch_timeout_ms`) to correctly assemble and classify fragmented HTTP/1.1 and HTTP/2 probes (e.g., `PRI * HTTP/2.0`) before routing them to masking heuristics. - Prevent recursive masking loops (FD exhaustion) by verifying the mask target is not the proxy's own listener via local interface enumeration. Concurrency & Reliability: - Eliminate executor waker storms during quota lock contention by replacing the spin-waker task with inline `Sleep` and exponential backoff. - Roll back user quota reservations (`rollback_me2c_quota_reservation`) if a network write fails, preventing Head-of-Line (HoL) blocking from permanently burning data quotas. - Recover gracefully from idle-registry `Mutex` poisoning instead of panicking, ensuring isolated thread failures do not break the proxy. - Fix `auth_probe_scan_start_offset` modulo logic to ensure bounds safety. Testing: - Add extensive adversarial, timing, fuzzing, and invariant test suites for both the client and handshake modules.
This commit is contained in:
@@ -1884,6 +1884,32 @@ impl Stats {
|
||||
stats.octets_to_client.fetch_add(bytes, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn sub_user_octets_to(&self, user: &str, bytes: u64) {
|
||||
if !self.telemetry_user_enabled() {
|
||||
return;
|
||||
}
|
||||
self.maybe_cleanup_user_stats();
|
||||
let Some(stats) = self.user_stats.get(user) else {
|
||||
return;
|
||||
};
|
||||
|
||||
Self::touch_user_stats(stats.value());
|
||||
let counter = &stats.octets_to_client;
|
||||
let mut current = counter.load(Ordering::Relaxed);
|
||||
loop {
|
||||
let next = current.saturating_sub(bytes);
|
||||
match counter.compare_exchange_weak(
|
||||
current,
|
||||
next,
|
||||
Ordering::Relaxed,
|
||||
Ordering::Relaxed,
|
||||
) {
|
||||
Ok(_) => break,
|
||||
Err(actual) => current = actual,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn increment_user_msgs_from(&self, user: &str) {
|
||||
if !self.telemetry_user_enabled() {
|
||||
return;
|
||||
@@ -2440,3 +2466,7 @@ mod connection_lease_security_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "tests/replay_checker_security_tests.rs"]
|
||||
mod replay_checker_security_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/user_octets_sub_security_tests.rs"]
|
||||
mod user_octets_sub_security_tests;
|
||||
|
||||
151
src/stats/tests/user_octets_sub_security_tests.rs
Normal file
151
src/stats/tests/user_octets_sub_security_tests.rs
Normal file
@@ -0,0 +1,151 @@
|
||||
use super::*;
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
#[test]
|
||||
fn sub_user_octets_to_underflow_saturates_at_zero() {
|
||||
let stats = Stats::new();
|
||||
let user = "sub-underflow-user";
|
||||
|
||||
stats.add_user_octets_to(user, 3);
|
||||
stats.sub_user_octets_to(user, 100);
|
||||
|
||||
assert_eq!(stats.get_user_total_octets(user), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_user_octets_to_does_not_affect_octets_from_client() {
|
||||
let stats = Stats::new();
|
||||
let user = "sub-isolation-user";
|
||||
|
||||
stats.add_user_octets_from(user, 17);
|
||||
stats.add_user_octets_to(user, 5);
|
||||
stats.sub_user_octets_to(user, 3);
|
||||
|
||||
assert_eq!(stats.get_user_total_octets(user), 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn light_fuzz_add_sub_model_matches_saturating_reference() {
|
||||
let stats = Stats::new();
|
||||
let user = "sub-fuzz-user";
|
||||
let mut seed = 0x91D2_4CB8_EE77_1101u64;
|
||||
let mut model_to = 0u64;
|
||||
|
||||
for _ in 0..8192 {
|
||||
seed ^= seed << 7;
|
||||
seed ^= seed >> 9;
|
||||
seed ^= seed << 8;
|
||||
|
||||
let amt = ((seed >> 8) & 0x3f) + 1;
|
||||
if (seed & 1) == 0 {
|
||||
stats.add_user_octets_to(user, amt);
|
||||
model_to = model_to.saturating_add(amt);
|
||||
} else {
|
||||
stats.sub_user_octets_to(user, amt);
|
||||
model_to = model_to.saturating_sub(amt);
|
||||
}
|
||||
}
|
||||
|
||||
assert_eq!(stats.get_user_total_octets(user), model_to);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stress_parallel_add_sub_never_underflows_or_panics() {
|
||||
let stats = Arc::new(Stats::new());
|
||||
let user = "sub-stress-user";
|
||||
// Pre-fund with a large offset so subtractions never saturate at zero.
|
||||
// This guarantees commutative updates, making the final state deterministic.
|
||||
let base_offset = 10_000_000u64;
|
||||
stats.add_user_octets_to(user, base_offset);
|
||||
|
||||
let mut workers = Vec::new();
|
||||
|
||||
for tid in 0..16u64 {
|
||||
let stats_for_thread = Arc::clone(&stats);
|
||||
workers.push(thread::spawn(move || {
|
||||
let mut seed = 0xD00D_1000_0000_0000u64 ^ tid;
|
||||
let mut net_delta = 0i64;
|
||||
for _ in 0..4096 {
|
||||
seed ^= seed << 7;
|
||||
seed ^= seed >> 9;
|
||||
seed ^= seed << 8;
|
||||
let amt = ((seed >> 8) & 0x1f) + 1;
|
||||
|
||||
if (seed & 1) == 0 {
|
||||
stats_for_thread.add_user_octets_to(user, amt);
|
||||
net_delta += amt as i64;
|
||||
} else {
|
||||
stats_for_thread.sub_user_octets_to(user, amt);
|
||||
net_delta -= amt as i64;
|
||||
}
|
||||
}
|
||||
|
||||
net_delta
|
||||
}));
|
||||
}
|
||||
|
||||
let mut expected_net_delta = 0i64;
|
||||
for worker in workers {
|
||||
expected_net_delta += worker
|
||||
.join()
|
||||
.expect("sub-user stress worker must not panic");
|
||||
}
|
||||
|
||||
let expected_total = (base_offset as i64 + expected_net_delta) as u64;
|
||||
let total = stats.get_user_total_octets(user);
|
||||
assert_eq!(
|
||||
total, expected_total,
|
||||
"concurrent add/sub lost updates or suffered ABA races"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sub_user_octets_to_missing_user_is_noop() {
|
||||
let stats = Stats::new();
|
||||
stats.sub_user_octets_to("missing-user", 1024);
|
||||
assert_eq!(stats.get_user_total_octets("missing-user"), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stress_parallel_per_user_models_remain_exact() {
|
||||
let stats = Arc::new(Stats::new());
|
||||
let mut workers = Vec::new();
|
||||
|
||||
for tid in 0..16u64 {
|
||||
let stats_for_thread = Arc::clone(&stats);
|
||||
workers.push(thread::spawn(move || {
|
||||
let user = format!("sub-per-user-{tid}");
|
||||
let mut seed = 0xFACE_0000_0000_0000u64 ^ tid;
|
||||
let mut model = 0u64;
|
||||
|
||||
for _ in 0..4096 {
|
||||
seed ^= seed << 7;
|
||||
seed ^= seed >> 9;
|
||||
seed ^= seed << 8;
|
||||
let amt = ((seed >> 8) & 0x3f) + 1;
|
||||
|
||||
if (seed & 1) == 0 {
|
||||
stats_for_thread.add_user_octets_to(&user, amt);
|
||||
model = model.saturating_add(amt);
|
||||
} else {
|
||||
stats_for_thread.sub_user_octets_to(&user, amt);
|
||||
model = model.saturating_sub(amt);
|
||||
}
|
||||
}
|
||||
|
||||
(user, model)
|
||||
}));
|
||||
}
|
||||
|
||||
for worker in workers {
|
||||
let (user, model) = worker
|
||||
.join()
|
||||
.expect("per-user subtract stress worker must not panic");
|
||||
assert_eq!(
|
||||
stats.get_user_total_octets(&user),
|
||||
model,
|
||||
"per-user parallel model diverged"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user