mirror of https://github.com/telemt/telemt.git
372 lines
11 KiB
Rust
372 lines
11 KiB
Rust
use super::*;
|
|
use tokio::io::{duplex, empty, sink, AsyncReadExt, AsyncWriteExt};
|
|
use tokio::time::{sleep, timeout, Duration};
|
|
|
|
fn oracle_len(
|
|
total_sent: usize,
|
|
shape_enabled: bool,
|
|
ended_by_eof: bool,
|
|
initial_len: usize,
|
|
floor: usize,
|
|
cap: usize,
|
|
) -> usize {
|
|
if shape_enabled && ended_by_eof && initial_len > 0 {
|
|
next_mask_shape_bucket(total_sent, floor, cap)
|
|
} else {
|
|
total_sent
|
|
}
|
|
}
|
|
|
|
async fn run_relay_case(
|
|
initial: Vec<u8>,
|
|
extra: Vec<u8>,
|
|
close_client: bool,
|
|
shape_enabled: bool,
|
|
floor: usize,
|
|
cap: usize,
|
|
above_cap_blur: bool,
|
|
above_cap_blur_max_bytes: usize,
|
|
) -> Vec<u8> {
|
|
let (client_reader, mut client_writer) = duplex(8192);
|
|
let (mut mask_observer, mask_writer) = duplex(8192);
|
|
|
|
let relay = tokio::spawn(async move {
|
|
relay_to_mask(
|
|
client_reader,
|
|
sink(),
|
|
empty(),
|
|
mask_writer,
|
|
&initial,
|
|
shape_enabled,
|
|
floor,
|
|
cap,
|
|
above_cap_blur,
|
|
above_cap_blur_max_bytes,
|
|
)
|
|
.await;
|
|
});
|
|
|
|
if !extra.is_empty() {
|
|
client_writer.write_all(&extra).await.unwrap();
|
|
}
|
|
|
|
if close_client {
|
|
client_writer.shutdown().await.unwrap();
|
|
}
|
|
|
|
timeout(Duration::from_secs(2), relay).await.unwrap().unwrap();
|
|
|
|
if !close_client {
|
|
drop(client_writer);
|
|
}
|
|
|
|
let mut observed = Vec::new();
|
|
timeout(Duration::from_secs(2), mask_observer.read_to_end(&mut observed))
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
observed
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_negative_timeout_path_never_shapes_even_with_blur_enabled() {
|
|
let initial = b"GET /timeout-path HTTP/1.1\r\n".to_vec();
|
|
let extra = vec![0xCC; 700];
|
|
let total = initial.len() + extra.len();
|
|
|
|
let observed = run_relay_case(
|
|
initial.clone(),
|
|
extra.clone(),
|
|
false,
|
|
true,
|
|
512,
|
|
4096,
|
|
true,
|
|
1024,
|
|
)
|
|
.await;
|
|
|
|
assert_eq!(observed.len(), total, "timeout path must stay unshaped");
|
|
assert_eq!(&observed[..initial.len()], initial.as_slice());
|
|
assert_eq!(&observed[initial.len()..], extra.as_slice());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_positive_clean_eof_path_shapes_and_preserves_prefix() {
|
|
let initial = b"GET /ok HTTP/1.1\r\n".to_vec();
|
|
let extra = vec![0x55; 300];
|
|
let total = initial.len() + extra.len();
|
|
|
|
let observed = run_relay_case(initial.clone(), extra.clone(), true, true, 512, 4096, false, 0).await;
|
|
|
|
let expected_len = oracle_len(total, true, true, initial.len(), 512, 4096);
|
|
assert_eq!(observed.len(), expected_len, "clean EOF path must be bucket-shaped");
|
|
assert_eq!(&observed[..initial.len()], initial.as_slice());
|
|
assert_eq!(&observed[initial.len()..(initial.len() + extra.len())], extra.as_slice());
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_edge_empty_initial_remains_transparent_under_clean_eof() {
|
|
let initial = Vec::new();
|
|
let extra = vec![0xA1; 257];
|
|
|
|
let observed = run_relay_case(initial, extra.clone(), true, true, 512, 4096, false, 0).await;
|
|
|
|
assert_eq!(observed.len(), extra.len(), "empty initial_data must never trigger shaping");
|
|
assert_eq!(observed, extra);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_light_fuzz_oracle_matches_for_eof_and_timeout_variants() {
|
|
let floor = 512usize;
|
|
let cap = 4096usize;
|
|
|
|
// Deterministic xorshift to keep this fuzz test stable in CI.
|
|
let mut s: u64 = 0x9E37_79B9_7F4A_7C15;
|
|
for _ in 0..96 {
|
|
s ^= s << 7;
|
|
s ^= s >> 9;
|
|
s ^= s << 8;
|
|
let initial_len = (s as usize) % 48;
|
|
|
|
s ^= s << 7;
|
|
s ^= s >> 9;
|
|
s ^= s << 8;
|
|
let extra_len = (s as usize) % 1800;
|
|
|
|
s ^= s << 7;
|
|
s ^= s >> 9;
|
|
s ^= s << 8;
|
|
let close_client = (s & 1) == 0;
|
|
|
|
let initial = vec![0x42; initial_len];
|
|
let extra = vec![0x99; extra_len];
|
|
let total = initial_len + extra_len;
|
|
|
|
let observed = run_relay_case(
|
|
initial.clone(),
|
|
extra.clone(),
|
|
close_client,
|
|
true,
|
|
floor,
|
|
cap,
|
|
false,
|
|
0,
|
|
)
|
|
.await;
|
|
|
|
let expected = oracle_len(total, true, close_client, initial_len, floor, cap);
|
|
assert_eq!(
|
|
observed.len(),
|
|
expected,
|
|
"oracle mismatch: initial_len={initial_len} extra_len={extra_len} close_client={close_client}"
|
|
);
|
|
|
|
if initial_len > 0 {
|
|
assert_eq!(&observed[..initial_len], initial.as_slice());
|
|
}
|
|
if extra_len > 0 {
|
|
assert_eq!(
|
|
&observed[initial_len..(initial_len + extra_len)],
|
|
extra.as_slice(),
|
|
"payload prefix must remain byte-for-byte before any optional shaping tail"
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_stress_parallel_mixed_sessions_keep_oracle_and_no_hangs() {
|
|
let mut tasks = Vec::new();
|
|
|
|
for i in 0..48usize {
|
|
tasks.push(tokio::spawn(async move {
|
|
let initial_len = if i % 3 == 0 { 0 } else { 5 + (i % 19) };
|
|
let extra_len = 64 + (i * 37 % 1300);
|
|
let close_client = i % 2 == 0;
|
|
|
|
let initial = vec![i as u8; initial_len];
|
|
let extra = vec![0xE0 | ((i as u8) & 0x0F); extra_len];
|
|
let total = initial_len + extra_len;
|
|
|
|
let observed = run_relay_case(
|
|
initial.clone(),
|
|
extra.clone(),
|
|
close_client,
|
|
true,
|
|
512,
|
|
4096,
|
|
false,
|
|
0,
|
|
)
|
|
.await;
|
|
|
|
let expected = oracle_len(total, true, close_client, initial_len, 512, 4096);
|
|
assert_eq!(
|
|
observed.len(),
|
|
expected,
|
|
"stress oracle mismatch for worker={i} close_client={close_client}"
|
|
);
|
|
|
|
if initial_len > 0 {
|
|
assert_eq!(&observed[..initial_len], initial.as_slice());
|
|
}
|
|
if extra_len > 0 {
|
|
assert_eq!(&observed[initial_len..(initial_len + extra_len)], extra.as_slice());
|
|
}
|
|
}));
|
|
}
|
|
|
|
for task in tasks {
|
|
timeout(Duration::from_secs(3), task).await.unwrap().unwrap();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_integration_slow_drip_timeout_is_cut_without_tail_leak() {
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let backend_addr = listener.local_addr().unwrap();
|
|
let initial = b"GET /drip-guard HTTP/1.1\r\nHost: front.example\r\n\r\n".to_vec();
|
|
|
|
let accept_task = tokio::spawn({
|
|
let initial = initial.clone();
|
|
async move {
|
|
let (mut stream, _) = listener.accept().await.unwrap();
|
|
let mut observed = vec![0u8; initial.len()];
|
|
stream.read_exact(&mut observed).await.unwrap();
|
|
assert_eq!(observed, initial);
|
|
|
|
let mut one = [0u8; 1];
|
|
let r = timeout(Duration::from_millis(220), stream.read_exact(&mut one)).await;
|
|
assert!(r.is_err() || r.unwrap().is_err(), "no post-timeout drip/tail may reach backend");
|
|
}
|
|
});
|
|
|
|
let mut config = ProxyConfig::default();
|
|
config.general.beobachten = false;
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = backend_addr.port();
|
|
config.censorship.mask_shape_hardening = true;
|
|
config.censorship.mask_shape_bucket_floor_bytes = 512;
|
|
config.censorship.mask_shape_bucket_cap_bytes = 4096;
|
|
|
|
let peer: SocketAddr = "198.51.100.245:53101".parse().unwrap();
|
|
let local: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
|
|
let (mut client_writer, client_reader) = duplex(1024);
|
|
let (_client_visible_reader, client_visible_writer) = duplex(1024);
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
let relay = tokio::spawn(async move {
|
|
handle_bad_client(
|
|
client_reader,
|
|
client_visible_writer,
|
|
&initial,
|
|
peer,
|
|
local,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
});
|
|
|
|
sleep(Duration::from_millis(160)).await;
|
|
let _ = client_writer.write_all(b"X").await;
|
|
|
|
timeout(Duration::from_secs(2), relay).await.unwrap().unwrap();
|
|
timeout(Duration::from_secs(2), accept_task).await.unwrap().unwrap();
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_above_cap_blur_statistical_quality_and_bounds() {
|
|
let base_len = 5005usize; // 5-byte header + 5000 payload
|
|
let max_extra = 64usize;
|
|
let mut extras = Vec::new();
|
|
|
|
for _ in 0..192 {
|
|
let observed = run_relay_case(
|
|
vec![0x16, 0x03, 0x01, 0x1B, 0x58],
|
|
vec![0xAA; 5000],
|
|
true,
|
|
true,
|
|
512,
|
|
4096,
|
|
true,
|
|
max_extra,
|
|
)
|
|
.await;
|
|
|
|
assert!(
|
|
observed.len() >= base_len && observed.len() <= base_len + max_extra,
|
|
"above-cap blur length must stay in bounded window"
|
|
);
|
|
extras.push(observed.len() - base_len);
|
|
}
|
|
|
|
let unique: std::collections::BTreeSet<_> = extras.iter().copied().collect();
|
|
let mean = extras.iter().copied().sum::<usize>() as f64 / extras.len() as f64;
|
|
|
|
// For uniform [0..=64], mean is ~32. Keep wide bounds to avoid CI flakiness.
|
|
assert!(
|
|
(20.0..=44.0).contains(&mean),
|
|
"blur mean drifted too far from expected center, mean={mean:.2}"
|
|
);
|
|
assert!(
|
|
unique.len() >= 16,
|
|
"blur distribution appears too low-entropy, unique_extras={}",
|
|
unique.len()
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_above_cap_blur_parallel_stress_keeps_bounds() {
|
|
let max_extra = 96usize;
|
|
let mut tasks = Vec::new();
|
|
|
|
for i in 0..64usize {
|
|
tasks.push(tokio::spawn(async move {
|
|
let body_len = 4500 + (i % 256);
|
|
let base_len = 5 + body_len;
|
|
|
|
let observed = run_relay_case(
|
|
vec![0x16, 0x03, 0x01, 0x1B, 0x58],
|
|
vec![0xA0 | ((i as u8) & 0x0F); body_len],
|
|
true,
|
|
true,
|
|
512,
|
|
4096,
|
|
true,
|
|
max_extra,
|
|
)
|
|
.await;
|
|
|
|
assert!(
|
|
observed.len() >= base_len && observed.len() <= base_len + max_extra,
|
|
"parallel blur bounds violated for worker={i}: observed_len={} base_len={} max_extra={}",
|
|
observed.len(),
|
|
base_len,
|
|
max_extra
|
|
);
|
|
}));
|
|
}
|
|
|
|
for task in tasks {
|
|
timeout(Duration::from_secs(3), task).await.unwrap().unwrap();
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn masking_shape_guard_above_cap_blur_disabled_keeps_exact_length_even_on_clean_eof() {
|
|
let initial = vec![0x16, 0x03, 0x01, 0x1B, 0x58];
|
|
let body = vec![0x77; 5200];
|
|
let expected = initial.len() + body.len();
|
|
|
|
let observed = run_relay_case(initial, body, true, true, 512, 4096, false, 0).await;
|
|
assert_eq!(
|
|
observed.len(),
|
|
expected,
|
|
"without above-cap blur the output must remain exact even on clean EOF"
|
|
);
|
|
}
|