Refactor auth probe eviction logic and improve performance

- Simplified eviction candidate selection in `auth_probe_record_failure_with_state` by tracking the oldest candidate directly.
- Enhanced the handling of stale entries to ensure newcomers are tracked even under capacity constraints.
- Added tests to verify behavior under stress conditions and ensure newcomers are correctly managed.
- Updated `decode_user_secrets` to prioritize preferred users based on SNI hints.
- Introduced new tests for TLS SNI handling and replay protection mechanisms.
- Improved deduplication hash stability and collision resistance in middle relay logic.
- Refined cutover handling in route mode to ensure consistent error messaging and session management.
This commit is contained in:
David Osipov
2026-03-18 00:38:59 +04:00
parent a7cffb547e
commit c2443e6f1a
14 changed files with 2376 additions and 50 deletions

View File

@@ -27,6 +27,10 @@ pub const TLS_DIGEST_POS: usize = 11;
pub const TLS_DIGEST_HALF_LEN: usize = 16;
/// Time skew limits for anti-replay (in seconds)
///
/// The default window is intentionally narrow to reduce replay acceptance.
/// Operators with known clock-drifted clients should tune deployment config
/// (for example replay-window policy) to match their environment.
pub const TIME_SKEW_MIN: i64 = -2 * 60; // 2 minutes before
pub const TIME_SKEW_MAX: i64 = 2 * 60; // 2 minutes after
/// Maximum accepted boot-time timestamp (seconds) before skew checks are enforced.
@@ -316,7 +320,14 @@ pub fn validate_tls_handshake_with_replay_window(
};
let replay_window_u32 = u32::try_from(replay_window_secs).unwrap_or(u32::MAX);
let boot_time_cap_secs = BOOT_TIME_MAX_SECS.min(replay_window_u32);
// Boot-time bypass and ignore_time_skew serve different compatibility paths.
// When skew checks are disabled, force boot-time cap to zero to prevent
// accidental future coupling of boot-time logic into the ignore-skew path.
let boot_time_cap_secs = if ignore_time_skew {
0
} else {
BOOT_TIME_MAX_SECS.min(replay_window_u32)
};
validate_tls_handshake_at_time_with_boot_cap(
handshake,
@@ -411,7 +422,7 @@ fn validate_tls_handshake_at_time_with_boot_cap(
if !ignore_time_skew {
// Allow very small timestamps (boot time instead of unix time)
// This is a quirk in some clients that use uptime instead of real time
let is_boot_time = timestamp < boot_time_cap_secs;
let is_boot_time = boot_time_cap_secs > 0 && timestamp < boot_time_cap_secs;
if !is_boot_time {
let time_diff = now - i64::from(timestamp);
if !(TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff) {
@@ -705,10 +716,10 @@ pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
return false;
}
// TLS record header: 0x16 (handshake) 0x03 0x01 (TLS 1.0)
// TLS ClientHello commonly uses legacy record versions 0x0301 or 0x0303.
first_bytes[0] == TLS_RECORD_HANDSHAKE
&& first_bytes[1] == 0x03
&& first_bytes[2] == 0x01
&& (first_bytes[2] == 0x01 || first_bytes[2] == 0x03)
}
/// Parse TLS record header, returns (record_type, length)

View File

@@ -731,6 +731,246 @@ fn replay_window_cap_still_allows_small_boot_timestamp() {
);
}
#[test]
fn ignore_time_skew_explicitly_decouples_from_boot_time_cap() {
let secret = b"ignore_skew_boot_cap_decouple_test";
let ts: u32 = 1;
let h = make_valid_tls_handshake(secret, ts);
let secrets = vec![("u".to_string(), secret.to_vec())];
let cap_zero = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, 0);
let cap_nonzero =
validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, true, 0, BOOT_TIME_MAX_SECS);
assert!(cap_zero.is_some(), "ignore_time_skew=true must accept valid HMAC");
assert!(
cap_nonzero.is_some(),
"ignore_time_skew path must not depend on boot-time cap"
);
let a = cap_zero.unwrap();
let b = cap_nonzero.unwrap();
assert_eq!(a.user, b.user);
assert_eq!(a.timestamp, b.timestamp);
}
#[test]
fn adversarial_small_boot_timestamp_matrix_rejected_when_boot_cap_forced_zero() {
let secret = b"boot_cap_zero_matrix_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let now: i64 = 1_700_000_000;
for ts in 0u32..1024u32 {
let h = make_valid_tls_handshake(secret, ts);
let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0);
assert!(
result.is_none(),
"boot cap=0 must reject timestamp {ts} when skew checks are active"
);
}
}
#[test]
fn light_fuzz_boot_cap_zero_rejects_small_timestamp_space() {
let secret = b"boot_cap_zero_fuzz_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let now: i64 = 1_700_000_000;
let mut s: u64 = 0x9E37_79B9_7F4A_7C15;
for _ in 0..4096 {
s ^= s << 7;
s ^= s >> 9;
s ^= s << 8;
let ts = (s as u32) % 2048;
let h = make_valid_tls_handshake(secret, ts);
let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0);
assert!(
result.is_none(),
"fuzzed boot-range timestamp {ts} must be rejected when cap=0"
);
}
}
#[test]
fn stress_boot_cap_zero_rejection_is_deterministic_under_high_iteration_count() {
let secret = b"boot_cap_zero_stress_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let now: i64 = 1_700_000_000;
for i in 0u32..20_000u32 {
let ts = i % 4096;
let h = make_valid_tls_handshake(secret, ts);
let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0);
assert!(
result.is_none(),
"iteration {i}: timestamp {ts} must be rejected with cap=0"
);
}
}
#[test]
fn replay_window_one_allows_only_zero_timestamp_boot_bypass() {
let secret = b"replay_window_one_boot_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let ts0 = make_valid_tls_handshake(secret, 0);
let ts1 = make_valid_tls_handshake(secret, 1);
assert!(
validate_tls_handshake_with_replay_window(&ts0, &secrets, false, 1).is_some(),
"replay_window=1 must allow timestamp 0 via boot-time compatibility"
);
assert!(
validate_tls_handshake_with_replay_window(&ts1, &secrets, false, 1).is_none(),
"replay_window=1 must reject timestamp 1 on normal wall-clock systems"
);
}
#[test]
fn replay_window_two_allows_ts0_ts1_but_rejects_ts2() {
let secret = b"replay_window_two_boot_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let ts0 = make_valid_tls_handshake(secret, 0);
let ts1 = make_valid_tls_handshake(secret, 1);
let ts2 = make_valid_tls_handshake(secret, 2);
assert!(validate_tls_handshake_with_replay_window(&ts0, &secrets, false, 2).is_some());
assert!(validate_tls_handshake_with_replay_window(&ts1, &secrets, false, 2).is_some());
assert!(
validate_tls_handshake_with_replay_window(&ts2, &secrets, false, 2).is_none(),
"timestamp equal to replay-window cap must not use boot-time bypass"
);
}
#[test]
fn adversarial_skew_boundary_matrix_accepts_only_inclusive_window_when_boot_disabled() {
let secret = b"skew_boundary_matrix_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let now: i64 = 1_700_000_000;
for offset in -1500i64..=1500i64 {
let ts_i64 = now - offset;
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for test matrix");
let h = make_valid_tls_handshake(secret, ts);
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
.is_some();
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&offset);
assert_eq!(
accepted, expected,
"offset {offset} must match inclusive skew window when boot bypass is disabled"
);
}
}
#[test]
fn light_fuzz_skew_window_rejects_outside_range_when_boot_disabled() {
let secret = b"skew_outside_fuzz_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let now: i64 = 1_700_000_000;
let mut s: u64 = 0x0123_4567_89AB_CDEF;
for _ in 0..4096 {
s ^= s << 7;
s ^= s >> 9;
s ^= s << 8;
let magnitude = 1300i64 + ((s % 2000u64) as i64);
let sign = if (s & 1) == 0 { 1i64 } else { -1i64 };
let offset = sign * magnitude;
let ts_i64 = now - offset;
let ts = u32::try_from(ts_i64).expect("timestamp must fit u32 for fuzz test");
let h = make_valid_tls_handshake(secret, ts);
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
.is_some();
assert!(
!accepted,
"offset {offset} must be rejected outside strict skew window"
);
}
}
#[test]
fn stress_boot_disabled_validation_matches_time_diff_oracle() {
let secret = b"boot_disabled_oracle_stress_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let now: i64 = 1_700_000_000;
let mut s: u64 = 0xBADC_0FFE_EE11_2233;
for _ in 0..25_000 {
s ^= s << 7;
s ^= s >> 9;
s ^= s << 8;
let ts = s as u32;
let h = make_valid_tls_handshake(secret, ts);
let accepted = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, now, 0)
.is_some();
let time_diff = now - i64::from(ts);
let expected = (TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff);
assert_eq!(
accepted, expected,
"boot-disabled validation must match pure time-diff oracle"
);
}
}
#[test]
fn integration_large_user_list_with_boot_disabled_finds_only_matching_user() {
let now: i64 = 1_700_000_000;
let target_secret = b"target_user_secret";
let target_ts = (now - 1) as u32;
let handshake = make_valid_tls_handshake(target_secret, target_ts);
let mut secrets = Vec::new();
for i in 0..512u32 {
secrets.push((format!("noise-{i}"), format!("noise-secret-{i}").into_bytes()));
}
secrets.push(("target-user".to_string(), target_secret.to_vec()));
let result = validate_tls_handshake_at_time_with_boot_cap(&handshake, &secrets, false, now, 0)
.expect("matching user should validate within strict skew window");
assert_eq!(result.user, "target-user");
}
#[test]
fn light_fuzz_ignore_time_skew_accepts_wide_timestamp_range_with_valid_hmac() {
let secret = b"ignore_skew_fuzz_accept_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
let mut s: u64 = 0xC0FF_EE11_2233_4455;
for _ in 0..2048 {
s ^= s << 7;
s ^= s >> 9;
s ^= s << 8;
let ts = s as u32;
let h = make_valid_tls_handshake(secret, ts);
let result = validate_tls_handshake_with_replay_window(&h, &secrets, true, 60);
assert!(
result.is_some(),
"ignore_time_skew=true must accept valid HMAC for arbitrary timestamp"
);
}
}
#[test]
fn light_fuzz_small_replay_window_rejects_far_timestamps_when_skew_enabled() {
let secret = b"replay_window_reject_fuzz_test";
let secrets = vec![("u".to_string(), secret.to_vec())];
for ts in 300u32..=1323u32 {
let h = make_valid_tls_handshake(secret, ts);
let result = validate_tls_handshake_at_time_with_boot_cap(&h, &secrets, false, 0, 300);
assert!(
result.is_none(),
"with skew checks enabled and boot cap=300, timestamp >=300 at now=0 must be rejected"
);
}
}
// ------------------------------------------------------------------
// Extreme timestamp values
// ------------------------------------------------------------------
@@ -897,7 +1137,9 @@ fn first_matching_user_wins_over_later_duplicate_secret() {
#[test]
fn test_is_tls_handshake() {
assert!(is_tls_handshake(&[0x16, 0x03, 0x01]));
assert!(is_tls_handshake(&[0x16, 0x03, 0x03]));
assert!(is_tls_handshake(&[0x16, 0x03, 0x01, 0x02, 0x00]));
assert!(is_tls_handshake(&[0x16, 0x03, 0x03, 0x02, 0x00]));
assert!(!is_tls_handshake(&[0x17, 0x03, 0x01]));
assert!(!is_tls_handshake(&[0x16, 0x03, 0x02]));
assert!(!is_tls_handshake(&[0x16, 0x03]));
@@ -1502,3 +1744,83 @@ fn server_hello_new_session_ticket_count_matches_configuration() {
"response must contain one main application record plus configured ticket-like tail records"
);
}
#[test]
fn exhaustive_tls_minor_version_classification_matches_policy() {
for minor in 0u8..=u8::MAX {
let first = [TLS_RECORD_HANDSHAKE, 0x03, minor];
let expected = minor == 0x01 || minor == 0x03;
assert_eq!(
is_tls_handshake(&first),
expected,
"minor version {minor:#04x} classification mismatch"
);
}
}
#[test]
fn light_fuzz_tls_header_classifier_and_parser_policy_consistency() {
// Deterministic xorshift state keeps this fuzz test reproducible.
let mut s: u64 = 0x9E37_79B9_AA95_5A5D;
for _ in 0..10_000 {
s ^= s << 7;
s ^= s >> 9;
s ^= s << 8;
let header = [
(s & 0xff) as u8,
((s >> 8) & 0xff) as u8,
((s >> 16) & 0xff) as u8,
((s >> 24) & 0xff) as u8,
((s >> 32) & 0xff) as u8,
];
let classified = is_tls_handshake(&header[..3]);
let expected_classified = header[0] == TLS_RECORD_HANDSHAKE
&& header[1] == 0x03
&& (header[2] == 0x01 || header[2] == 0x03);
assert_eq!(
classified,
expected_classified,
"classifier policy mismatch for header {header:02x?}"
);
let parsed = parse_tls_record_header(&header);
let expected_parsed = header[1] == 0x03 && (header[2] == 0x01 || header[2] == TLS_VERSION[1]);
assert_eq!(
parsed.is_some(),
expected_parsed,
"parser policy mismatch for header {header:02x?}"
);
}
}
#[test]
fn stress_random_noise_handshakes_never_authenticate() {
let secret = b"stress_noise_secret";
let secrets = vec![("noise-user".to_string(), secret.to_vec())];
// Deterministic xorshift state keeps this stress test reproducible.
let mut s: u64 = 0xD1B5_4A32_9C6E_77F1;
for _ in 0..5_000 {
s ^= s << 7;
s ^= s >> 9;
s ^= s << 8;
let len = 1 + ((s as usize) % 196);
let mut buf = vec![0u8; len];
for b in &mut buf {
s ^= s << 7;
s ^= s >> 9;
s ^= s << 8;
*b = (s & 0xff) as u8;
}
assert!(
validate_tls_handshake(&buf, &secrets, true).is_none(),
"random noise must never authenticate"
);
}
}