From 83fc9d6db3daf27def14a7e6acc9902c6377c0c0 Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Sat, 21 Feb 2026 03:36:13 +0300 Subject: [PATCH] Middle-End Fixes Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/config/defaults.rs | 61 +++++++++++ src/config/types.rs | 75 +++++++++++++ src/main.rs | 3 + src/protocol/tls.rs | 164 +++++++++++++++++------------ src/proxy/handshake.rs | 36 +++++++ src/tls_front/emulator.rs | 28 ++++- src/transport/middle_proxy/pool.rs | 6 +- 7 files changed, 300 insertions(+), 73 deletions(-) diff --git a/src/config/defaults.rs b/src/config/defaults.rs index b68c120..2dee3e0 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -118,6 +118,67 @@ pub(crate) fn default_max_client_frame() -> usize { 16 * 1024 * 1024 } +pub(crate) fn default_tls_new_session_tickets() -> u8 { + 0 +} + +pub(crate) fn default_server_hello_delay_min_ms() -> u64 { + 0 +} + +pub(crate) fn default_server_hello_delay_max_ms() -> u64 { + 0 +} + +pub(crate) fn default_alpn_enforce() -> bool { + true +} + +pub(crate) fn default_stun_servers() -> Vec { + vec![ + "stun.l.google.com:19302".to_string(), + "stun1.l.google.com:19302".to_string(), + "stun2.l.google.com:19302".to_string(), + "stun.stunprotocol.org:3478".to_string(), + "stun.voip.eutelia.it:3478".to_string(), + ] +} + +pub(crate) fn default_http_ip_detect_urls() -> Vec { + vec![ + "https://ifconfig.me/ip".to_string(), + "https://api.ipify.org".to_string(), + ] +} + +pub(crate) fn default_cache_public_ip_path() -> String { + "cache/public_ip.txt".to_string() +} + +pub(crate) fn default_proxy_secret_reload_secs() -> u64 { + 12 * 60 * 60 +} + +pub(crate) fn default_proxy_config_reload_secs() -> u64 { + 12 * 60 * 60 +} + +pub(crate) fn default_ntp_check() -> bool { + true +} + +pub(crate) fn default_ntp_servers() -> Vec { + vec!["pool.ntp.org".to_string()] +} + +pub(crate) fn default_fast_mode_min_tls_record() -> usize { + 0 +} + +pub(crate) fn default_degradation_min_unavailable_dc_groups() -> u8 { + 2 +} + // Custom deserializer helpers #[derive(Deserialize)] diff --git a/src/config/types.rs b/src/config/types.rs index 41b7a3f..6c54598 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -96,6 +96,22 @@ pub struct NetworkConfig { #[serde(default)] pub multipath: bool, + + /// STUN servers list for public IP discovery. + #[serde(default = "default_stun_servers")] + pub stun_servers: Vec, + + /// Enable TCP STUN fallback when UDP is blocked. + #[serde(default)] + pub stun_tcp_fallback: bool, + + /// HTTP-based public IP detection endpoints (fallback after STUN). + #[serde(default = "default_http_ip_detect_urls")] + pub http_ip_detect_urls: Vec, + + /// Cache file path for detected public IP. + #[serde(default = "default_cache_public_ip_path")] + pub cache_public_ip_path: String, } impl Default for NetworkConfig { @@ -105,6 +121,10 @@ impl Default for NetworkConfig { ipv6: None, prefer: 4, multipath: false, + stun_servers: default_stun_servers(), + stun_tcp_fallback: true, + http_ip_detect_urls: default_http_ip_detect_urls(), + cache_public_ip_path: default_cache_public_ip_path(), } } } @@ -227,6 +247,34 @@ pub struct GeneralConfig { /// [general.links] — proxy link generation overrides. #[serde(default)] pub links: LinksConfig, + + /// Minimum TLS record size when fast_mode coalescing is enabled (0 = disabled). + #[serde(default = "default_fast_mode_min_tls_record")] + pub fast_mode_min_tls_record: usize, + + /// Automatically reload proxy-secret every N seconds. + #[serde(default = "default_proxy_secret_reload_secs")] + pub proxy_secret_auto_reload_secs: u64, + + /// Automatically reload proxy-multi.conf every N seconds. + #[serde(default = "default_proxy_config_reload_secs")] + pub proxy_config_auto_reload_secs: u64, + + /// Enable NTP drift check at startup. + #[serde(default = "default_ntp_check")] + pub ntp_check: bool, + + /// NTP servers for drift check. + #[serde(default = "default_ntp_servers")] + pub ntp_servers: Vec, + + /// Enable auto-degradation from ME to Direct-DC. + #[serde(default = "default_true")] + pub auto_degradation_enabled: bool, + + /// Minimum unavailable ME DC groups before degrading. + #[serde(default = "default_degradation_min_unavailable_dc_groups")] + pub degradation_min_unavailable_dc_groups: u8, } impl Default for GeneralConfig { @@ -262,6 +310,13 @@ impl Default for GeneralConfig { links: LinksConfig::default(), crypto_pending_buffer: default_crypto_pending_buffer(), max_client_frame: default_max_client_frame(), + fast_mode_min_tls_record: default_fast_mode_min_tls_record(), + proxy_secret_auto_reload_secs: default_proxy_secret_reload_secs(), + proxy_config_auto_reload_secs: default_proxy_config_reload_secs(), + ntp_check: default_ntp_check(), + ntp_servers: default_ntp_servers(), + auto_degradation_enabled: true, + degradation_min_unavailable_dc_groups: default_degradation_min_unavailable_dc_groups(), } } } @@ -406,6 +461,22 @@ pub struct AntiCensorshipConfig { /// Directory to store TLS front cache (on disk). #[serde(default = "default_tls_front_dir")] pub tls_front_dir: String, + + /// Minimum server_hello delay in milliseconds (anti-fingerprint). + #[serde(default = "default_server_hello_delay_min_ms")] + pub server_hello_delay_min_ms: u64, + + /// Maximum server_hello delay in milliseconds. + #[serde(default = "default_server_hello_delay_max_ms")] + pub server_hello_delay_max_ms: u64, + + /// Number of NewSessionTicket messages to emit post-handshake. + #[serde(default = "default_tls_new_session_tickets")] + pub tls_new_session_tickets: u8, + + /// Enforce ALPN echo of client preference. + #[serde(default = "default_alpn_enforce")] + pub alpn_enforce: bool, } impl Default for AntiCensorshipConfig { @@ -420,6 +491,10 @@ impl Default for AntiCensorshipConfig { fake_cert_len: default_fake_cert_len(), tls_emulation: false, tls_front_dir: default_tls_front_dir(), + server_hello_delay_min_ms: default_server_hello_delay_min_ms(), + server_hello_delay_max_ms: default_server_hello_delay_max_ms(), + tls_new_session_tickets: default_tls_new_session_tickets(), + alpn_enforce: default_alpn_enforce(), } } } diff --git a/src/main.rs b/src/main.rs index 8446403..0e635de 100644 --- a/src/main.rs +++ b/src/main.rs @@ -213,6 +213,9 @@ async fn main() -> std::result::Result<(), Box> { "Modes: classic={} secure={} tls={}", config.general.modes.classic, config.general.modes.secure, config.general.modes.tls ); + if config.general.modes.classic { + warn!("Classic mode is vulnerable to DPI detection; enable only for legacy clients"); + } info!("TLS domain: {}", config.censorship.tls_domain); if let Some(ref sock) = config.censorship.mask_unix_sock { info!("Mask: {} -> unix:{}", config.censorship.mask, sock); diff --git a/src/protocol/tls.rs b/src/protocol/tls.rs index 5f890a5..a0d1d46 100644 --- a/src/protocol/tls.rs +++ b/src/protocol/tls.rs @@ -32,6 +32,7 @@ pub const TIME_SKEW_MAX: i64 = 10 * 60; // 10 minutes after mod extension_type { pub const KEY_SHARE: u16 = 0x0033; pub const SUPPORTED_VERSIONS: u16 = 0x002b; + pub const ALPN: u16 = 0x0010; } /// TLS Cipher Suites @@ -62,6 +63,7 @@ pub struct TlsValidation { // ============= TLS Extension Builder ============= /// Builder for TLS extensions with correct length calculation +#[derive(Clone)] struct TlsExtensionBuilder { extensions: Vec, } @@ -108,6 +110,27 @@ impl TlsExtensionBuilder { self } + + /// Add ALPN extension with a single selected protocol. + fn add_alpn(&mut self, proto: &[u8]) -> &mut Self { + // Extension type: ALPN (0x0010) + self.extensions.extend_from_slice(&extension_type::ALPN.to_be_bytes()); + + // ALPN extension format: + // extension_data length (2 bytes) + // protocols length (2 bytes) + // protocol name length (1 byte) + // protocol name bytes + let proto_len = proto.len() as u8; + let list_len: u16 = 1 + proto_len as u16; + let ext_len: u16 = 2 + list_len; + + self.extensions.extend_from_slice(&ext_len.to_be_bytes()); + self.extensions.extend_from_slice(&list_len.to_be_bytes()); + self.extensions.push(proto_len); + self.extensions.extend_from_slice(proto); + self + } /// Build final extensions with length prefix fn build(self) -> Vec { @@ -144,6 +167,8 @@ struct ServerHelloBuilder { compression: u8, /// Extensions extensions: TlsExtensionBuilder, + /// Selected ALPN protocol (if any) + alpn: Option>, } impl ServerHelloBuilder { @@ -154,6 +179,7 @@ impl ServerHelloBuilder { cipher_suite: cipher_suite::TLS_AES_128_GCM_SHA256, compression: 0x00, extensions: TlsExtensionBuilder::new(), + alpn: None, } } @@ -167,10 +193,19 @@ impl ServerHelloBuilder { self.extensions.add_supported_versions(0x0304); self } + + fn with_alpn(mut self, proto: Option>) -> Self { + self.alpn = proto; + self + } /// Build ServerHello message (without record header) fn build_message(&self) -> Vec { - let extensions = self.extensions.extensions.clone(); + let mut ext_builder = self.extensions.clone(); + if let Some(ref alpn) = self.alpn { + ext_builder.add_alpn(alpn); + } + let extensions = ext_builder.extensions.clone(); let extensions_len = extensions.len() as u16; // Calculate total length @@ -350,6 +385,8 @@ pub fn build_server_hello( session_id: &[u8], fake_cert_len: usize, rng: &SecureRandom, + alpn: Option>, + new_session_tickets: u8, ) -> Vec { const MIN_APP_DATA: usize = 64; const MAX_APP_DATA: usize = 16640; // RFC 8446 §5.2 upper bound @@ -360,6 +397,7 @@ pub fn build_server_hello( let server_hello = ServerHelloBuilder::new(session_id.to_vec()) .with_x25519_key(&x25519_key) .with_tls13_version() + .with_alpn(alpn) .build_record(); // Build Change Cipher Spec record @@ -380,13 +418,31 @@ pub fn build_server_hello( // deterministic DPI fingerprints (fixed inner content type markers). app_data_record.extend_from_slice(&fake_cert); + // Build optional NewSessionTicket records (TLS 1.3 handshake messages are encrypted; + // here we mimic with opaque ApplicationData records of plausible size). + let mut tickets = Vec::new(); + if new_session_tickets > 0 { + for _ in 0..new_session_tickets { + let ticket_len: usize = rng.range(48) + 48; // 48-95 bytes + let mut record = Vec::with_capacity(5 + ticket_len); + record.push(TLS_RECORD_APPLICATION); + record.extend_from_slice(&TLS_VERSION); + record.extend_from_slice(&(ticket_len as u16).to_be_bytes()); + record.extend_from_slice(&rng.bytes(ticket_len)); + tickets.push(record); + } + } + // Combine all records let mut response = Vec::with_capacity( - server_hello.len() + change_cipher_spec.len() + app_data_record.len() + server_hello.len() + change_cipher_spec.len() + app_data_record.len() + tickets.iter().map(|r| r.len()).sum::() ); response.extend_from_slice(&server_hello); response.extend_from_slice(&change_cipher_spec); response.extend_from_slice(&app_data_record); + for t in &tickets { + response.extend_from_slice(t); + } // Compute HMAC for the response let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + response.len()); @@ -480,85 +536,53 @@ pub fn extract_sni_from_client_hello(handshake: &[u8]) -> Option { None } -/// Extract ALPN protocol list from TLS ClientHello. -pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Option> { - if handshake.len() < 43 || handshake[0] != TLS_RECORD_HANDSHAKE { - return None; - } - +/// Extract ALPN protocol list from ClientHello, return in offered order. +pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec> { let mut pos = 5; // after record header - if handshake.get(pos).copied()? != 0x01 { - return None; // not ClientHello + if handshake.get(pos) != Some(&0x01) { + return Vec::new(); } - - // Handshake length bytes - pos += 4; // type + len (3) - - // version (2) + random (32) - pos += 2 + 32; - if pos + 1 > handshake.len() { - return None; - } - - let session_id_len = *handshake.get(pos)? as usize; + pos += 4; // type + len + pos += 2 + 32; // version + random + if pos >= handshake.len() { return Vec::new(); } + let session_id_len = *handshake.get(pos).unwrap_or(&0) as usize; pos += 1 + session_id_len; - if pos + 2 > handshake.len() { - return None; - } - - let cipher_suites_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize; - pos += 2 + cipher_suites_len; - if pos + 1 > handshake.len() { - return None; - } - - let comp_len = *handshake.get(pos)? as usize; + if pos + 2 > handshake.len() { return Vec::new(); } + let cipher_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize; + pos += 2 + cipher_len; + if pos >= handshake.len() { return Vec::new(); } + let comp_len = *handshake.get(pos).unwrap_or(&0) as usize; pos += 1 + comp_len; - if pos + 2 > handshake.len() { - return None; - } - - let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize; + if pos + 2 > handshake.len() { return Vec::new(); } + let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize; pos += 2; let ext_end = pos + ext_len; - if ext_end > handshake.len() { - return None; - } - + if ext_end > handshake.len() { return Vec::new(); } + let mut out = Vec::new(); while pos + 4 <= ext_end { - let etype = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]); - let elen = u16::from_be_bytes([handshake[pos + 2], handshake[pos + 3]]) as usize; + let etype = u16::from_be_bytes([handshake[pos], handshake[pos+1]]); + let elen = u16::from_be_bytes([handshake[pos+2], handshake[pos+3]]) as usize; pos += 4; - if pos + elen > ext_end { + if pos + elen > ext_end { break; } + if etype == extension_type::ALPN && elen >= 3 { + let list_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize; + let mut lp = pos + 2; + let list_end = (pos + 2).saturating_add(list_len).min(pos + elen); + while lp + 1 <= list_end { + let plen = handshake[lp] as usize; + lp += 1; + if lp + plen > list_end { break; } + out.push(handshake[lp..lp+plen].to_vec()); + lp += plen; + } break; } - - if etype == 0x0010 && elen >= 3 { - // ALPN - let list_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize; - let mut alpn_pos = pos + 2; - let list_end = std::cmp::min(alpn_pos + list_len, pos + elen); - let mut protocols = Vec::new(); - while alpn_pos < list_end { - let proto_len = *handshake.get(alpn_pos)? as usize; - alpn_pos += 1; - if alpn_pos + proto_len > list_end { - break; - } - if let Ok(p) = std::str::from_utf8(&handshake[alpn_pos..alpn_pos + proto_len]) { - protocols.push(p.to_string()); - } - alpn_pos += proto_len; - } - return Some(protocols); - } - pos += elen; } - - None + out } + /// Check if bytes look like a TLS ClientHello pub fn is_tls_handshake(first_bytes: &[u8]) -> bool { if first_bytes.len() < 3 { @@ -737,7 +761,7 @@ mod tests { let session_id = vec![0xAA; 32]; let rng = SecureRandom::new(); - let response = build_server_hello(secret, &client_digest, &session_id, 2048, &rng); + let response = build_server_hello(secret, &client_digest, &session_id, 2048, &rng, None, 0); // Should have at least 3 records assert!(response.len() > 100); @@ -770,8 +794,8 @@ mod tests { let session_id = vec![0xAA; 32]; let rng = SecureRandom::new(); - let response1 = build_server_hello(secret, &client_digest, &session_id, 1024, &rng); - let response2 = build_server_hello(secret, &client_digest, &session_id, 1024, &rng); + let response1 = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 0); + let response2 = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 0); // Digest position should have non-zero data let digest1 = &response1[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN]; diff --git a/src/proxy/handshake.rs b/src/proxy/handshake.rs index 685d999..8d48c8b 100644 --- a/src/proxy/handshake.rs +++ b/src/proxy/handshake.rs @@ -7,6 +7,7 @@ use tracing::{debug, warn, trace, info}; use zeroize::Zeroize; use crate::crypto::{sha256, AesCtr, SecureRandom}; +use rand::Rng; use crate::protocol::constants::*; use crate::protocol::tls; use crate::stream::{FakeTlsReader, FakeTlsWriter, CryptoReader, CryptoWriter}; @@ -119,6 +120,23 @@ where None }; + let alpn_list = if config.censorship.alpn_enforce { + tls::extract_alpn_from_client_hello(handshake) + } else { + Vec::new() + }; + let selected_alpn = if config.censorship.alpn_enforce { + if alpn_list.iter().any(|p| p == b"h2") { + Some(b"h2".to_vec()) + } else if alpn_list.iter().any(|p| p == b"http/1.1") { + Some(b"http/1.1".to_vec()) + } else { + None + } + } else { + None + }; + let response = if let Some(cached_entry) = cached { emulator::build_emulated_server_hello( secret, @@ -126,6 +144,8 @@ where &validation.session_id, &cached_entry, rng, + selected_alpn.clone(), + config.censorship.tls_new_session_tickets, ) } else { tls::build_server_hello( @@ -134,9 +154,25 @@ where &validation.session_id, config.censorship.fake_cert_len, rng, + selected_alpn.clone(), + config.censorship.tls_new_session_tickets, ) }; + // Optional anti-fingerprint delay before sending ServerHello. + if config.censorship.server_hello_delay_max_ms > 0 { + let min = config.censorship.server_hello_delay_min_ms; + let max = config.censorship.server_hello_delay_max_ms.max(min); + let delay_ms = if max == min { + max + } else { + rand::rng().random_range(min..=max) + }; + if delay_ms > 0 { + tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await; + } + } + debug!(peer = %peer, response_len = response.len(), "Sending TLS ServerHello"); if let Err(e) = writer.write_all(&response).await { diff --git a/src/tls_front/emulator.rs b/src/tls_front/emulator.rs index 2bf6872..4d3e64d 100644 --- a/src/tls_front/emulator.rs +++ b/src/tls_front/emulator.rs @@ -34,6 +34,8 @@ pub fn build_emulated_server_hello( session_id: &[u8], cached: &CachedTlsData, rng: &SecureRandom, + alpn: Option>, + new_session_tickets: u8, ) -> Vec { // --- ServerHello --- let mut extensions = Vec::new(); @@ -48,6 +50,15 @@ pub fn build_emulated_server_hello( extensions.extend_from_slice(&0x002bu16.to_be_bytes()); extensions.extend_from_slice(&(2u16).to_be_bytes()); extensions.extend_from_slice(&0x0304u16.to_be_bytes()); + if let Some(alpn_proto) = &alpn { + extensions.extend_from_slice(&0x0010u16.to_be_bytes()); + let list_len: u16 = 1 + alpn_proto.len() as u16; + let ext_len: u16 = 2 + list_len; + extensions.extend_from_slice(&ext_len.to_be_bytes()); + extensions.extend_from_slice(&list_len.to_be_bytes()); + extensions.push(alpn_proto.len() as u8); + extensions.extend_from_slice(alpn_proto); + } let extensions_len = extensions.len() as u16; @@ -118,10 +129,25 @@ pub fn build_emulated_server_hello( } // --- Combine --- - let mut response = Vec::with_capacity(server_hello.len() + change_cipher_spec.len() + app_data.len()); + // Optional NewSessionTicket mimic records (opaque ApplicationData for fingerprint). + let mut tickets = Vec::new(); + if new_session_tickets > 0 { + for _ in 0..new_session_tickets { + let ticket_len: usize = rng.range(48) + 48; + let mut rec = Vec::with_capacity(5 + ticket_len); + rec.push(TLS_RECORD_APPLICATION); + rec.extend_from_slice(&TLS_VERSION); + rec.extend_from_slice(&(ticket_len as u16).to_be_bytes()); + rec.extend_from_slice(&rng.bytes(ticket_len)); + tickets.extend_from_slice(&rec); + } + } + + let mut response = Vec::with_capacity(server_hello.len() + change_cipher_spec.len() + app_data.len() + tickets.len()); response.extend_from_slice(&server_hello); response.extend_from_slice(&change_cipher_spec); response.extend_from_slice(&app_data); + response.extend_from_slice(&tickets); // --- HMAC --- let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + response.len()); diff --git a/src/transport/middle_proxy/pool.rs b/src/transport/middle_proxy/pool.rs index 84c526f..3572671 100644 --- a/src/transport/middle_proxy/pool.rs +++ b/src/transport/middle_proxy/pool.rs @@ -567,12 +567,14 @@ impl MePool { let cancel_keepalive = cancel_keepalive_token; tokio::spawn(async move { // Per-writer jittered start to avoid phase sync. - let initial_jitter_ms = rand::rng().random_range(0..=keepalive_jitter.as_millis().max(1) as u64); + let jitter_cap_ms = keepalive_interval.as_millis() / 2; + let effective_jitter_ms = keepalive_jitter.as_millis().min(jitter_cap_ms).max(1); + let initial_jitter_ms = rand::rng().random_range(0..=effective_jitter_ms as u64); tokio::time::sleep(Duration::from_millis(initial_jitter_ms)).await; loop { tokio::select! { _ = cancel_keepalive.cancelled() => break, - _ = tokio::time::sleep(keepalive_interval + Duration::from_millis(rand::rng().random_range(0..=keepalive_jitter.as_millis() as u64))) => {} + _ = tokio::time::sleep(keepalive_interval + Duration::from_millis(rand::rng().random_range(0..=effective_jitter_ms as u64))) => {} } if tx_keepalive.send(WriterCommand::Keepalive).await.is_err() { break;