mirror of
https://github.com/telemt/telemt.git
synced 2026-05-02 01: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:
100
src/proxy/tests/masking_rng_hoist_perf_regression_tests.rs
Normal file
100
src/proxy/tests/masking_rng_hoist_perf_regression_tests.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use super::*;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::time::{Duration, timeout};
|
||||
|
||||
async fn collect_padding(
|
||||
total_sent: usize,
|
||||
enabled: bool,
|
||||
floor: usize,
|
||||
cap: usize,
|
||||
above_cap_blur: bool,
|
||||
blur_max: usize,
|
||||
aggressive: bool,
|
||||
) -> Vec<u8> {
|
||||
let (mut tx, mut rx) = tokio::io::duplex(256 * 1024);
|
||||
|
||||
maybe_write_shape_padding(
|
||||
&mut tx,
|
||||
total_sent,
|
||||
enabled,
|
||||
floor,
|
||||
cap,
|
||||
above_cap_blur,
|
||||
blur_max,
|
||||
aggressive,
|
||||
)
|
||||
.await;
|
||||
|
||||
drop(tx);
|
||||
|
||||
let mut output = Vec::new();
|
||||
timeout(Duration::from_secs(1), rx.read_to_end(&mut output))
|
||||
.await
|
||||
.expect("reading padded output timed out")
|
||||
.expect("failed reading padded output");
|
||||
output
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn padding_output_is_not_all_zero() {
|
||||
let output = collect_padding(1, true, 256, 4096, false, 0, false).await;
|
||||
|
||||
assert!(
|
||||
output.len() >= 255,
|
||||
"expected at least 255 padding bytes, got {}",
|
||||
output.len()
|
||||
);
|
||||
|
||||
let nonzero = output.iter().filter(|&&b| b != 0).count();
|
||||
// In 255 bytes of uniform randomness, the expected number of zero bytes is ~1.
|
||||
// A weak nonzero check can miss severe entropy collapse.
|
||||
assert!(
|
||||
nonzero >= 240,
|
||||
"RNG output entropy collapsed, too many zero bytes: {} nonzero out of {}",
|
||||
nonzero,
|
||||
output.len(),
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn padding_reaches_first_bucket_boundary() {
|
||||
let output = collect_padding(1, true, 64, 4096, false, 0, false).await;
|
||||
assert_eq!(output.len(), 63);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn disabled_padding_produces_no_output() {
|
||||
let output = collect_padding(0, false, 256, 4096, false, 0, false).await;
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn at_cap_without_blur_produces_no_output() {
|
||||
let output = collect_padding(4096, true, 64, 4096, false, 0, false).await;
|
||||
assert!(output.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn above_cap_blur_is_positive_and_bounded_in_aggressive_mode() {
|
||||
let output = collect_padding(4096, true, 64, 4096, true, 128, true).await;
|
||||
assert!(!output.is_empty());
|
||||
assert!(output.len() <= 128, "blur exceeded max: {}", output.len());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stress_padding_runs_are_not_constant_pattern() {
|
||||
// Stress and sanity-check: repeated runs should not collapse to identical
|
||||
// first 16 bytes across all samples.
|
||||
let mut first_chunks = Vec::new();
|
||||
for _ in 0..64 {
|
||||
let out = collect_padding(1, true, 64, 4096, false, 0, false).await;
|
||||
first_chunks.push(out[..16].to_vec());
|
||||
}
|
||||
|
||||
let first = &first_chunks[0];
|
||||
let all_same = first_chunks.iter().all(|chunk| chunk == first);
|
||||
assert!(
|
||||
!all_same,
|
||||
"all stress samples had identical prefix, rng output appears degenerate"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user