diff --git a/src/config/load.rs b/src/config/load.rs index 0f0990c..2e27edb 100644 --- a/src/config/load.rs +++ b/src/config/load.rs @@ -1635,6 +1635,22 @@ mod tests { cfg_mask.censorship.unknown_sni_action, UnknownSniAction::Mask ); + + let cfg_accept: ProxyConfig = toml::from_str( + r#" + [server] + [general] + [network] + [access] + [censorship] + unknown_sni_action = "accept" + "#, + ) + .unwrap(); + assert_eq!( + cfg_accept.censorship.unknown_sni_action, + UnknownSniAction::Accept + ); } #[test] diff --git a/src/config/types.rs b/src/config/types.rs index a1ffd13..0a5af21 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -1502,6 +1502,7 @@ pub enum UnknownSniAction { #[default] Drop, Mask, + Accept, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] diff --git a/src/proxy/handshake.rs b/src/proxy/handshake.rs index 16d0c5e..8524cff 100644 --- a/src/proxy/handshake.rs +++ b/src/proxy/handshake.rs @@ -813,31 +813,45 @@ where }; if client_sni.is_some() && matched_tls_domain.is_none() && preferred_user_hint.is_none() { - auth_probe_record_failure_in(shared, peer.ip(), Instant::now()); - maybe_apply_server_hello_delay(config).await; let sni = client_sni.as_deref().unwrap_or_default(); - let log_now = Instant::now(); - if should_emit_unknown_sni_warn_in(shared, log_now) { - warn!( - peer = %peer, - sni = %sni, - unknown_sni = true, - unknown_sni_action = ?config.censorship.unknown_sni_action, - "TLS handshake rejected by unknown SNI policy" - ); - } else { - info!( - peer = %peer, - sni = %sni, - unknown_sni = true, - unknown_sni_action = ?config.censorship.unknown_sni_action, - "TLS handshake rejected by unknown SNI policy" - ); + match config.censorship.unknown_sni_action { + UnknownSniAction::Accept => { + debug!( + peer = %peer, + sni = %sni, + unknown_sni = true, + unknown_sni_action = ?config.censorship.unknown_sni_action, + "TLS handshake accepted by unknown SNI policy" + ); + } + action @ (UnknownSniAction::Drop | UnknownSniAction::Mask) => { + auth_probe_record_failure_in(shared, peer.ip(), Instant::now()); + maybe_apply_server_hello_delay(config).await; + let log_now = Instant::now(); + if should_emit_unknown_sni_warn_in(shared, log_now) { + warn!( + peer = %peer, + sni = %sni, + unknown_sni = true, + unknown_sni_action = ?action, + "TLS handshake rejected by unknown SNI policy" + ); + } else { + info!( + peer = %peer, + sni = %sni, + unknown_sni = true, + unknown_sni_action = ?action, + "TLS handshake rejected by unknown SNI policy" + ); + } + return match action { + UnknownSniAction::Drop => HandshakeResult::Error(ProxyError::UnknownTlsSni), + UnknownSniAction::Mask => HandshakeResult::BadClient { reader, writer }, + UnknownSniAction::Accept => unreachable!(), + }; + } } - return match config.censorship.unknown_sni_action { - UnknownSniAction::Drop => HandshakeResult::Error(ProxyError::UnknownTlsSni), - UnknownSniAction::Mask => HandshakeResult::BadClient { reader, writer }, - }; } let secrets = decode_user_secrets_in(shared, config, preferred_user_hint); diff --git a/src/proxy/tests/handshake_security_tests.rs b/src/proxy/tests/handshake_security_tests.rs index d8396b5..df3bbe0 100644 --- a/src/proxy/tests/handshake_security_tests.rs +++ b/src/proxy/tests/handshake_security_tests.rs @@ -1006,6 +1006,64 @@ async fn tls_unknown_sni_mask_policy_falls_back_to_bad_client() { assert!(matches!(result, HandshakeResult::BadClient { .. })); } +#[tokio::test] +async fn tls_unknown_sni_accept_policy_continues_auth_path() { + let secret = [0x4Bu8; 16]; + let mut config = test_config_with_secret_hex("4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b4b"); + config.censorship.unknown_sni_action = UnknownSniAction::Accept; + + let replay_checker = ReplayChecker::new(128, Duration::from_secs(60)); + let rng = SecureRandom::new(); + let peer: SocketAddr = "198.51.100.210: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::Success(_))); +} + +#[tokio::test] +async fn tls_unknown_sni_accept_policy_still_requires_valid_secret() { + let mut config = test_config_with_secret_hex("4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c4c"); + config.censorship.unknown_sni_action = UnknownSniAction::Accept; + + let replay_checker = ReplayChecker::new(128, Duration::from_secs(60)); + let rng = SecureRandom::new(); + let peer: SocketAddr = "198.51.100.211:44326".parse().unwrap(); + let attacker_secret = [0x4Du8; 16]; + let handshake = make_valid_tls_client_hello_with_sni_and_alpn( + &attacker_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];