mirror of https://github.com/telemt/telemt.git
TLS Validator
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
parent
bb71de0230
commit
8db566dbe9
|
|
@ -1267,6 +1267,10 @@ mod tests {
|
||||||
cfg.server.proxy_protocol_trusted_cidrs,
|
cfg.server.proxy_protocol_trusted_cidrs,
|
||||||
default_proxy_protocol_trusted_cidrs()
|
default_proxy_protocol_trusted_cidrs()
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
cfg.censorship.unknown_sni_action,
|
||||||
|
UnknownSniAction::Drop
|
||||||
|
);
|
||||||
assert_eq!(cfg.server.api.listen, default_api_listen());
|
assert_eq!(cfg.server.api.listen, default_api_listen());
|
||||||
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
assert_eq!(cfg.server.api.whitelist, default_api_whitelist());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1403,6 +1407,10 @@ mod tests {
|
||||||
server.proxy_protocol_trusted_cidrs,
|
server.proxy_protocol_trusted_cidrs,
|
||||||
default_proxy_protocol_trusted_cidrs()
|
default_proxy_protocol_trusted_cidrs()
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AntiCensorshipConfig::default().unknown_sni_action,
|
||||||
|
UnknownSniAction::Drop
|
||||||
|
);
|
||||||
assert_eq!(server.api.listen, default_api_listen());
|
assert_eq!(server.api.listen, default_api_listen());
|
||||||
assert_eq!(server.api.whitelist, default_api_whitelist());
|
assert_eq!(server.api.whitelist, default_api_whitelist());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
@ -1473,6 +1481,34 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unknown_sni_action_parses_and_defaults_to_drop() {
|
||||||
|
let cfg_default: ProxyConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[server]
|
||||||
|
[general]
|
||||||
|
[network]
|
||||||
|
[access]
|
||||||
|
[censorship]
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cfg_default.censorship.unknown_sni_action, UnknownSniAction::Drop);
|
||||||
|
|
||||||
|
let cfg_mask: ProxyConfig = toml::from_str(
|
||||||
|
r#"
|
||||||
|
[server]
|
||||||
|
[general]
|
||||||
|
[network]
|
||||||
|
[access]
|
||||||
|
[censorship]
|
||||||
|
unknown_sni_action = "mask"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(cfg_mask.censorship.unknown_sni_action, UnknownSniAction::Mask);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dc_overrides_allow_string_and_array() {
|
fn dc_overrides_allow_string_and_array() {
|
||||||
let toml = r#"
|
let toml = r#"
|
||||||
|
|
|
||||||
|
|
@ -1359,6 +1359,14 @@ impl Default for TimeoutsConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum UnknownSniAction {
|
||||||
|
#[default]
|
||||||
|
Drop,
|
||||||
|
Mask,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AntiCensorshipConfig {
|
pub struct AntiCensorshipConfig {
|
||||||
#[serde(default = "default_tls_domain")]
|
#[serde(default = "default_tls_domain")]
|
||||||
|
|
@ -1368,6 +1376,10 @@ pub struct AntiCensorshipConfig {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub tls_domains: Vec<String>,
|
pub tls_domains: Vec<String>,
|
||||||
|
|
||||||
|
/// Policy for TLS ClientHello with unknown (non-configured) SNI.
|
||||||
|
#[serde(default)]
|
||||||
|
pub unknown_sni_action: UnknownSniAction,
|
||||||
|
|
||||||
/// Upstream scope used for TLS front metadata fetches.
|
/// Upstream scope used for TLS front metadata fetches.
|
||||||
/// Empty value keeps default upstream routing behavior.
|
/// Empty value keeps default upstream routing behavior.
|
||||||
#[serde(default = "default_tls_fetch_scope")]
|
#[serde(default = "default_tls_fetch_scope")]
|
||||||
|
|
@ -1478,6 +1490,7 @@ impl Default for AntiCensorshipConfig {
|
||||||
Self {
|
Self {
|
||||||
tls_domain: default_tls_domain(),
|
tls_domain: default_tls_domain(),
|
||||||
tls_domains: Vec::new(),
|
tls_domains: Vec::new(),
|
||||||
|
unknown_sni_action: UnknownSniAction::Drop,
|
||||||
tls_fetch_scope: default_tls_fetch_scope(),
|
tls_fetch_scope: default_tls_fetch_scope(),
|
||||||
mask: default_true(),
|
mask: default_true(),
|
||||||
mask_host: None,
|
mask_host: None,
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,9 @@ pub enum ProxyError {
|
||||||
#[error("Invalid proxy protocol header")]
|
#[error("Invalid proxy protocol header")]
|
||||||
InvalidProxyProtocol,
|
InvalidProxyProtocol,
|
||||||
|
|
||||||
|
#[error("Unknown TLS SNI")]
|
||||||
|
UnknownTlsSni,
|
||||||
|
|
||||||
#[error("Proxy error: {0}")]
|
#[error("Proxy error: {0}")]
|
||||||
Proxy(String),
|
Proxy(String),
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,13 @@ fn record_handshake_failure_class(
|
||||||
record_beobachten_class(beobachten, config, peer_ip, class);
|
record_beobachten_class(beobachten, config, peer_ip, class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn increment_bad_on_unknown_tls_sni(stats: &Stats, error: &ProxyError) {
|
||||||
|
if matches!(error, ProxyError::UnknownTlsSni) {
|
||||||
|
stats.increment_connects_bad();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_trusted_proxy_source(peer_ip: IpAddr, trusted: &[IpNetwork]) -> bool {
|
fn is_trusted_proxy_source(peer_ip: IpAddr, trusted: &[IpNetwork]) -> bool {
|
||||||
if trusted.is_empty() {
|
if trusted.is_empty() {
|
||||||
static EMPTY_PROXY_TRUST_WARNED: OnceLock<AtomicBool> = OnceLock::new();
|
static EMPTY_PROXY_TRUST_WARNED: OnceLock<AtomicBool> = OnceLock::new();
|
||||||
|
|
@ -508,7 +515,10 @@ where
|
||||||
beobachten.clone(),
|
beobachten.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
HandshakeResult::Error(e) => return Err(e),
|
HandshakeResult::Error(e) => {
|
||||||
|
increment_bad_on_unknown_tls_sni(stats.as_ref(), &e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
||||||
|
|
@ -959,7 +969,10 @@ impl RunningClientHandler {
|
||||||
self.beobachten.clone(),
|
self.beobachten.clone(),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
HandshakeResult::Error(e) => return Err(e),
|
HandshakeResult::Error(e) => {
|
||||||
|
increment_bad_on_unknown_tls_sni(stats.as_ref(), &e);
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
debug!(peer = %peer, "Reading MTProto handshake through TLS");
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt};
|
||||||
use tracing::{debug, trace, warn};
|
use tracing::{debug, trace, warn};
|
||||||
use zeroize::{Zeroize, Zeroizing};
|
use zeroize::{Zeroize, Zeroizing};
|
||||||
|
|
||||||
use crate::config::ProxyConfig;
|
use crate::config::{ProxyConfig, UnknownSniAction};
|
||||||
use crate::crypto::{AesCtr, SecureRandom, sha256};
|
use crate::crypto::{AesCtr, SecureRandom, sha256};
|
||||||
use crate::error::{HandshakeResult, ProxyError};
|
use crate::error::{HandshakeResult, ProxyError};
|
||||||
use crate::protocol::constants::*;
|
use crate::protocol::constants::*;
|
||||||
|
|
@ -510,6 +510,21 @@ fn decode_user_secrets(
|
||||||
secrets
|
secrets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn find_matching_tls_domain<'a>(config: &'a ProxyConfig, sni: &str) -> Option<&'a str> {
|
||||||
|
if config.censorship.tls_domain.eq_ignore_ascii_case(sni) {
|
||||||
|
return Some(config.censorship.tls_domain.as_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
for domain in &config.censorship.tls_domains {
|
||||||
|
if domain.eq_ignore_ascii_case(sni) {
|
||||||
|
return Some(domain.as_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
async fn maybe_apply_server_hello_delay(config: &ProxyConfig) {
|
async fn maybe_apply_server_hello_delay(config: &ProxyConfig) {
|
||||||
if config.censorship.server_hello_delay_max_ms == 0 {
|
if config.censorship.server_hello_delay_max_ms == 0 {
|
||||||
return;
|
return;
|
||||||
|
|
@ -593,6 +608,25 @@ where
|
||||||
}
|
}
|
||||||
|
|
||||||
let client_sni = tls::extract_sni_from_client_hello(handshake);
|
let client_sni = tls::extract_sni_from_client_hello(handshake);
|
||||||
|
let matched_tls_domain = client_sni
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|sni| find_matching_tls_domain(config, sni));
|
||||||
|
|
||||||
|
if client_sni.is_some() && matched_tls_domain.is_none() {
|
||||||
|
auth_probe_record_failure(peer.ip(), Instant::now());
|
||||||
|
maybe_apply_server_hello_delay(config).await;
|
||||||
|
debug!(
|
||||||
|
peer = %peer,
|
||||||
|
sni = ?client_sni,
|
||||||
|
action = ?config.censorship.unknown_sni_action,
|
||||||
|
"TLS handshake rejected by unknown SNI policy"
|
||||||
|
);
|
||||||
|
return match config.censorship.unknown_sni_action {
|
||||||
|
UnknownSniAction::Drop => HandshakeResult::Error(ProxyError::UnknownTlsSni),
|
||||||
|
UnknownSniAction::Mask => HandshakeResult::BadClient { reader, writer },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let secrets = decode_user_secrets(config, client_sni.as_deref());
|
let secrets = decode_user_secrets(config, client_sni.as_deref());
|
||||||
|
|
||||||
let validation = match tls::validate_tls_handshake_with_replay_window(
|
let validation = match tls::validate_tls_handshake_with_replay_window(
|
||||||
|
|
@ -633,16 +667,8 @@ where
|
||||||
|
|
||||||
let cached = if config.censorship.tls_emulation {
|
let cached = if config.censorship.tls_emulation {
|
||||||
if let Some(cache) = tls_cache.as_ref() {
|
if let Some(cache) = tls_cache.as_ref() {
|
||||||
let selected_domain = if let Some(sni) = client_sni.as_ref() {
|
let selected_domain = matched_tls_domain.unwrap_or(config.censorship.tls_domain.as_str());
|
||||||
if cache.contains_domain(sni).await {
|
let cached_entry = cache.get(selected_domain).await;
|
||||||
sni.clone()
|
|
||||||
} else {
|
|
||||||
config.censorship.tls_domain.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
config.censorship.tls_domain.clone()
|
|
||||||
};
|
|
||||||
let cached_entry = cache.get(&selected_domain).await;
|
|
||||||
let use_full_cert_payload = cache
|
let use_full_cert_payload = cache
|
||||||
.take_full_cert_budget_for_ip(
|
.take_full_cert_budget_for_ip(
|
||||||
peer.ip(),
|
peer.ip(),
|
||||||
|
|
|
||||||
|
|
@ -956,6 +956,89 @@ async fn stress_tls_sni_preferred_user_hint_scales_to_large_user_set() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tls_unknown_sni_drop_policy_returns_hard_error() {
|
||||||
|
let secret = [0x48u8; 16];
|
||||||
|
let mut config = test_config_with_secret_hex("48484848484848484848484848484848");
|
||||||
|
config.censorship.unknown_sni_action = UnknownSniAction::Drop;
|
||||||
|
|
||||||
|
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
||||||
|
let rng = SecureRandom::new();
|
||||||
|
let peer: SocketAddr = "198.51.100.190:44326".parse().unwrap();
|
||||||
|
let handshake =
|
||||||
|
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "unknown.example", &[b"h2"]);
|
||||||
|
|
||||||
|
let result = handle_tls_handshake(
|
||||||
|
&handshake,
|
||||||
|
tokio::io::empty(),
|
||||||
|
tokio::io::sink(),
|
||||||
|
peer,
|
||||||
|
&config,
|
||||||
|
&replay_checker,
|
||||||
|
&rng,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
result,
|
||||||
|
HandshakeResult::Error(ProxyError::UnknownTlsSni)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tls_unknown_sni_mask_policy_falls_back_to_bad_client() {
|
||||||
|
let secret = [0x49u8; 16];
|
||||||
|
let mut config = test_config_with_secret_hex("49494949494949494949494949494949");
|
||||||
|
config.censorship.unknown_sni_action = UnknownSniAction::Mask;
|
||||||
|
|
||||||
|
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
||||||
|
let rng = SecureRandom::new();
|
||||||
|
let peer: SocketAddr = "198.51.100.191:44326".parse().unwrap();
|
||||||
|
let handshake =
|
||||||
|
make_valid_tls_client_hello_with_sni_and_alpn(&secret, 0, "unknown.example", &[b"h2"]);
|
||||||
|
|
||||||
|
let result = handle_tls_handshake(
|
||||||
|
&handshake,
|
||||||
|
tokio::io::empty(),
|
||||||
|
tokio::io::sink(),
|
||||||
|
peer,
|
||||||
|
&config,
|
||||||
|
&replay_checker,
|
||||||
|
&rng,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(result, HandshakeResult::BadClient { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn tls_missing_sni_keeps_legacy_auth_path() {
|
||||||
|
let secret = [0x4Au8; 16];
|
||||||
|
let mut config = test_config_with_secret_hex("4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a4a");
|
||||||
|
config.censorship.unknown_sni_action = UnknownSniAction::Drop;
|
||||||
|
|
||||||
|
let replay_checker = ReplayChecker::new(128, Duration::from_secs(60));
|
||||||
|
let rng = SecureRandom::new();
|
||||||
|
let peer: SocketAddr = "198.51.100.192:44326".parse().unwrap();
|
||||||
|
let handshake = make_valid_tls_handshake(&secret, 0);
|
||||||
|
|
||||||
|
let result = handle_tls_handshake(
|
||||||
|
&handshake,
|
||||||
|
tokio::io::empty(),
|
||||||
|
tokio::io::sink(),
|
||||||
|
peer,
|
||||||
|
&config,
|
||||||
|
&replay_checker,
|
||||||
|
&rng,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(matches!(result, HandshakeResult::Success(_)));
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn alpn_enforce_rejects_unsupported_client_alpn() {
|
async fn alpn_enforce_rejects_unsupported_client_alpn() {
|
||||||
let secret = [0x33u8; 16];
|
let secret = [0x33u8; 16];
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue