mirror of
https://github.com/telemt/telemt.git
synced 2026-06-23 11:21:10 +03:00
183 lines
6.3 KiB
Rust
183 lines
6.3 KiB
Rust
use super::*;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
use tokio::net::TcpListener;
|
|
use tokio::time::Duration;
|
|
|
|
async fn capture_forwarded_len_with_optional_eof(
|
|
body_sent: usize,
|
|
shape_hardening: bool,
|
|
above_cap_blur: bool,
|
|
above_cap_blur_max_bytes: usize,
|
|
close_client_after_write: bool,
|
|
) -> usize {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let backend_addr = listener.local_addr().unwrap();
|
|
|
|
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 = shape_hardening;
|
|
config.censorship.mask_shape_bucket_floor_bytes = 512;
|
|
config.censorship.mask_shape_bucket_cap_bytes = 4096;
|
|
config.censorship.mask_shape_above_cap_blur = above_cap_blur;
|
|
config.censorship.mask_shape_above_cap_blur_max_bytes = above_cap_blur_max_bytes;
|
|
|
|
let accept_task = tokio::spawn(async move {
|
|
let (mut stream, _) = listener.accept().await.unwrap();
|
|
let mut got = Vec::new();
|
|
let _ = tokio::time::timeout(Duration::from_secs(2), stream.read_to_end(&mut got)).await;
|
|
got.len()
|
|
});
|
|
|
|
let (server_reader, mut client_writer) = duplex(64 * 1024);
|
|
let (_client_visible_reader, client_visible_writer) = duplex(64 * 1024);
|
|
let peer: SocketAddr = "198.51.100.241:57241".parse().unwrap();
|
|
let local: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
let mut probe = vec![0u8; 5 + body_sent];
|
|
probe[0] = 0x16;
|
|
probe[1] = 0x03;
|
|
probe[2] = 0x01;
|
|
probe[3..5].copy_from_slice(&7000u16.to_be_bytes());
|
|
probe[5..].fill(0x73);
|
|
|
|
let fallback = tokio::spawn(async move {
|
|
handle_bad_client(
|
|
server_reader,
|
|
client_visible_writer,
|
|
&probe,
|
|
peer,
|
|
local,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
});
|
|
|
|
if close_client_after_write {
|
|
client_writer.shutdown().await.unwrap();
|
|
} else {
|
|
client_writer.write_all(b"keepalive").await.unwrap();
|
|
tokio::time::sleep(Duration::from_millis(170)).await;
|
|
drop(client_writer);
|
|
}
|
|
|
|
let _ = tokio::time::timeout(Duration::from_secs(4), fallback)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
|
|
tokio::time::timeout(Duration::from_secs(4), accept_task)
|
|
.await
|
|
.unwrap()
|
|
.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "red-team detector: shaping on non-EOF timeout path is disabled by design to prevent post-timeout tail leaks"]
|
|
async fn security_shape_padding_applies_without_client_eof_when_backend_silent() {
|
|
let body_sent = 17usize;
|
|
let hardened_floor = 512usize;
|
|
|
|
let with_eof = capture_forwarded_len_with_optional_eof(body_sent, true, false, 0, true).await;
|
|
let without_eof =
|
|
capture_forwarded_len_with_optional_eof(body_sent, true, false, 0, false).await;
|
|
|
|
assert!(
|
|
with_eof >= hardened_floor,
|
|
"EOF path should be shaped to floor (with_eof={with_eof}, floor={hardened_floor})"
|
|
);
|
|
assert!(
|
|
without_eof >= hardened_floor,
|
|
"non-EOF path should also be shaped when backend is silent (without_eof={without_eof}, floor={hardened_floor})"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "red-team detector: blur currently allows zero-extra sample by design within [0..=max] bound"]
|
|
async fn security_above_cap_blur_never_emits_exact_base_length() {
|
|
let body_sent = 5000usize;
|
|
let base = 5 + body_sent;
|
|
let max_blur = 1usize;
|
|
|
|
for _ in 0..64 {
|
|
let observed =
|
|
capture_forwarded_len_with_optional_eof(body_sent, true, true, max_blur, true).await;
|
|
assert!(
|
|
observed > base,
|
|
"above-cap blur must add at least one byte when enabled (observed={observed}, base={base})"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "red-team detector: shape padding currently depends on EOF, enabling idle-timeout bypass probes"]
|
|
async fn redteam_detector_shape_padding_must_not_depend_on_client_eof() {
|
|
let body_sent = 17usize;
|
|
let hardened_floor = 512usize;
|
|
|
|
let with_eof = capture_forwarded_len_with_optional_eof(body_sent, true, false, 0, true).await;
|
|
let without_eof =
|
|
capture_forwarded_len_with_optional_eof(body_sent, true, false, 0, false).await;
|
|
|
|
assert!(
|
|
with_eof >= hardened_floor,
|
|
"sanity check failed: EOF path should be shaped to floor (with_eof={with_eof}, floor={hardened_floor})"
|
|
);
|
|
|
|
assert!(
|
|
without_eof >= hardened_floor,
|
|
"strict anti-probing model expects shaping even without EOF; observed without_eof={without_eof}, floor={hardened_floor}"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "red-team detector: zero-extra above-cap blur samples leak exact class boundary"]
|
|
async fn redteam_detector_above_cap_blur_must_never_emit_exact_base_length() {
|
|
let body_sent = 5000usize;
|
|
let base = 5 + body_sent;
|
|
let mut saw_exact_base = false;
|
|
let max_blur = 1usize;
|
|
|
|
for _ in 0..96 {
|
|
let observed =
|
|
capture_forwarded_len_with_optional_eof(body_sent, true, true, max_blur, true).await;
|
|
if observed == base {
|
|
saw_exact_base = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
assert!(
|
|
!saw_exact_base,
|
|
"strict anti-classifier model expects >0 blur always; observed exact base length leaks class"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
#[ignore = "red-team detector: disjoint above-cap ranges enable near-perfect size-class classification"]
|
|
async fn redteam_detector_above_cap_blur_ranges_for_far_classes_should_overlap() {
|
|
let mut a_min = usize::MAX;
|
|
let mut a_max = 0usize;
|
|
let mut b_min = usize::MAX;
|
|
let mut b_max = 0usize;
|
|
|
|
for _ in 0..48 {
|
|
let a = capture_forwarded_len_with_optional_eof(5000, true, true, 64, true).await;
|
|
let b = capture_forwarded_len_with_optional_eof(7000, true, true, 64, true).await;
|
|
a_min = a_min.min(a);
|
|
a_max = a_max.max(a);
|
|
b_min = b_min.min(b);
|
|
b_max = b_max.max(b);
|
|
}
|
|
|
|
let overlap = a_min <= b_max && b_min <= a_max;
|
|
assert!(
|
|
overlap,
|
|
"strict anti-classifier model expects overlapping output bands; class_a=[{a_min},{a_max}] class_b=[{b_min},{b_max}]"
|
|
);
|
|
}
|