mirror of
https://github.com/telemt/telemt.git
synced 2026-04-18 11:04:09 +03:00
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:
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user