mirror of https://github.com/telemt/telemt.git
183 lines
5.9 KiB
Rust
183 lines
5.9 KiB
Rust
use super::*;
|
|
use std::net::{IpAddr, Ipv4Addr};
|
|
use std::time::{Duration, Instant};
|
|
|
|
#[test]
|
|
fn positive_preauth_throttle_activates_after_failure_threshold() {
|
|
let shared = ProxySharedState::new();
|
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
|
|
let ip = IpAddr::V4(Ipv4Addr::new(198, 51, 100, 20));
|
|
let now = Instant::now();
|
|
|
|
for _ in 0..AUTH_PROBE_BACKOFF_START_FAILS {
|
|
auth_probe_record_failure_in(shared.as_ref(), ip, now);
|
|
}
|
|
|
|
assert!(
|
|
auth_probe_is_throttled_in(shared.as_ref(), ip, now),
|
|
"peer must be throttled once fail streak reaches threshold"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn negative_unrelated_peer_remains_unthrottled() {
|
|
let shared = ProxySharedState::new();
|
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
|
|
let attacker = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 12));
|
|
let benign = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 13));
|
|
let now = Instant::now();
|
|
|
|
for _ in 0..AUTH_PROBE_BACKOFF_START_FAILS {
|
|
auth_probe_record_failure_in(shared.as_ref(), attacker, now);
|
|
}
|
|
|
|
assert!(auth_probe_is_throttled_in(shared.as_ref(), attacker, now));
|
|
assert!(
|
|
!auth_probe_is_throttled_in(shared.as_ref(), benign, now),
|
|
"throttle state must stay scoped to normalized peer key"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn edge_expired_entry_is_pruned_and_no_longer_throttled() {
|
|
let shared = ProxySharedState::new();
|
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
|
|
let ip = IpAddr::V4(Ipv4Addr::new(192, 0, 2, 41));
|
|
let base = Instant::now();
|
|
for _ in 0..AUTH_PROBE_BACKOFF_START_FAILS {
|
|
auth_probe_record_failure_in(shared.as_ref(), ip, base);
|
|
}
|
|
|
|
let expired_at = base + Duration::from_secs(AUTH_PROBE_TRACK_RETENTION_SECS + 1);
|
|
assert!(
|
|
!auth_probe_is_throttled_in(shared.as_ref(), ip, expired_at),
|
|
"expired entries must not keep throttling peers"
|
|
);
|
|
|
|
let state = auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
assert!(
|
|
state.get(&normalize_auth_probe_ip(ip)).is_none(),
|
|
"expired lookup should prune stale state"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn adversarial_saturation_grace_requires_extra_failures_before_preauth_throttle() {
|
|
let shared = ProxySharedState::new();
|
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
|
|
let ip = IpAddr::V4(Ipv4Addr::new(198, 18, 0, 7));
|
|
let now = Instant::now();
|
|
|
|
for _ in 0..AUTH_PROBE_BACKOFF_START_FAILS {
|
|
auth_probe_record_failure_in(shared.as_ref(), ip, now);
|
|
}
|
|
auth_probe_note_saturation_in(shared.as_ref(), now);
|
|
|
|
assert!(
|
|
!auth_probe_should_apply_preauth_throttle_in(shared.as_ref(), ip, now),
|
|
"during global saturation, peer must receive configured grace window"
|
|
);
|
|
|
|
for _ in 0..AUTH_PROBE_SATURATION_GRACE_FAILS {
|
|
auth_probe_record_failure_in(shared.as_ref(), ip, now + Duration::from_millis(1));
|
|
}
|
|
|
|
assert!(
|
|
auth_probe_should_apply_preauth_throttle_in(shared.as_ref(), ip, now + Duration::from_millis(1)),
|
|
"after grace failures are exhausted, preauth throttle must activate"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn integration_over_cap_insertion_keeps_probe_map_bounded() {
|
|
let shared = ProxySharedState::new();
|
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
|
|
let now = Instant::now();
|
|
for idx in 0..(AUTH_PROBE_TRACK_MAX_ENTRIES + 1024) {
|
|
let ip = IpAddr::V4(Ipv4Addr::new(
|
|
10,
|
|
((idx / 65_536) % 256) as u8,
|
|
((idx / 256) % 256) as u8,
|
|
(idx % 256) as u8,
|
|
));
|
|
auth_probe_record_failure_in(shared.as_ref(), ip, now);
|
|
}
|
|
|
|
let tracked = auth_probe_state_for_testing_in_shared(shared.as_ref()).len();
|
|
assert!(
|
|
tracked <= AUTH_PROBE_TRACK_MAX_ENTRIES,
|
|
"probe map must remain hard bounded under insertion storm"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn light_fuzz_randomized_failures_preserve_cap_and_nonzero_streaks() {
|
|
let shared = ProxySharedState::new();
|
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
|
|
let mut seed = 0x4D53_5854_6F66_6175u64;
|
|
let now = Instant::now();
|
|
|
|
for _ in 0..8192 {
|
|
seed ^= seed << 7;
|
|
seed ^= seed >> 9;
|
|
seed ^= seed << 8;
|
|
|
|
let ip = IpAddr::V4(Ipv4Addr::new(
|
|
(seed >> 24) as u8,
|
|
(seed >> 16) as u8,
|
|
(seed >> 8) as u8,
|
|
seed as u8,
|
|
));
|
|
auth_probe_record_failure_in(shared.as_ref(), ip, now + Duration::from_millis((seed & 0x3f) as u64));
|
|
}
|
|
|
|
let state = auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
assert!(state.len() <= AUTH_PROBE_TRACK_MAX_ENTRIES);
|
|
for entry in state.iter() {
|
|
assert!(entry.value().fail_streak > 0);
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
|
|
async fn stress_parallel_failure_flood_keeps_state_hard_capped() {
|
|
let shared = ProxySharedState::new();
|
|
clear_auth_probe_state_for_testing_in_shared(shared.as_ref());
|
|
|
|
let start = Instant::now();
|
|
let mut tasks = Vec::new();
|
|
|
|
for worker in 0..8u8 {
|
|
let shared = shared.clone();
|
|
tasks.push(tokio::spawn(async move {
|
|
for i in 0..4096u32 {
|
|
let ip = IpAddr::V4(Ipv4Addr::new(
|
|
172,
|
|
worker,
|
|
((i >> 8) & 0xff) as u8,
|
|
(i & 0xff) as u8,
|
|
));
|
|
auth_probe_record_failure_in(shared.as_ref(), ip, start + Duration::from_millis((i % 4) as u64));
|
|
}
|
|
}));
|
|
}
|
|
|
|
for task in tasks {
|
|
task.await.expect("stress worker must not panic");
|
|
}
|
|
|
|
let tracked = auth_probe_state_for_testing_in_shared(shared.as_ref()).len();
|
|
assert!(
|
|
tracked <= AUTH_PROBE_TRACK_MAX_ENTRIES,
|
|
"parallel failure flood must not exceed cap"
|
|
);
|
|
|
|
let probe = IpAddr::V4(Ipv4Addr::new(172, 3, 4, 5));
|
|
let _ = auth_probe_is_throttled_in(shared.as_ref(), probe, start + Duration::from_millis(2));
|
|
}
|