mirror of
https://github.com/telemt/telemt.git
synced 2026-04-18 02:54:10 +03:00
Add comprehensive security tests for masking and shape hardening features
- Introduced red-team expected-fail tests for client masking shape hardening. - Added integration tests for masking AB envelope blur to improve obfuscation. - Implemented masking security tests to validate the behavior of masking under various conditions. - Created tests for masking shape above-cap blur to ensure proper functionality. - Developed adversarial tests for masking shape hardening to evaluate robustness against attacks. - Added timing normalization security tests to assess the effectiveness of timing obfuscation. - Implemented red-team expected-fail tests for timing side-channel vulnerabilities.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
use std::str;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
use rand::{Rng, RngCore};
|
||||
use tokio::net::TcpStream;
|
||||
#[cfg(unix)]
|
||||
use tokio::net::UnixStream;
|
||||
@@ -73,7 +74,7 @@ fn next_mask_shape_bucket(total: usize, floor: usize, cap: usize) -> usize {
|
||||
None => return total,
|
||||
}
|
||||
if bucket > cap {
|
||||
return total;
|
||||
return cap;
|
||||
}
|
||||
}
|
||||
bucket
|
||||
@@ -85,6 +86,8 @@ async fn maybe_write_shape_padding<W>(
|
||||
enabled: bool,
|
||||
floor: usize,
|
||||
cap: usize,
|
||||
above_cap_blur: bool,
|
||||
above_cap_blur_max_bytes: usize,
|
||||
)
|
||||
where
|
||||
W: AsyncWrite + Unpin,
|
||||
@@ -93,15 +96,47 @@ where
|
||||
return;
|
||||
}
|
||||
|
||||
let bucket = next_mask_shape_bucket(total_sent, floor, cap);
|
||||
if bucket <= total_sent {
|
||||
let target_total = if total_sent >= cap && above_cap_blur && above_cap_blur_max_bytes > 0 {
|
||||
let mut rng = rand::rng();
|
||||
let extra = rng.random_range(0..=above_cap_blur_max_bytes);
|
||||
total_sent.saturating_add(extra)
|
||||
} else {
|
||||
next_mask_shape_bucket(total_sent, floor, cap)
|
||||
};
|
||||
|
||||
if target_total <= total_sent {
|
||||
return;
|
||||
}
|
||||
|
||||
let pad_len = bucket - total_sent;
|
||||
let pad = vec![0u8; pad_len];
|
||||
let _ = timeout(MASK_TIMEOUT, mask_write.write_all(&pad)).await;
|
||||
let _ = timeout(MASK_TIMEOUT, mask_write.flush()).await;
|
||||
let mut remaining = target_total - total_sent;
|
||||
let mut pad_chunk = [0u8; 1024];
|
||||
let deadline = Instant::now() + MASK_TIMEOUT;
|
||||
|
||||
while remaining > 0 {
|
||||
let now = Instant::now();
|
||||
if now >= deadline {
|
||||
return;
|
||||
}
|
||||
|
||||
let write_len = remaining.min(pad_chunk.len());
|
||||
{
|
||||
let mut rng = rand::rng();
|
||||
rng.fill_bytes(&mut pad_chunk[..write_len]);
|
||||
}
|
||||
let write_budget = deadline.saturating_duration_since(now);
|
||||
match timeout(write_budget, mask_write.write_all(&pad_chunk[..write_len])).await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(_)) | Err(_) => return,
|
||||
}
|
||||
remaining -= write_len;
|
||||
}
|
||||
|
||||
let now = Instant::now();
|
||||
if now >= deadline {
|
||||
return;
|
||||
}
|
||||
let flush_budget = deadline.saturating_duration_since(now);
|
||||
let _ = timeout(flush_budget, mask_write.flush()).await;
|
||||
}
|
||||
|
||||
async fn write_proxy_header_with_timeout<W>(mask_write: &mut W, header: &[u8]) -> bool
|
||||
@@ -134,10 +169,33 @@ async fn wait_mask_connect_budget(started: Instant) {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_mask_outcome_budget(started: Instant) {
|
||||
fn mask_outcome_target_budget(config: &ProxyConfig) -> Duration {
|
||||
if config.censorship.mask_timing_normalization_enabled {
|
||||
let floor = config.censorship.mask_timing_normalization_floor_ms;
|
||||
let ceiling = config.censorship.mask_timing_normalization_ceiling_ms;
|
||||
if ceiling > floor {
|
||||
let mut rng = rand::rng();
|
||||
return Duration::from_millis(rng.random_range(floor..=ceiling));
|
||||
}
|
||||
return Duration::from_millis(floor);
|
||||
}
|
||||
|
||||
MASK_TIMEOUT
|
||||
}
|
||||
|
||||
async fn wait_mask_connect_budget_if_needed(started: Instant, config: &ProxyConfig) {
|
||||
if config.censorship.mask_timing_normalization_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
wait_mask_connect_budget(started).await;
|
||||
}
|
||||
|
||||
async fn wait_mask_outcome_budget(started: Instant, config: &ProxyConfig) {
|
||||
let target = mask_outcome_target_budget(config);
|
||||
let elapsed = started.elapsed();
|
||||
if elapsed < MASK_TIMEOUT {
|
||||
tokio::time::sleep(MASK_TIMEOUT - elapsed).await;
|
||||
if elapsed < target {
|
||||
tokio::time::sleep(target - elapsed).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +305,7 @@ where
|
||||
build_mask_proxy_header(config.censorship.mask_proxy_protocol, peer, local_addr);
|
||||
if let Some(header) = proxy_header {
|
||||
if !write_proxy_header_with_timeout(&mut mask_write, &header).await {
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -262,6 +320,8 @@ where
|
||||
config.censorship.mask_shape_hardening,
|
||||
config.censorship.mask_shape_bucket_floor_bytes,
|
||||
config.censorship.mask_shape_bucket_cap_bytes,
|
||||
config.censorship.mask_shape_above_cap_blur,
|
||||
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -269,18 +329,18 @@ where
|
||||
{
|
||||
debug!("Mask relay timed out (unix socket)");
|
||||
}
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
wait_mask_connect_budget(connect_started).await;
|
||||
wait_mask_connect_budget_if_needed(connect_started, config).await;
|
||||
debug!(error = %e, "Failed to connect to mask unix socket");
|
||||
consume_client_data_with_timeout(reader).await;
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("Timeout connecting to mask unix socket");
|
||||
consume_client_data_with_timeout(reader).await;
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
}
|
||||
}
|
||||
return;
|
||||
@@ -313,7 +373,7 @@ where
|
||||
let (mask_read, mut mask_write) = stream.into_split();
|
||||
if let Some(header) = proxy_header {
|
||||
if !write_proxy_header_with_timeout(&mut mask_write, &header).await {
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -328,6 +388,8 @@ where
|
||||
config.censorship.mask_shape_hardening,
|
||||
config.censorship.mask_shape_bucket_floor_bytes,
|
||||
config.censorship.mask_shape_bucket_cap_bytes,
|
||||
config.censorship.mask_shape_above_cap_blur,
|
||||
config.censorship.mask_shape_above_cap_blur_max_bytes,
|
||||
),
|
||||
)
|
||||
.await
|
||||
@@ -335,18 +397,18 @@ where
|
||||
{
|
||||
debug!("Mask relay timed out");
|
||||
}
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
wait_mask_connect_budget(connect_started).await;
|
||||
wait_mask_connect_budget_if_needed(connect_started, config).await;
|
||||
debug!(error = %e, "Failed to connect to mask host");
|
||||
consume_client_data_with_timeout(reader).await;
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("Timeout connecting to mask host");
|
||||
consume_client_data_with_timeout(reader).await;
|
||||
wait_mask_outcome_budget(outcome_started).await;
|
||||
wait_mask_outcome_budget(outcome_started, config).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -361,6 +423,8 @@ async fn relay_to_mask<R, W, MR, MW>(
|
||||
shape_hardening_enabled: bool,
|
||||
shape_bucket_floor_bytes: usize,
|
||||
shape_bucket_cap_bytes: usize,
|
||||
shape_above_cap_blur: bool,
|
||||
shape_above_cap_blur_max_bytes: usize,
|
||||
)
|
||||
where
|
||||
R: AsyncRead + Unpin + Send + 'static,
|
||||
@@ -386,6 +450,8 @@ where
|
||||
shape_hardening_enabled,
|
||||
shape_bucket_floor_bytes,
|
||||
shape_bucket_cap_bytes,
|
||||
shape_above_cap_blur,
|
||||
shape_above_cap_blur_max_bytes,
|
||||
)
|
||||
.await;
|
||||
let _ = mask_write.shutdown().await;
|
||||
@@ -414,3 +480,23 @@ mod security_tests;
|
||||
#[cfg(test)]
|
||||
#[path = "tests/masking_adversarial_tests.rs"]
|
||||
mod adversarial_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/masking_shape_hardening_adversarial_tests.rs"]
|
||||
mod masking_shape_hardening_adversarial_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/masking_shape_above_cap_blur_security_tests.rs"]
|
||||
mod masking_shape_above_cap_blur_security_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/masking_timing_normalization_security_tests.rs"]
|
||||
mod masking_timing_normalization_security_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/masking_ab_envelope_blur_integration_security_tests.rs"]
|
||||
mod masking_ab_envelope_blur_integration_security_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests/masking_timing_sidechannel_redteam_expected_fail_tests.rs"]
|
||||
mod masking_timing_sidechannel_redteam_expected_fail_tests;
|
||||
|
||||
Reference in New Issue
Block a user