mirror of https://github.com/telemt/telemt.git
293 lines
8.2 KiB
Rust
293 lines
8.2 KiB
Rust
use super::*;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
fn write_temp_config(contents: &str) -> PathBuf {
|
|
let nonce = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.expect("system time must be after unix epoch")
|
|
.as_nanos();
|
|
let path = std::env::temp_dir().join(format!("telemt-load-mask-shape-security-{nonce}.toml"));
|
|
fs::write(&path, contents).expect("temp config write must succeed");
|
|
path
|
|
}
|
|
|
|
fn remove_temp_config(path: &PathBuf) {
|
|
let _ = fs::remove_file(path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_zero_mask_shape_bucket_floor_bytes() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_bucket_floor_bytes = 0
|
|
mask_shape_bucket_cap_bytes = 4096
|
|
"#,
|
|
);
|
|
|
|
let err =
|
|
ProxyConfig::load(&path).expect_err("zero mask_shape_bucket_floor_bytes must be rejected");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("censorship.mask_shape_bucket_floor_bytes must be > 0"),
|
|
"error must explain floor>0 invariant, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_mask_shape_bucket_cap_less_than_floor() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_bucket_floor_bytes = 1024
|
|
mask_shape_bucket_cap_bytes = 512
|
|
"#,
|
|
);
|
|
|
|
let err =
|
|
ProxyConfig::load(&path).expect_err("mask_shape_bucket_cap_bytes < floor must be rejected");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains(
|
|
"censorship.mask_shape_bucket_cap_bytes must be >= censorship.mask_shape_bucket_floor_bytes"
|
|
),
|
|
"error must explain cap>=floor invariant, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_accepts_mask_shape_bucket_cap_equal_to_floor() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_hardening = true
|
|
mask_shape_bucket_floor_bytes = 1024
|
|
mask_shape_bucket_cap_bytes = 1024
|
|
"#,
|
|
);
|
|
|
|
let cfg = ProxyConfig::load(&path).expect("equal cap and floor must be accepted");
|
|
assert!(cfg.censorship.mask_shape_hardening);
|
|
assert_eq!(cfg.censorship.mask_shape_bucket_floor_bytes, 1024);
|
|
assert_eq!(cfg.censorship.mask_shape_bucket_cap_bytes, 1024);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_above_cap_blur_when_shape_hardening_disabled() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_hardening = false
|
|
mask_shape_above_cap_blur = true
|
|
mask_shape_above_cap_blur_max_bytes = 64
|
|
"#,
|
|
);
|
|
|
|
let err =
|
|
ProxyConfig::load(&path).expect_err("above-cap blur must require shape hardening enabled");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains(
|
|
"censorship.mask_shape_above_cap_blur requires censorship.mask_shape_hardening = true"
|
|
),
|
|
"error must explain blur prerequisite, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_above_cap_blur_with_zero_max_bytes() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_hardening = true
|
|
mask_shape_above_cap_blur = true
|
|
mask_shape_above_cap_blur_max_bytes = 0
|
|
"#,
|
|
);
|
|
|
|
let err =
|
|
ProxyConfig::load(&path).expect_err("above-cap blur max bytes must be > 0 when enabled");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("censorship.mask_shape_above_cap_blur_max_bytes must be > 0 when censorship.mask_shape_above_cap_blur is enabled"),
|
|
"error must explain blur max bytes invariant, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_timing_normalization_floor_zero_when_enabled() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_timing_normalization_enabled = true
|
|
mask_timing_normalization_floor_ms = 0
|
|
mask_timing_normalization_ceiling_ms = 200
|
|
"#,
|
|
);
|
|
|
|
let err =
|
|
ProxyConfig::load(&path).expect_err("timing normalization floor must be > 0 when enabled");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("censorship.mask_timing_normalization_floor_ms must be > 0 when censorship.mask_timing_normalization_enabled is true"),
|
|
"error must explain timing floor invariant, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_timing_normalization_ceiling_below_floor() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_timing_normalization_enabled = true
|
|
mask_timing_normalization_floor_ms = 220
|
|
mask_timing_normalization_ceiling_ms = 200
|
|
"#,
|
|
);
|
|
|
|
let err = ProxyConfig::load(&path).expect_err("timing normalization ceiling must be >= floor");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("censorship.mask_timing_normalization_ceiling_ms must be >= censorship.mask_timing_normalization_floor_ms"),
|
|
"error must explain timing ceiling/floor invariant, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_accepts_valid_timing_normalization_and_above_cap_blur_config() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_hardening = true
|
|
mask_shape_above_cap_blur = true
|
|
mask_shape_above_cap_blur_max_bytes = 128
|
|
mask_timing_normalization_enabled = true
|
|
mask_timing_normalization_floor_ms = 150
|
|
mask_timing_normalization_ceiling_ms = 240
|
|
"#,
|
|
);
|
|
|
|
let cfg = ProxyConfig::load(&path)
|
|
.expect("valid blur and timing normalization settings must be accepted");
|
|
assert!(cfg.censorship.mask_shape_hardening);
|
|
assert!(cfg.censorship.mask_shape_above_cap_blur);
|
|
assert_eq!(cfg.censorship.mask_shape_above_cap_blur_max_bytes, 128);
|
|
assert!(cfg.censorship.mask_timing_normalization_enabled);
|
|
assert_eq!(cfg.censorship.mask_timing_normalization_floor_ms, 150);
|
|
assert_eq!(cfg.censorship.mask_timing_normalization_ceiling_ms, 240);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_aggressive_shape_mode_when_shape_hardening_disabled() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_hardening = false
|
|
mask_shape_hardening_aggressive_mode = true
|
|
"#,
|
|
);
|
|
|
|
let err = ProxyConfig::load(&path)
|
|
.expect_err("aggressive shape hardening mode must require shape hardening enabled");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("censorship.mask_shape_hardening_aggressive_mode requires censorship.mask_shape_hardening = true"),
|
|
"error must explain aggressive-mode prerequisite, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_accepts_aggressive_shape_mode_when_shape_hardening_enabled() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_shape_hardening = true
|
|
mask_shape_hardening_aggressive_mode = true
|
|
mask_shape_above_cap_blur = true
|
|
mask_shape_above_cap_blur_max_bytes = 8
|
|
"#,
|
|
);
|
|
|
|
let cfg = ProxyConfig::load(&path)
|
|
.expect("aggressive shape hardening mode should be accepted when prerequisites are met");
|
|
assert!(cfg.censorship.mask_shape_hardening);
|
|
assert!(cfg.censorship.mask_shape_hardening_aggressive_mode);
|
|
assert!(cfg.censorship.mask_shape_above_cap_blur);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_zero_mask_relay_max_bytes() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_relay_max_bytes = 0
|
|
"#,
|
|
);
|
|
|
|
let err = ProxyConfig::load(&path).expect_err("mask_relay_max_bytes must be > 0");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("censorship.mask_relay_max_bytes must be > 0"),
|
|
"error must explain non-zero relay cap invariant, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_mask_relay_max_bytes_above_upper_bound() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_relay_max_bytes = 67108865
|
|
"#,
|
|
);
|
|
|
|
let err =
|
|
ProxyConfig::load(&path).expect_err("mask_relay_max_bytes above hard cap must be rejected");
|
|
let msg = err.to_string();
|
|
assert!(
|
|
msg.contains("censorship.mask_relay_max_bytes must be <= 67108864"),
|
|
"error must explain relay cap upper bound invariant, got: {msg}"
|
|
);
|
|
|
|
remove_temp_config(&path);
|
|
}
|
|
|
|
#[test]
|
|
fn load_accepts_valid_mask_relay_max_bytes() {
|
|
let path = write_temp_config(
|
|
r#"
|
|
[censorship]
|
|
mask_relay_max_bytes = 8388608
|
|
"#,
|
|
);
|
|
|
|
let cfg = ProxyConfig::load(&path).expect("valid mask_relay_max_bytes must be accepted");
|
|
assert_eq!(cfg.censorship.mask_relay_max_bytes, 8_388_608);
|
|
|
|
remove_temp_config(&path);
|
|
}
|