feat(tls): add boot time timestamp constant and validation for SNI hostnames

- Introduced `BOOT_TIME_MAX_SECS` constant to define the maximum accepted boot-time timestamp.
- Updated `validate_tls_handshake_at_time` to utilize the new boot time constant for timestamp validation.
- Enhanced `extract_sni_from_client_hello` to validate SNI hostnames against specified criteria, rejecting invalid hostnames.
- Added tests to ensure proper handling of boot time timestamps and SNI validation.

feat(handshake): improve user secret decoding and ALPN enforcement

- Refactored user secret decoding to provide better error handling and logging for invalid secrets.
- Added tests for concurrent identical handshakes to ensure replay protection works as expected.
- Implemented ALPN enforcement in handshake processing, rejecting unsupported protocols and allowing valid ones.

fix(masking): implement timeout handling for masking operations

- Added timeout handling for writing proxy headers and consuming client data in masking.
- Adjusted timeout durations for testing to ensure faster feedback during unit tests.
- Introduced tests to verify behavior when masking is disabled and when proxy header writes exceed the timeout.

test(masking): add tests for slowloris connections and proxy header timeouts

- Created tests to validate that slowloris connections are closed by consume timeout when masking is disabled.
- Added a test for proxy header write timeout to ensure it returns false when the write operation does not complete.
This commit is contained in:
David Osipov
2026-03-16 21:37:59 +04:00
parent 213ce4555a
commit e4a50f9286
7 changed files with 895 additions and 25 deletions

View File

@@ -29,6 +29,8 @@ pub const TLS_DIGEST_HALF_LEN: usize = 16;
/// Time skew limits for anti-replay (in seconds)
pub const TIME_SKEW_MIN: i64 = -20 * 60; // 20 minutes before
pub const TIME_SKEW_MAX: i64 = 10 * 60; // 10 minutes after
/// Maximum accepted boot-time timestamp (seconds) before skew checks are enforced.
pub const BOOT_TIME_MAX_SECS: u32 = 7 * 24 * 60 * 60;
// ============= Private Constants =============
@@ -364,7 +366,7 @@ fn validate_tls_handshake_at_time(
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 < 60 * 60 * 24 * 1000; // < ~2.7 years in seconds
let is_boot_time = timestamp < BOOT_TIME_MAX_SECS;
if !is_boot_time {
let time_diff = now - i64::from(timestamp);
if !(TIME_SKEW_MIN..=TIME_SKEW_MAX).contains(&time_diff) {
@@ -563,7 +565,9 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
if name_type == 0 && name_len > 0
&& let Ok(host) = std::str::from_utf8(&handshake[sn_pos..sn_pos + name_len])
{
return Some(host.to_string());
if is_valid_sni_hostname(host) {
return Some(host.to_string());
}
}
sn_pos += name_len;
}
@@ -574,6 +578,35 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option<String> {
None
}
fn is_valid_sni_hostname(host: &str) -> bool {
if host.is_empty() || host.len() > 253 {
return false;
}
if host.starts_with('.') || host.ends_with('.') {
return false;
}
if host.parse::<std::net::IpAddr>().is_ok() {
return false;
}
for label in host.split('.') {
if label.is_empty() || label.len() > 63 {
return false;
}
if label.starts_with('-') || label.ends_with('-') {
return false;
}
if !label
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'-')
{
return false;
}
}
true
}
/// Extract ALPN protocol list from ClientHello, return in offered order.
pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
let mut pos = 5; // after record header

View File

@@ -286,8 +286,8 @@ fn boot_time_timestamp_accepted_without_ignore_flag() {
// Timestamps below the boot-time threshold are treated as client uptime,
// not real wall-clock time. The proxy allows them regardless of skew.
let secret = b"boot_time_test";
// 86_400_000 / 2 is well below the boot-time threshold (~2.74 years worth of seconds).
let boot_ts: u32 = 86_400_000 / 2;
// Keep this safely below BOOT_TIME_MAX_SECS to assert bypass behavior.
let boot_ts: u32 = BOOT_TIME_MAX_SECS / 2;
let handshake = make_valid_tls_handshake(secret, boot_ts);
let secrets = vec![("u".to_string(), secret.to_vec())];
assert!(
@@ -611,13 +611,13 @@ fn zero_length_session_id_accepted() {
// Boot-time threshold — exact boundary precision
// ------------------------------------------------------------------
/// timestamp = 86_399_999 is the last value inside the boot-time window.
/// timestamp = BOOT_TIME_MAX_SECS - 1 is the last value inside the boot-time window.
/// is_boot_time = true → skew check is skipped entirely → accepted even
/// when `now` is far from the timestamp.
#[test]
fn timestamp_one_below_boot_threshold_bypasses_skew_check() {
let secret = b"boot_last_value_test";
let ts: u32 = 86_400_000 - 1;
let ts: u32 = BOOT_TIME_MAX_SECS - 1;
let h = make_valid_tls_handshake(secret, ts);
let secrets = vec![("u".to_string(), secret.to_vec())];
@@ -625,17 +625,17 @@ fn timestamp_one_below_boot_threshold_bypasses_skew_check() {
// Boot-time bypass must prevent the skew check from running.
assert!(
validate_tls_handshake_at_time(&h, &secrets, false, 0).is_some(),
"ts=86_399_999 must bypass skew check regardless of now"
"ts=BOOT_TIME_MAX_SECS-1 must bypass skew check regardless of now"
);
}
/// timestamp = 86_400_000 is the first value outside the boot-time window.
/// timestamp = BOOT_TIME_MAX_SECS is the first value outside the boot-time window.
/// is_boot_time = false → skew check IS applied. Two sub-cases confirm this:
/// once with now chosen so the skew passes (accepted) and once where it fails.
#[test]
fn timestamp_at_boot_threshold_triggers_skew_check() {
let secret = b"boot_exact_value_test";
let ts: u32 = 86_400_000;
let ts: u32 = BOOT_TIME_MAX_SECS;
let h = make_valid_tls_handshake(secret, ts);
let secrets = vec![("u".to_string(), secret.to_vec())];
@@ -643,14 +643,14 @@ fn timestamp_at_boot_threshold_triggers_skew_check() {
let now_valid: i64 = ts as i64 + 50;
assert!(
validate_tls_handshake_at_time(&h, &secrets, false, now_valid).is_some(),
"ts=86_400_000 within skew window must be accepted via skew check"
"ts=BOOT_TIME_MAX_SECS within skew window must be accepted via skew check"
);
// now = 0 → time_diff = -86_400_000, outside window → rejected.
// If the boot-time bypass were wrongly applied here this would pass.
assert!(
validate_tls_handshake_at_time(&h, &secrets, false, 0).is_none(),
"ts=86_400_000 far from now must be rejected — no boot-time bypass"
"ts=BOOT_TIME_MAX_SECS far from now must be rejected — no boot-time bypass"
);
}
@@ -675,7 +675,7 @@ fn u32_max_timestamp_accepted_with_ignore_time_skew() {
);
}
/// u32::MAX > 86_400_000 so the skew check runs. With any realistic `now`
/// u32::MAX > BOOT_TIME_MAX_SECS so the skew check runs. With any realistic `now`
/// (~1.7 billion), time_diff = now - u32::MAX is deeply negative — far outside
/// [-1200, 600] — so the handshake must be rejected without overflow.
#[test]
@@ -1109,6 +1109,25 @@ fn extract_sni_rejects_zero_length_host_name() {
assert!(extract_sni_from_client_hello(&ch).is_none());
}
#[test]
fn extract_sni_rejects_raw_ipv4_literals() {
let ch = build_client_hello_with_exts(Vec::new(), "203.0.113.10");
assert!(extract_sni_from_client_hello(&ch).is_none());
}
#[test]
fn extract_sni_rejects_invalid_label_characters() {
let ch = build_client_hello_with_exts(Vec::new(), "exa_mple.com");
assert!(extract_sni_from_client_hello(&ch).is_none());
}
#[test]
fn extract_sni_rejects_oversized_label() {
let oversized = format!("{}.example.com", "a".repeat(64));
let ch = build_client_hello_with_exts(Vec::new(), &oversized);
assert!(extract_sni_from_client_hello(&ch).is_none());
}
#[test]
fn extract_sni_rejects_when_extension_block_is_truncated() {
let mut ext_blob = Vec::new();