mirror of https://github.com/telemt/telemt.git
214 lines
7.7 KiB
Rust
214 lines
7.7 KiB
Rust
use super::*;
|
|
use std::sync::Arc;
|
|
use tokio::io::duplex;
|
|
use tokio::net::TcpListener;
|
|
use tokio::time::{Instant, Duration};
|
|
use crate::config::ProxyConfig;
|
|
use crate::stats::beobachten::BeobachtenStore;
|
|
|
|
// ------------------------------------------------------------------
|
|
// Probing Indistinguishability (OWASP ASVS 5.1.7)
|
|
// ------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn masking_probes_indistinguishable_timing() {
|
|
let mut config = ProxyConfig::default();
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = 80; // Should timeout/refuse
|
|
|
|
let peer: SocketAddr = "192.0.2.10:443".parse().unwrap();
|
|
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
// Test different probe types
|
|
let probes = vec![
|
|
(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n".to_vec(), "HTTP"),
|
|
(b"SSH-2.0-probe".to_vec(), "SSH"),
|
|
(vec![0x16, 0x03, 0x03, 0x00, 0x05, 0x01, 0x00, 0x00, 0x01, 0x00], "TLS-scanner"),
|
|
(vec![0x42; 5], "port-scanner"),
|
|
];
|
|
|
|
for (probe, type_name) in probes {
|
|
let (client_reader, _client_writer) = duplex(256);
|
|
let (_client_visible_reader, client_visible_writer) = duplex(256);
|
|
|
|
let start = Instant::now();
|
|
handle_bad_client(
|
|
client_reader,
|
|
client_visible_writer,
|
|
&probe,
|
|
peer,
|
|
local_addr,
|
|
&config,
|
|
&beobachten,
|
|
).await;
|
|
|
|
let elapsed = start.elapsed();
|
|
|
|
// We expect any outcome to take roughly MASK_TIMEOUT (50ms in tests)
|
|
// to mask whether the backend was reachable or refused.
|
|
assert!(elapsed >= Duration::from_millis(30), "Probe {type_name} finished too fast: {elapsed:?}");
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Masking Budget Stress Tests (OWASP ASVS 5.1.6)
|
|
// ------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn masking_budget_stress_under_load() {
|
|
let mut config = ProxyConfig::default();
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = 1; // Unlikely port
|
|
|
|
let peer: SocketAddr = "192.0.2.20:443".parse().unwrap();
|
|
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
let beobachten = Arc::new(BeobachtenStore::new());
|
|
|
|
let mut tasks = Vec::new();
|
|
for _ in 0..50 {
|
|
let (client_reader, _client_writer) = duplex(256);
|
|
let (_client_visible_reader, client_visible_writer) = duplex(256);
|
|
let config = config.clone();
|
|
let beobachten = Arc::clone(&beobachten);
|
|
|
|
tasks.push(tokio::spawn(async move {
|
|
let start = Instant::now();
|
|
handle_bad_client(
|
|
client_reader,
|
|
client_visible_writer,
|
|
b"probe",
|
|
peer,
|
|
local_addr,
|
|
&config,
|
|
&beobachten,
|
|
).await;
|
|
start.elapsed()
|
|
}));
|
|
}
|
|
|
|
for task in tasks {
|
|
let elapsed = task.await.unwrap();
|
|
assert!(elapsed >= Duration::from_millis(30), "Stress probe finished too fast: {elapsed:?}");
|
|
}
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// detect_client_type Fingerprint Check
|
|
// ------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_detect_client_type_boundary_cases() {
|
|
// 9 bytes = port-scanner
|
|
assert_eq!(detect_client_type(&[0x42; 9]), "port-scanner");
|
|
// 10 bytes = unknown
|
|
assert_eq!(detect_client_type(&[0x42; 10]), "unknown");
|
|
|
|
// HTTP verbs without trailing space
|
|
assert_eq!(detect_client_type(b"GET/"), "port-scanner"); // because len < 10
|
|
assert_eq!(detect_client_type(b"GET /path"), "HTTP");
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Priority 2: Slowloris and Slow Read Attacks (OWASP ASVS 5.1.5)
|
|
// ------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn masking_slowloris_client_idle_timeout_rejected() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let backend_addr = listener.local_addr().unwrap();
|
|
let initial = b"GET / 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 drip = [0u8; 1];
|
|
let drip_read = tokio::time::timeout(Duration::from_millis(220), stream.read_exact(&mut drip)).await;
|
|
assert!(
|
|
drip_read.is_err() || drip_read.unwrap().is_err(),
|
|
"backend must not receive post-timeout slowloris drip bytes"
|
|
);
|
|
}
|
|
});
|
|
|
|
let mut config = ProxyConfig::default();
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = backend_addr.port();
|
|
|
|
let beobachten = BeobachtenStore::new();
|
|
let peer: SocketAddr = "192.0.2.10:12345".parse().unwrap();
|
|
let local: SocketAddr = "192.0.2.1:443".parse().unwrap();
|
|
|
|
let (mut client_writer, client_reader) = duplex(1024);
|
|
let (_client_visible_reader, client_visible_writer) = duplex(1024);
|
|
|
|
let handle = tokio::spawn(async move {
|
|
handle_bad_client(
|
|
client_reader,
|
|
client_visible_writer,
|
|
&initial,
|
|
peer,
|
|
local,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
});
|
|
|
|
tokio::time::sleep(Duration::from_millis(160)).await;
|
|
let _ = client_writer.write_all(b"X").await;
|
|
|
|
handle.await.unwrap();
|
|
accept_task.await.unwrap();
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Priority 2: Fallback Server Down / Fingerprinting (OWASP ASVS 5.1.7)
|
|
// ------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn masking_fallback_down_mimics_timeout() {
|
|
let mut config = ProxyConfig::default();
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = 1; // Unlikely port
|
|
|
|
let (server_reader, server_writer) = duplex(1024);
|
|
let beobachten = BeobachtenStore::new();
|
|
let peer: SocketAddr = "192.0.2.12:12345".parse().unwrap();
|
|
let local: SocketAddr = "192.0.2.1:443".parse().unwrap();
|
|
|
|
let start = Instant::now();
|
|
handle_bad_client(server_reader, server_writer, b"GET / HTTP/1.1\r\n", peer, local, &config, &beobachten).await;
|
|
|
|
let elapsed = start.elapsed();
|
|
// It should wait for MASK_TIMEOUT (50ms in tests) even if connection was refused immediately
|
|
assert!(elapsed >= Duration::from_millis(40), "Must respect connect budget even on failure: {:?}", elapsed);
|
|
}
|
|
|
|
// ------------------------------------------------------------------
|
|
// Priority 2: SSRF Prevention (OWASP ASVS 5.1.2)
|
|
// ------------------------------------------------------------------
|
|
|
|
#[tokio::test]
|
|
async fn masking_ssrf_resolve_internal_ranges_blocked() {
|
|
use crate::network::dns_overrides::resolve_socket_addr;
|
|
|
|
let blocked_ips = ["127.0.0.1", "169.254.169.254", "10.0.0.1", "192.168.1.1", "0.0.0.0"];
|
|
|
|
for ip in blocked_ips {
|
|
assert!(
|
|
resolve_socket_addr(ip, 80).is_none(),
|
|
"runtime DNS overrides must not resolve unconfigured literal host targets"
|
|
);
|
|
}
|
|
}
|