mirror of https://github.com/telemt/telemt.git
331 lines
10 KiB
Rust
331 lines
10 KiB
Rust
use super::*;
|
|
use std::net::SocketAddr;
|
|
use std::net::TcpListener as StdTcpListener;
|
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, duplex};
|
|
use tokio::net::TcpListener;
|
|
use tokio::time::{Duration, Instant, timeout};
|
|
|
|
fn closed_local_port() -> u16 {
|
|
let listener = StdTcpListener::bind("127.0.0.1:0").unwrap();
|
|
let port = listener.local_addr().unwrap().port();
|
|
drop(listener);
|
|
port
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_detection_matches_literal_ipv4_listener() {
|
|
let local: SocketAddr = "198.51.100.40:443".parse().unwrap();
|
|
assert!(is_mask_target_local_listener_async("198.51.100.40", 443, local, None,).await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_detection_matches_bracketed_ipv6_listener() {
|
|
let local: SocketAddr = "[2001:db8::44]:8443".parse().unwrap();
|
|
assert!(is_mask_target_local_listener_async("[2001:db8::44]", 8443, local, None,).await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_detection_keeps_same_ip_different_port_forwardable() {
|
|
let local: SocketAddr = "203.0.113.44:443".parse().unwrap();
|
|
assert!(!is_mask_target_local_listener_async("203.0.113.44", 8443, local, None,).await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_detection_normalizes_ipv4_mapped_ipv6_literal() {
|
|
let local: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
assert!(is_mask_target_local_listener_async("::ffff:127.0.0.1", 443, local, None,).await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_detection_unspecified_bind_blocks_loopback_target() {
|
|
let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
|
|
assert!(is_mask_target_local_listener_async("127.0.0.1", 443, local, None,).await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_detection_unspecified_bind_keeps_remote_target_forwardable() {
|
|
let local: SocketAddr = "0.0.0.0:443".parse().unwrap();
|
|
let remote: SocketAddr = "198.51.100.44:443".parse().unwrap();
|
|
assert!(!is_mask_target_local_listener_async("mask.example", 443, local, Some(remote),).await);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_fallback_refuses_recursive_loopback_connect() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let local_addr = listener.local_addr().unwrap();
|
|
let accept_task = tokio::spawn(async move {
|
|
timeout(Duration::from_millis(120), listener.accept())
|
|
.await
|
|
.is_ok()
|
|
});
|
|
|
|
let mut config = ProxyConfig::default();
|
|
config.general.beobachten = false;
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_unix_sock = None;
|
|
config.censorship.mask_host = Some(local_addr.ip().to_string());
|
|
config.censorship.mask_port = local_addr.port();
|
|
config.censorship.mask_proxy_protocol = 0;
|
|
|
|
let peer: SocketAddr = "203.0.113.90:55090".parse().unwrap();
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
handle_bad_client(
|
|
tokio::io::empty(),
|
|
tokio::io::sink(),
|
|
b"GET /",
|
|
peer,
|
|
local_addr,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
|
|
let accepted = accept_task.await.unwrap();
|
|
assert!(
|
|
!accepted,
|
|
"self-target masking must fail closed without connecting to local listener"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn same_ip_different_port_still_forwards_to_mask_backend() {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let backend_addr = listener.local_addr().unwrap();
|
|
let probe = b"GET /".to_vec();
|
|
let accept_task = tokio::spawn({
|
|
let expected = probe.clone();
|
|
async move {
|
|
let (mut stream, _) = listener.accept().await.unwrap();
|
|
let mut got = vec![0u8; expected.len()];
|
|
stream.read_exact(&mut got).await.unwrap();
|
|
assert_eq!(got, expected);
|
|
}
|
|
});
|
|
|
|
let mut config = ProxyConfig::default();
|
|
config.general.beobachten = false;
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_unix_sock = None;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = backend_addr.port();
|
|
config.censorship.mask_proxy_protocol = 0;
|
|
|
|
let peer: SocketAddr = "203.0.113.91:55091".parse().unwrap();
|
|
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
handle_bad_client(
|
|
tokio::io::empty(),
|
|
tokio::io::sink(),
|
|
&probe,
|
|
peer,
|
|
local_addr,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
|
|
timeout(Duration::from_secs(2), accept_task)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn detect_client_type_http_boundary_get_and_post() {
|
|
assert_eq!(detect_client_type(b"GET "), "HTTP");
|
|
assert_eq!(detect_client_type(b"GET /"), "HTTP");
|
|
|
|
assert_eq!(detect_client_type(b"POST"), "HTTP");
|
|
assert_eq!(detect_client_type(b"POST "), "HTTP");
|
|
assert_eq!(detect_client_type(b"POSTX"), "HTTP");
|
|
}
|
|
|
|
#[test]
|
|
fn detect_client_type_tls_and_length_boundaries() {
|
|
assert_eq!(detect_client_type(b"\x16\x03\x01"), "port-scanner");
|
|
assert_eq!(detect_client_type(b"\x16\x03\x01\x00"), "TLS-scanner");
|
|
|
|
assert_eq!(detect_client_type(b"123456789"), "port-scanner");
|
|
assert_eq!(detect_client_type(b"1234567890"), "unknown");
|
|
}
|
|
|
|
#[test]
|
|
fn build_mask_proxy_header_v1_cross_family_falls_back_to_unknown() {
|
|
let peer: SocketAddr = "192.168.1.5:12345".parse().unwrap();
|
|
let local: SocketAddr = "[2001:db8::1]:443".parse().unwrap();
|
|
let header = build_mask_proxy_header(1, peer, local).unwrap();
|
|
assert_eq!(header, b"PROXY UNKNOWN\r\n");
|
|
}
|
|
|
|
#[test]
|
|
fn next_mask_shape_bucket_checked_mul_overflow_fails_closed() {
|
|
let floor = usize::MAX / 2 + 1;
|
|
let cap = usize::MAX;
|
|
let total = floor + 1;
|
|
assert_eq!(next_mask_shape_bucket(total, floor, cap), total);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn self_target_reject_path_keeps_timing_budget() {
|
|
let mut config = ProxyConfig::default();
|
|
config.general.beobachten = false;
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_unix_sock = None;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = 443;
|
|
|
|
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
let peer: SocketAddr = "203.0.113.92:55092".parse().unwrap();
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
let (client, server) = duplex(1024);
|
|
drop(client);
|
|
|
|
let started = Instant::now();
|
|
handle_bad_client(
|
|
server,
|
|
tokio::io::sink(),
|
|
b"GET / HTTP/1.1\r\n",
|
|
peer,
|
|
local_addr,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
|
|
let elapsed = started.elapsed();
|
|
assert!(
|
|
elapsed >= Duration::from_millis(40) && elapsed < Duration::from_millis(250),
|
|
"self-target reject path must keep coarse timing budget without stalling"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn relay_path_idle_timeout_eviction_remains_effective() {
|
|
let (client_read, mut client_write) = duplex(1024);
|
|
let (mask_read, mask_write) = duplex(1024);
|
|
|
|
tokio::spawn(async move {
|
|
tokio::time::sleep(Duration::from_millis(10)).await;
|
|
client_write.write_all(b"a").await.unwrap();
|
|
tokio::time::sleep(Duration::from_millis(180)).await;
|
|
let _ = client_write.write_all(b"b").await;
|
|
});
|
|
|
|
let started = Instant::now();
|
|
relay_to_mask(
|
|
client_read,
|
|
tokio::io::sink(),
|
|
mask_read,
|
|
mask_write,
|
|
b"init",
|
|
false,
|
|
0,
|
|
0,
|
|
false,
|
|
0,
|
|
false,
|
|
5 * 1024 * 1024,
|
|
)
|
|
.await;
|
|
|
|
let elapsed = started.elapsed();
|
|
assert!(
|
|
elapsed >= Duration::from_millis(90) && elapsed < Duration::from_millis(180),
|
|
"idle-timeout eviction must occur before late trickle write"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn offline_mask_target_refusal_respects_timing_normalization_budget() {
|
|
let mut config = ProxyConfig::default();
|
|
config.general.beobachten = false;
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_unix_sock = None;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = closed_local_port();
|
|
config.censorship.mask_timing_normalization_enabled = true;
|
|
config.censorship.mask_timing_normalization_floor_ms = 120;
|
|
config.censorship.mask_timing_normalization_ceiling_ms = 120;
|
|
|
|
let peer: SocketAddr = "203.0.113.93:55093".parse().unwrap();
|
|
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
let (mut client, server) = duplex(1024);
|
|
let started = Instant::now();
|
|
let task = tokio::spawn(async move {
|
|
handle_bad_client(
|
|
server,
|
|
tokio::io::sink(),
|
|
b"GET / HTTP/1.1\r\n\r\n",
|
|
peer,
|
|
local_addr,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
});
|
|
|
|
client.shutdown().await.unwrap();
|
|
timeout(Duration::from_secs(2), task)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
let elapsed = started.elapsed();
|
|
|
|
assert!(
|
|
elapsed >= Duration::from_millis(110) && elapsed < Duration::from_millis(220),
|
|
"offline-refusal path must honor normalization budget without unbounded drift"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn offline_mask_target_refusal_with_idle_client_is_bounded_by_consume_timeout() {
|
|
let mut config = ProxyConfig::default();
|
|
config.general.beobachten = false;
|
|
config.censorship.mask = true;
|
|
config.censorship.mask_unix_sock = None;
|
|
config.censorship.mask_host = Some("127.0.0.1".to_string());
|
|
config.censorship.mask_port = closed_local_port();
|
|
config.censorship.mask_timing_normalization_enabled = false;
|
|
|
|
let peer: SocketAddr = "203.0.113.94:55094".parse().unwrap();
|
|
let local_addr: SocketAddr = "127.0.0.1:443".parse().unwrap();
|
|
let beobachten = BeobachtenStore::new();
|
|
|
|
let (mut client, server) = duplex(1024);
|
|
let started = Instant::now();
|
|
let task = tokio::spawn(async move {
|
|
handle_bad_client(
|
|
server,
|
|
tokio::io::sink(),
|
|
b"GET / HTTP/1.1\r\n\r\n",
|
|
peer,
|
|
local_addr,
|
|
&config,
|
|
&beobachten,
|
|
)
|
|
.await;
|
|
});
|
|
|
|
tokio::time::sleep(Duration::from_millis(120)).await;
|
|
client
|
|
.write_all(b"still-open-before-timeout")
|
|
.await
|
|
.expect("connection should still be open before consume timeout expires");
|
|
|
|
timeout(Duration::from_secs(2), task)
|
|
.await
|
|
.unwrap()
|
|
.unwrap();
|
|
let elapsed = started.elapsed();
|
|
|
|
assert!(
|
|
elapsed >= Duration::from_millis(190) && elapsed < Duration::from_millis(350),
|
|
"offline-refusal path must not retain idle client indefinitely"
|
|
);
|
|
}
|