mirror of https://github.com/telemt/telemt.git
Middle-End Fixes
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
parent
c9a043d8d5
commit
83fc9d6db3
|
|
@ -118,6 +118,67 @@ pub(crate) fn default_max_client_frame() -> usize {
|
||||||
16 * 1024 * 1024
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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<String> {
|
||||||
|
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
|
// Custom deserializer helpers
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,22 @@ pub struct NetworkConfig {
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub multipath: bool,
|
pub multipath: bool,
|
||||||
|
|
||||||
|
/// STUN servers list for public IP discovery.
|
||||||
|
#[serde(default = "default_stun_servers")]
|
||||||
|
pub stun_servers: Vec<String>,
|
||||||
|
|
||||||
|
/// 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<String>,
|
||||||
|
|
||||||
|
/// Cache file path for detected public IP.
|
||||||
|
#[serde(default = "default_cache_public_ip_path")]
|
||||||
|
pub cache_public_ip_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for NetworkConfig {
|
impl Default for NetworkConfig {
|
||||||
|
|
@ -105,6 +121,10 @@ impl Default for NetworkConfig {
|
||||||
ipv6: None,
|
ipv6: None,
|
||||||
prefer: 4,
|
prefer: 4,
|
||||||
multipath: false,
|
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.
|
/// [general.links] — proxy link generation overrides.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub links: LinksConfig,
|
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<String>,
|
||||||
|
|
||||||
|
/// 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 {
|
impl Default for GeneralConfig {
|
||||||
|
|
@ -262,6 +310,13 @@ impl Default for GeneralConfig {
|
||||||
links: LinksConfig::default(),
|
links: LinksConfig::default(),
|
||||||
crypto_pending_buffer: default_crypto_pending_buffer(),
|
crypto_pending_buffer: default_crypto_pending_buffer(),
|
||||||
max_client_frame: default_max_client_frame(),
|
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).
|
/// Directory to store TLS front cache (on disk).
|
||||||
#[serde(default = "default_tls_front_dir")]
|
#[serde(default = "default_tls_front_dir")]
|
||||||
pub tls_front_dir: String,
|
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 {
|
impl Default for AntiCensorshipConfig {
|
||||||
|
|
@ -420,6 +491,10 @@ impl Default for AntiCensorshipConfig {
|
||||||
fake_cert_len: default_fake_cert_len(),
|
fake_cert_len: default_fake_cert_len(),
|
||||||
tls_emulation: false,
|
tls_emulation: false,
|
||||||
tls_front_dir: default_tls_front_dir(),
|
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(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,6 +213,9 @@ async fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
|
||||||
"Modes: classic={} secure={} tls={}",
|
"Modes: classic={} secure={} tls={}",
|
||||||
config.general.modes.classic, config.general.modes.secure, config.general.modes.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);
|
info!("TLS domain: {}", config.censorship.tls_domain);
|
||||||
if let Some(ref sock) = config.censorship.mask_unix_sock {
|
if let Some(ref sock) = config.censorship.mask_unix_sock {
|
||||||
info!("Mask: {} -> unix:{}", config.censorship.mask, sock);
|
info!("Mask: {} -> unix:{}", config.censorship.mask, sock);
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ pub const TIME_SKEW_MAX: i64 = 10 * 60; // 10 minutes after
|
||||||
mod extension_type {
|
mod extension_type {
|
||||||
pub const KEY_SHARE: u16 = 0x0033;
|
pub const KEY_SHARE: u16 = 0x0033;
|
||||||
pub const SUPPORTED_VERSIONS: u16 = 0x002b;
|
pub const SUPPORTED_VERSIONS: u16 = 0x002b;
|
||||||
|
pub const ALPN: u16 = 0x0010;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TLS Cipher Suites
|
/// TLS Cipher Suites
|
||||||
|
|
@ -62,6 +63,7 @@ pub struct TlsValidation {
|
||||||
// ============= TLS Extension Builder =============
|
// ============= TLS Extension Builder =============
|
||||||
|
|
||||||
/// Builder for TLS extensions with correct length calculation
|
/// Builder for TLS extensions with correct length calculation
|
||||||
|
#[derive(Clone)]
|
||||||
struct TlsExtensionBuilder {
|
struct TlsExtensionBuilder {
|
||||||
extensions: Vec<u8>,
|
extensions: Vec<u8>,
|
||||||
}
|
}
|
||||||
|
|
@ -109,6 +111,27 @@ impl TlsExtensionBuilder {
|
||||||
self
|
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
|
/// Build final extensions with length prefix
|
||||||
fn build(self) -> Vec<u8> {
|
fn build(self) -> Vec<u8> {
|
||||||
let mut result = Vec::with_capacity(2 + self.extensions.len());
|
let mut result = Vec::with_capacity(2 + self.extensions.len());
|
||||||
|
|
@ -144,6 +167,8 @@ struct ServerHelloBuilder {
|
||||||
compression: u8,
|
compression: u8,
|
||||||
/// Extensions
|
/// Extensions
|
||||||
extensions: TlsExtensionBuilder,
|
extensions: TlsExtensionBuilder,
|
||||||
|
/// Selected ALPN protocol (if any)
|
||||||
|
alpn: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServerHelloBuilder {
|
impl ServerHelloBuilder {
|
||||||
|
|
@ -154,6 +179,7 @@ impl ServerHelloBuilder {
|
||||||
cipher_suite: cipher_suite::TLS_AES_128_GCM_SHA256,
|
cipher_suite: cipher_suite::TLS_AES_128_GCM_SHA256,
|
||||||
compression: 0x00,
|
compression: 0x00,
|
||||||
extensions: TlsExtensionBuilder::new(),
|
extensions: TlsExtensionBuilder::new(),
|
||||||
|
alpn: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -168,9 +194,18 @@ impl ServerHelloBuilder {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn with_alpn(mut self, proto: Option<Vec<u8>>) -> Self {
|
||||||
|
self.alpn = proto;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Build ServerHello message (without record header)
|
/// Build ServerHello message (without record header)
|
||||||
fn build_message(&self) -> Vec<u8> {
|
fn build_message(&self) -> Vec<u8> {
|
||||||
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;
|
let extensions_len = extensions.len() as u16;
|
||||||
|
|
||||||
// Calculate total length
|
// Calculate total length
|
||||||
|
|
@ -350,6 +385,8 @@ pub fn build_server_hello(
|
||||||
session_id: &[u8],
|
session_id: &[u8],
|
||||||
fake_cert_len: usize,
|
fake_cert_len: usize,
|
||||||
rng: &SecureRandom,
|
rng: &SecureRandom,
|
||||||
|
alpn: Option<Vec<u8>>,
|
||||||
|
new_session_tickets: u8,
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
const MIN_APP_DATA: usize = 64;
|
const MIN_APP_DATA: usize = 64;
|
||||||
const MAX_APP_DATA: usize = 16640; // RFC 8446 §5.2 upper bound
|
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())
|
let server_hello = ServerHelloBuilder::new(session_id.to_vec())
|
||||||
.with_x25519_key(&x25519_key)
|
.with_x25519_key(&x25519_key)
|
||||||
.with_tls13_version()
|
.with_tls13_version()
|
||||||
|
.with_alpn(alpn)
|
||||||
.build_record();
|
.build_record();
|
||||||
|
|
||||||
// Build Change Cipher Spec record
|
// Build Change Cipher Spec record
|
||||||
|
|
@ -380,13 +418,31 @@ pub fn build_server_hello(
|
||||||
// deterministic DPI fingerprints (fixed inner content type markers).
|
// deterministic DPI fingerprints (fixed inner content type markers).
|
||||||
app_data_record.extend_from_slice(&fake_cert);
|
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
|
// Combine all records
|
||||||
let mut response = Vec::with_capacity(
|
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::<usize>()
|
||||||
);
|
);
|
||||||
response.extend_from_slice(&server_hello);
|
response.extend_from_slice(&server_hello);
|
||||||
response.extend_from_slice(&change_cipher_spec);
|
response.extend_from_slice(&change_cipher_spec);
|
||||||
response.extend_from_slice(&app_data_record);
|
response.extend_from_slice(&app_data_record);
|
||||||
|
for t in &tickets {
|
||||||
|
response.extend_from_slice(t);
|
||||||
|
}
|
||||||
|
|
||||||
// Compute HMAC for the response
|
// Compute HMAC for the response
|
||||||
let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + response.len());
|
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<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract ALPN protocol list from TLS ClientHello.
|
/// Extract ALPN protocol list from ClientHello, return in offered order.
|
||||||
pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Option<Vec<String>> {
|
pub fn extract_alpn_from_client_hello(handshake: &[u8]) -> Vec<Vec<u8>> {
|
||||||
if handshake.len() < 43 || handshake[0] != TLS_RECORD_HANDSHAKE {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut pos = 5; // after record header
|
let mut pos = 5; // after record header
|
||||||
if handshake.get(pos).copied()? != 0x01 {
|
if handshake.get(pos) != Some(&0x01) {
|
||||||
return None; // not ClientHello
|
return Vec::new();
|
||||||
}
|
}
|
||||||
|
pos += 4; // type + len
|
||||||
// Handshake length bytes
|
pos += 2 + 32; // version + random
|
||||||
pos += 4; // type + len (3)
|
if pos >= handshake.len() { return Vec::new(); }
|
||||||
|
let session_id_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||||
// version (2) + random (32)
|
|
||||||
pos += 2 + 32;
|
|
||||||
if pos + 1 > handshake.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let session_id_len = *handshake.get(pos)? as usize;
|
|
||||||
pos += 1 + session_id_len;
|
pos += 1 + session_id_len;
|
||||||
if pos + 2 > handshake.len() {
|
if pos + 2 > handshake.len() { return Vec::new(); }
|
||||||
return None;
|
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 cipher_suites_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
let comp_len = *handshake.get(pos).unwrap_or(&0) as usize;
|
||||||
pos += 2 + cipher_suites_len;
|
|
||||||
if pos + 1 > handshake.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let comp_len = *handshake.get(pos)? as usize;
|
|
||||||
pos += 1 + comp_len;
|
pos += 1 + comp_len;
|
||||||
if pos + 2 > handshake.len() {
|
if pos + 2 > handshake.len() { return Vec::new(); }
|
||||||
return None;
|
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos+1]]) as usize;
|
||||||
}
|
|
||||||
|
|
||||||
let ext_len = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]) as usize;
|
|
||||||
pos += 2;
|
pos += 2;
|
||||||
let ext_end = pos + ext_len;
|
let ext_end = pos + ext_len;
|
||||||
if ext_end > handshake.len() {
|
if ext_end > handshake.len() { return Vec::new(); }
|
||||||
return None;
|
let mut out = Vec::new();
|
||||||
}
|
|
||||||
|
|
||||||
while pos + 4 <= ext_end {
|
while pos + 4 <= ext_end {
|
||||||
let etype = u16::from_be_bytes([handshake[pos], handshake[pos + 1]]);
|
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 elen = u16::from_be_bytes([handshake[pos+2], handshake[pos+3]]) as usize;
|
||||||
pos += 4;
|
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;
|
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;
|
pos += elen;
|
||||||
}
|
}
|
||||||
|
out
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// Check if bytes look like a TLS ClientHello
|
/// Check if bytes look like a TLS ClientHello
|
||||||
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
pub fn is_tls_handshake(first_bytes: &[u8]) -> bool {
|
||||||
if first_bytes.len() < 3 {
|
if first_bytes.len() < 3 {
|
||||||
|
|
@ -737,7 +761,7 @@ mod tests {
|
||||||
let session_id = vec![0xAA; 32];
|
let session_id = vec![0xAA; 32];
|
||||||
|
|
||||||
let rng = SecureRandom::new();
|
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
|
// Should have at least 3 records
|
||||||
assert!(response.len() > 100);
|
assert!(response.len() > 100);
|
||||||
|
|
@ -770,8 +794,8 @@ mod tests {
|
||||||
let session_id = vec![0xAA; 32];
|
let session_id = vec![0xAA; 32];
|
||||||
|
|
||||||
let rng = SecureRandom::new();
|
let rng = SecureRandom::new();
|
||||||
let response1 = 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);
|
let response2 = build_server_hello(secret, &client_digest, &session_id, 1024, &rng, None, 0);
|
||||||
|
|
||||||
// Digest position should have non-zero data
|
// Digest position should have non-zero data
|
||||||
let digest1 = &response1[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN];
|
let digest1 = &response1[TLS_DIGEST_POS..TLS_DIGEST_POS + TLS_DIGEST_LEN];
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ use tracing::{debug, warn, trace, info};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::crypto::{sha256, AesCtr, SecureRandom};
|
use crate::crypto::{sha256, AesCtr, SecureRandom};
|
||||||
|
use rand::Rng;
|
||||||
use crate::protocol::constants::*;
|
use crate::protocol::constants::*;
|
||||||
use crate::protocol::tls;
|
use crate::protocol::tls;
|
||||||
use crate::stream::{FakeTlsReader, FakeTlsWriter, CryptoReader, CryptoWriter};
|
use crate::stream::{FakeTlsReader, FakeTlsWriter, CryptoReader, CryptoWriter};
|
||||||
|
|
@ -119,6 +120,23 @@ where
|
||||||
None
|
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 {
|
let response = if let Some(cached_entry) = cached {
|
||||||
emulator::build_emulated_server_hello(
|
emulator::build_emulated_server_hello(
|
||||||
secret,
|
secret,
|
||||||
|
|
@ -126,6 +144,8 @@ where
|
||||||
&validation.session_id,
|
&validation.session_id,
|
||||||
&cached_entry,
|
&cached_entry,
|
||||||
rng,
|
rng,
|
||||||
|
selected_alpn.clone(),
|
||||||
|
config.censorship.tls_new_session_tickets,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
tls::build_server_hello(
|
tls::build_server_hello(
|
||||||
|
|
@ -134,9 +154,25 @@ where
|
||||||
&validation.session_id,
|
&validation.session_id,
|
||||||
config.censorship.fake_cert_len,
|
config.censorship.fake_cert_len,
|
||||||
rng,
|
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");
|
debug!(peer = %peer, response_len = response.len(), "Sending TLS ServerHello");
|
||||||
|
|
||||||
if let Err(e) = writer.write_all(&response).await {
|
if let Err(e) = writer.write_all(&response).await {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,8 @@ pub fn build_emulated_server_hello(
|
||||||
session_id: &[u8],
|
session_id: &[u8],
|
||||||
cached: &CachedTlsData,
|
cached: &CachedTlsData,
|
||||||
rng: &SecureRandom,
|
rng: &SecureRandom,
|
||||||
|
alpn: Option<Vec<u8>>,
|
||||||
|
new_session_tickets: u8,
|
||||||
) -> Vec<u8> {
|
) -> Vec<u8> {
|
||||||
// --- ServerHello ---
|
// --- ServerHello ---
|
||||||
let mut extensions = Vec::new();
|
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(&0x002bu16.to_be_bytes());
|
||||||
extensions.extend_from_slice(&(2u16).to_be_bytes());
|
extensions.extend_from_slice(&(2u16).to_be_bytes());
|
||||||
extensions.extend_from_slice(&0x0304u16.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;
|
let extensions_len = extensions.len() as u16;
|
||||||
|
|
||||||
|
|
@ -118,10 +129,25 @@ pub fn build_emulated_server_hello(
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Combine ---
|
// --- 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(&server_hello);
|
||||||
response.extend_from_slice(&change_cipher_spec);
|
response.extend_from_slice(&change_cipher_spec);
|
||||||
response.extend_from_slice(&app_data);
|
response.extend_from_slice(&app_data);
|
||||||
|
response.extend_from_slice(&tickets);
|
||||||
|
|
||||||
// --- HMAC ---
|
// --- HMAC ---
|
||||||
let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + response.len());
|
let mut hmac_input = Vec::with_capacity(TLS_DIGEST_LEN + response.len());
|
||||||
|
|
|
||||||
|
|
@ -567,12 +567,14 @@ impl MePool {
|
||||||
let cancel_keepalive = cancel_keepalive_token;
|
let cancel_keepalive = cancel_keepalive_token;
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
// Per-writer jittered start to avoid phase sync.
|
// 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;
|
tokio::time::sleep(Duration::from_millis(initial_jitter_ms)).await;
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = cancel_keepalive.cancelled() => break,
|
_ = 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() {
|
if tx_keepalive.send(WriterCommand::Keepalive).await.is_err() {
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue