From c9a043d8d5cfc1c13e867b5da8d48a8df84b101b Mon Sep 17 00:00:00 2001 From: Alexey <247128645+axkurcom@users.noreply.github.com> Date: Sat, 21 Feb 2026 02:15:10 +0300 Subject: [PATCH] ME Frame too large Fixes Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com> --- src/config/defaults.rs | 8 ++ src/config/types.rs | 11 +++ src/protocol/constants.rs | 17 +++- src/protocol/tls.rs | 10 +-- src/proxy/direct_relay.rs | 3 +- src/proxy/handshake.rs | 3 +- src/proxy/middle_relay.rs | 149 +++++++++++++++++++++++------------- src/stream/crypto_stream.rs | 38 +++++---- src/stream/frame_stream.rs | 6 +- src/stream/mod.rs | 2 +- src/stream/tls_stream.rs | 15 ++-- src/tls_front/cache.rs | 22 +++++- 12 files changed, 189 insertions(+), 95 deletions(-) diff --git a/src/config/defaults.rs b/src/config/defaults.rs index f9d878a..b68c120 100644 --- a/src/config/defaults.rs +++ b/src/config/defaults.rs @@ -110,6 +110,14 @@ pub(crate) fn default_reconnect_backoff_cap_ms() -> u64 { 30_000 } +pub(crate) fn default_crypto_pending_buffer() -> usize { + 256 * 1024 +} + +pub(crate) fn default_max_client_frame() -> usize { + 16 * 1024 * 1024 +} + // Custom deserializer helpers #[derive(Deserialize)] diff --git a/src/config/types.rs b/src/config/types.rs index 6228cb8..41b7a3f 100644 --- a/src/config/types.rs +++ b/src/config/types.rs @@ -172,6 +172,15 @@ pub struct GeneralConfig { #[serde(default = "default_true")] pub me_keepalive_payload_random: bool, + /// Max pending ciphertext buffer per client writer (bytes). + /// Controls FakeTLS backpressure vs throughput. + #[serde(default = "default_crypto_pending_buffer")] + pub crypto_pending_buffer: usize, + + /// Maximum allowed client MTProto frame size (bytes). + #[serde(default = "default_max_client_frame")] + pub max_client_frame: usize, + /// Enable staggered warmup of extra ME writers. #[serde(default = "default_true")] pub me_warmup_stagger_enabled: bool, @@ -251,6 +260,8 @@ impl Default for GeneralConfig { log_level: LogLevel::Normal, disable_colors: false, links: LinksConfig::default(), + crypto_pending_buffer: default_crypto_pending_buffer(), + max_client_frame: default_max_client_frame(), } } } diff --git a/src/protocol/constants.rs b/src/protocol/constants.rs index 86cd2bd..826f2b2 100644 --- a/src/protocol/constants.rs +++ b/src/protocol/constants.rs @@ -1,6 +1,8 @@ //! Protocol constants and datacenter addresses use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; + +use crate::crypto::SecureRandom; use std::sync::LazyLock; // ============= Telegram Datacenters ============= @@ -151,7 +153,18 @@ pub const TLS_RECORD_ALERT: u8 = 0x15; /// Maximum TLS record size pub const MAX_TLS_RECORD_SIZE: usize = 16384; /// Maximum TLS chunk size (with overhead) -pub const MAX_TLS_CHUNK_SIZE: usize = 16384 + 24; +/// RFC 8446 §5.2 allows up to 16384 + 256 bytes of ciphertext +pub const MAX_TLS_CHUNK_SIZE: usize = 16384 + 256; + +/// Generate padding length for Secure Intermediate protocol. +/// Total (data + padding) must not be divisible by 4 per MTProto spec. +pub fn secure_padding_len(data_len: usize, rng: &SecureRandom) -> usize { + if data_len % 4 == 0 { + (rng.range(3) + 1) as usize // 1-3 + } else { + rng.range(4) as usize // 0-3 + } +} // ============= Timeouts ============= @@ -319,4 +332,4 @@ mod tests { assert_eq!(TG_DATACENTERS_V4.len(), 5); assert_eq!(TG_DATACENTERS_V6.len(), 5); } -} \ No newline at end of file +} diff --git a/src/protocol/tls.rs b/src/protocol/tls.rs index d7afdee..5f890a5 100644 --- a/src/protocol/tls.rs +++ b/src/protocol/tls.rs @@ -376,13 +376,9 @@ pub fn build_server_hello( app_data_record.push(TLS_RECORD_APPLICATION); app_data_record.extend_from_slice(&TLS_VERSION); app_data_record.extend_from_slice(&(fake_cert_len as u16).to_be_bytes()); - if fake_cert_len > 17 { - app_data_record.extend_from_slice(&fake_cert[..fake_cert_len - 17]); - app_data_record.push(0x16); // inner content type marker - app_data_record.extend_from_slice(&rng.bytes(16)); // AEAD-like tag mimic - } else { - app_data_record.extend_from_slice(&fake_cert); - } + // Fill ApplicationData with fully random bytes of desired length to avoid + // deterministic DPI fingerprints (fixed inner content type markers). + app_data_record.extend_from_slice(&fake_cert); // Combine all records let mut response = Vec::with_capacity( diff --git a/src/proxy/direct_relay.rs b/src/proxy/direct_relay.rs index 7b1ac1b..630937b 100644 --- a/src/proxy/direct_relay.rs +++ b/src/proxy/direct_relay.rs @@ -178,8 +178,9 @@ async fn do_tg_handshake_static( let (read_half, write_half) = stream.into_split(); + let max_pending = config.general.crypto_pending_buffer; Ok(( CryptoReader::new(read_half, tg_decryptor), - CryptoWriter::new(write_half, tg_encryptor), + CryptoWriter::new(write_half, tg_encryptor, max_pending), )) } diff --git a/src/proxy/handshake.rs b/src/proxy/handshake.rs index 8b61112..685d999 100644 --- a/src/proxy/handshake.rs +++ b/src/proxy/handshake.rs @@ -264,9 +264,10 @@ where "MTProto handshake successful" ); + let max_pending = config.general.crypto_pending_buffer; return HandshakeResult::Success(( CryptoReader::new(reader, decryptor), - CryptoWriter::new(writer, encryptor), + CryptoWriter::new(writer, encryptor, max_pending), success, )); } diff --git a/src/proxy/middle_relay.rs b/src/proxy/middle_relay.rs index 09dd532..2a17e7e 100644 --- a/src/proxy/middle_relay.rs +++ b/src/proxy/middle_relay.rs @@ -2,12 +2,13 @@ use std::net::SocketAddr; use std::sync::Arc; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use tracing::{debug, info, trace}; +use tokio::sync::oneshot; +use tracing::{debug, info, trace, warn}; use crate::config::ProxyConfig; use crate::crypto::SecureRandom; use crate::error::{ProxyError, Result}; -use crate::protocol::constants::*; +use crate::protocol::constants::{*, secure_padding_len}; use crate::proxy::handshake::HandshakeSuccess; use crate::stats::Stats; use crate::stream::{BufferPool, CryptoReader, CryptoWriter}; @@ -15,11 +16,11 @@ use crate::transport::middle_proxy::{MePool, MeResponse, proto_flags_for_tag}; pub(crate) async fn handle_via_middle_proxy( mut crypto_reader: CryptoReader, - mut crypto_writer: CryptoWriter, + crypto_writer: CryptoWriter, success: HandshakeSuccess, me_pool: Arc, stats: Arc, - _config: Arc, + config: Arc, _buffer_pool: Arc, local_addr: SocketAddr, rng: Arc, @@ -41,7 +42,7 @@ where "Routing via Middle-End" ); - let (conn_id, mut me_rx) = me_pool.registry().register().await; + let (conn_id, me_rx) = me_pool.registry().register().await; stats.increment_user_connects(&user); stats.increment_user_curr_connects(&user); @@ -56,59 +57,90 @@ where let translated_local_addr = me_pool.translate_our_addr(local_addr); - let result: Result<()> = loop { - tokio::select! { - client_frame = read_client_payload(&mut crypto_reader, proto_tag) => { - match client_frame { - Ok(Some((payload, quickack))) => { - trace!(conn_id, bytes = payload.len(), "C->ME frame"); - stats.add_user_octets_from(&user, payload.len() as u64); - let mut flags = proto_flags; - if quickack { - flags |= RPC_FLAG_QUICKACK; + let frame_limit = config.general.max_client_frame; + + let (stop_tx, mut stop_rx) = oneshot::channel::<()>(); + let mut me_rx_task = me_rx; + let stats_clone = stats.clone(); + let rng_clone = rng.clone(); + let user_clone = user.clone(); + let me_writer = tokio::spawn(async move { + let mut writer = crypto_writer; + loop { + tokio::select! { + msg = me_rx_task.recv() => { + match msg { + Some(MeResponse::Data { flags, data }) => { + trace!(conn_id, bytes = data.len(), flags, "ME->C data"); + stats_clone.add_user_octets_to(&user_clone, data.len() as u64); + write_client_payload(&mut writer, proto_tag, flags, &data, rng_clone.as_ref()).await?; } - if payload.len() >= 8 && payload[..8].iter().all(|b| *b == 0) { - flags |= RPC_FLAG_NOT_ENCRYPTED; + Some(MeResponse::Ack(confirm)) => { + trace!(conn_id, confirm, "ME->C quickack"); + write_client_ack(&mut writer, proto_tag, confirm).await?; + } + Some(MeResponse::Close) => { + debug!(conn_id, "ME sent close"); + return Ok(()); + } + None => { + debug!(conn_id, "ME channel closed"); + return Err(ProxyError::Proxy("ME connection lost".into())); } - me_pool.send_proxy_req( - conn_id, - success.dc_idx, - peer, - translated_local_addr, - &payload, - flags, - ).await?; } - Ok(None) => { - debug!(conn_id, "Client EOF"); - let _ = me_pool.send_close(conn_id).await; - break Ok(()); - } - Err(e) => break Err(e), } - } - me_msg = me_rx.recv() => { - match me_msg { - Some(MeResponse::Data { flags, data }) => { - trace!(conn_id, bytes = data.len(), flags, "ME->C data"); - stats.add_user_octets_to(&user, data.len() as u64); - write_client_payload(&mut crypto_writer, proto_tag, flags, &data, rng.as_ref()).await?; - } - Some(MeResponse::Ack(confirm)) => { - trace!(conn_id, confirm, "ME->C quickack"); - write_client_ack(&mut crypto_writer, proto_tag, confirm).await?; - } - Some(MeResponse::Close) => { - debug!(conn_id, "ME sent close"); - break Ok(()); - } - None => { - debug!(conn_id, "ME channel closed"); - break Err(ProxyError::Proxy("ME connection lost".into())); - } + _ = &mut stop_rx => { + debug!(conn_id, "ME writer stop signal"); + return Ok(()); } } } + }); + + let mut main_result: Result<()> = Ok(()); + loop { + match read_client_payload(&mut crypto_reader, proto_tag, frame_limit, &user).await { + Ok(Some((payload, quickack))) => { + trace!(conn_id, bytes = payload.len(), "C->ME frame"); + stats.add_user_octets_from(&user, payload.len() as u64); + let mut flags = proto_flags; + if quickack { + flags |= RPC_FLAG_QUICKACK; + } + if payload.len() >= 8 && payload[..8].iter().all(|b| *b == 0) { + flags |= RPC_FLAG_NOT_ENCRYPTED; + } + if let Err(e) = me_pool.send_proxy_req( + conn_id, + success.dc_idx, + peer, + translated_local_addr, + &payload, + flags, + ).await { + main_result = Err(e); + break; + } + } + Ok(None) => { + debug!(conn_id, "Client EOF"); + let _ = me_pool.send_close(conn_id).await; + break; + } + Err(e) => { + main_result = Err(e); + break; + } + } + } + + let _ = stop_tx.send(()); + let writer_result = me_writer.await.unwrap_or_else(|e| Err(ProxyError::Proxy(format!("ME writer join error: {e}")))); + + let result = match (main_result, writer_result) { + (Ok(()), Ok(())) => Ok(()), + (Err(e), _) => Err(e), + (_, Err(e)) => Err(e), }; debug!(user = %user, conn_id, "ME relay cleanup"); @@ -120,6 +152,8 @@ where async fn read_client_payload( client_reader: &mut CryptoReader, proto_tag: ProtoTag, + max_frame: usize, + user: &str, ) -> Result, bool)>> where R: AsyncRead + Unpin + Send + 'static, @@ -162,8 +196,15 @@ where } }; - if len > 16 * 1024 * 1024 { - return Err(ProxyError::Proxy(format!("Frame too large: {len}"))); + if len > max_frame { + warn!( + user = %user, + raw_len = len, + raw_len_hex = format_args!("0x{:08x}", len), + proto = ?proto_tag, + "Frame too large — possible crypto desync or TLS record error" + ); + return Err(ProxyError::Proxy(format!("Frame too large: {len} (max {max_frame})"))); } let mut payload = vec![0u8; len]; @@ -237,7 +278,7 @@ where } ProtoTag::Intermediate | ProtoTag::Secure => { let padding_len = if proto_tag == ProtoTag::Secure { - (rng.bytes(1)[0] % 4) as usize + secure_padding_len(data.len(), rng) } else { 0 }; diff --git a/src/stream/crypto_stream.rs b/src/stream/crypto_stream.rs index 4705fe6..ebb6f43 100644 --- a/src/stream/crypto_stream.rs +++ b/src/stream/crypto_stream.rs @@ -34,7 +34,7 @@ //! └────────────────────────────────────────┘ //! //! Backpressure -//! - pending ciphertext buffer is bounded (MAX_PENDING_WRITE) +//! - pending ciphertext buffer is bounded (configurable per connection) //! - pending is full and upstream is pending //! -> poll_write returns Poll::Pending //! -> do not accept any plaintext @@ -62,10 +62,9 @@ use super::state::{StreamState, YieldBuffer}; // ============= Constants ============= -/// Maximum size for pending ciphertext buffer (bounded backpressure). -/// Reduced to 64KB to prevent bufferbloat on mobile networks. -/// 512KB was causing high latency on 3G/LTE connections. -const MAX_PENDING_WRITE: usize = 64 * 1024; +/// Default size for pending ciphertext buffer (bounded backpressure). +/// Actual limit is supplied at runtime from configuration. +const DEFAULT_MAX_PENDING_WRITE: usize = 64 * 1024; /// Default read buffer capacity (reader mostly decrypts in-place into caller buffer). const DEFAULT_READ_CAPACITY: usize = 16 * 1024; @@ -427,15 +426,22 @@ pub struct CryptoWriter { encryptor: AesCtr, state: CryptoWriterState, scratch: BytesMut, + max_pending_write: usize, } impl CryptoWriter { - pub fn new(upstream: W, encryptor: AesCtr) -> Self { + pub fn new(upstream: W, encryptor: AesCtr, max_pending_write: usize) -> Self { + let max_pending = if max_pending_write == 0 { + DEFAULT_MAX_PENDING_WRITE + } else { + max_pending_write + }; Self { upstream, encryptor, state: CryptoWriterState::Idle, scratch: BytesMut::with_capacity(16 * 1024), + max_pending_write: max_pending.max(4 * 1024), } } @@ -484,10 +490,10 @@ impl CryptoWriter { } /// Ensure we are in Flushing state and return mutable pending buffer. - fn ensure_pending<'a>(state: &'a mut CryptoWriterState) -> &'a mut PendingCiphertext { + fn ensure_pending<'a>(state: &'a mut CryptoWriterState, max_pending: usize) -> &'a mut PendingCiphertext { if matches!(state, CryptoWriterState::Idle) { *state = CryptoWriterState::Flushing { - pending: PendingCiphertext::new(MAX_PENDING_WRITE), + pending: PendingCiphertext::new(max_pending), }; } @@ -498,14 +504,14 @@ impl CryptoWriter { } /// Select how many plaintext bytes can be accepted in buffering path - fn select_to_accept_for_buffering(state: &CryptoWriterState, buf_len: usize) -> usize { + fn select_to_accept_for_buffering(state: &CryptoWriterState, buf_len: usize, max_pending: usize) -> usize { if buf_len == 0 { return 0; } match state { CryptoWriterState::Flushing { pending } => buf_len.min(pending.remaining_capacity()), - CryptoWriterState::Idle => buf_len.min(MAX_PENDING_WRITE), + CryptoWriterState::Idle => buf_len.min(max_pending), CryptoWriterState::Poisoned { .. } => 0, } } @@ -603,7 +609,7 @@ impl AsyncWrite for CryptoWriter { Poll::Pending => { // Upstream blocked. Apply ideal backpressure let to_accept = - Self::select_to_accept_for_buffering(&this.state, buf.len()); + Self::select_to_accept_for_buffering(&this.state, buf.len(), this.max_pending_write); if to_accept == 0 { trace!( @@ -618,7 +624,7 @@ impl AsyncWrite for CryptoWriter { // Disjoint borrows let encryptor = &mut this.encryptor; - let pending = Self::ensure_pending(&mut this.state); + let pending = Self::ensure_pending(&mut this.state, this.max_pending_write); if let Err(e) = pending.push_encrypted(encryptor, plaintext) { if e.kind() == ErrorKind::WouldBlock { @@ -635,7 +641,7 @@ impl AsyncWrite for CryptoWriter { // 2) Fast path: pending empty -> write-through debug_assert!(matches!(this.state, CryptoWriterState::Idle)); - let to_accept = buf.len().min(MAX_PENDING_WRITE); + let to_accept = buf.len().min(this.max_pending_write); let plaintext = &buf[..to_accept]; Self::encrypt_into_scratch(&mut this.encryptor, &mut this.scratch, plaintext); @@ -645,7 +651,7 @@ impl AsyncWrite for CryptoWriter { // Upstream blocked: buffer FULL ciphertext for accepted bytes. let ciphertext = std::mem::take(&mut this.scratch); - let pending = Self::ensure_pending(&mut this.state); + let pending = Self::ensure_pending(&mut this.state, this.max_pending_write); pending.replace_with(ciphertext); Poll::Ready(Ok(to_accept)) @@ -672,7 +678,7 @@ impl AsyncWrite for CryptoWriter { let remainder = this.scratch.split_off(n); this.scratch.clear(); - let pending = Self::ensure_pending(&mut this.state); + let pending = Self::ensure_pending(&mut this.state, this.max_pending_write); pending.replace_with(remainder); Poll::Ready(Ok(to_accept)) @@ -767,4 +773,4 @@ impl AsyncWrite for PassthroughStream { fn poll_shutdown(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.inner).poll_shutdown(cx) } -} \ No newline at end of file +} diff --git a/src/stream/frame_stream.rs b/src/stream/frame_stream.rs index fd8c1b4..1ea6d1b 100644 --- a/src/stream/frame_stream.rs +++ b/src/stream/frame_stream.rs @@ -267,8 +267,8 @@ impl SecureIntermediateFrameWriter { return Ok(()); } - // Add random padding (0-3 bytes) - let padding_len = self.rng.range(4); + // Add padding so total length is never divisible by 4 (MTProto Secure) + let padding_len = secure_padding_len(data.len(), &self.rng); let padding = self.rng.bytes(padding_len); let total_len = data.len() + padding_len; @@ -585,4 +585,4 @@ mod tests { let (received, _) = reader.read_frame().await.unwrap(); assert_eq!(&received[..], &data[..]); } -} \ No newline at end of file +} diff --git a/src/stream/mod.rs b/src/stream/mod.rs index a86b56f..ea30e5e 100644 --- a/src/stream/mod.rs +++ b/src/stream/mod.rs @@ -40,4 +40,4 @@ pub use frame_stream::{ SecureIntermediateFrameReader, SecureIntermediateFrameWriter, MtprotoFrameReader, MtprotoFrameWriter, FrameReaderKind, FrameWriterKind, -}; \ No newline at end of file +}; diff --git a/src/stream/tls_stream.rs b/src/stream/tls_stream.rs index 6a3c1d6..edf970d 100644 --- a/src/stream/tls_stream.rs +++ b/src/stream/tls_stream.rs @@ -25,7 +25,8 @@ //! - However, the on-the-wire record length can exceed 16384 because TLS 1.3 //! uses AEAD and can include tag/overhead/padding. //! - Telegram FakeTLS clients (notably iOS) may send Application Data records -//! with length up to 16384 + 24 bytes. We accept that as MAX_TLS_CHUNK_SIZE. +//! with length up to 16384 + 256 bytes (RFC 8446 §5.2). We accept that as +//! MAX_TLS_CHUNK_SIZE. //! //! If you reject those (e.g. validate length <= 16384), you will see errors like: //! "TLS record too large: 16408 bytes" @@ -52,9 +53,8 @@ use super::state::{StreamState, HeaderBuffer, YieldBuffer, WriteBuffer}; const TLS_HEADER_SIZE: usize = 5; /// Maximum TLS fragment size we emit for Application Data. -/// Real TLS 1.3 ciphertexts often add ~16-24 bytes AEAD overhead, so to mimic -/// on-the-wire record sizes we allow up to 16384 + 24 bytes of plaintext. -const MAX_TLS_PAYLOAD: usize = 16384 + 24; +/// Real TLS 1.3 allows up to 16384 + 256 bytes of ciphertext (incl. tag). +const MAX_TLS_PAYLOAD: usize = 16384 + 256; /// Maximum pending write buffer for one record remainder. /// Note: we never queue unlimited amount of data here; state holds at most one record. @@ -91,7 +91,7 @@ impl TlsRecordHeader { /// - We accept TLS 1.0 header version for ClientHello-like records (0x03 0x01), /// and TLS 1.2/1.3 style version bytes for the rest (we use TLS_VERSION = 0x03 0x03). /// - For Application Data, Telegram FakeTLS may send payload length up to - /// MAX_TLS_CHUNK_SIZE (16384 + 24). + /// MAX_TLS_CHUNK_SIZE (16384 + 256). /// - For other record types we keep stricter bounds to avoid memory abuse. fn validate(&self) -> Result<()> { // Version: accept TLS 1.0 header (ClientHello quirk) and TLS_VERSION (0x0303). @@ -105,7 +105,7 @@ impl TlsRecordHeader { let len = self.length as usize; // Length checks depend on record type. - // Telegram FakeTLS: ApplicationData length may be 16384 + 24. + // Telegram FakeTLS: ApplicationData length may be 16384 + 256. match self.record_type { TLS_RECORD_APPLICATION => { if len > MAX_TLS_CHUNK_SIZE { @@ -755,9 +755,6 @@ impl AsyncWrite for FakeTlsWriter { payload_size: chunk_size, }; - // Wake to retry flushing soon. - cx.waker().wake_by_ref(); - Poll::Ready(Ok(chunk_size)) } } diff --git a/src/tls_front/cache.rs b/src/tls_front/cache.rs index dccdf2a..103c2b1 100644 --- a/src/tls_front/cache.rs +++ b/src/tls_front/cache.rs @@ -72,7 +72,27 @@ impl TlsFrontCache { continue; } if let Ok(data) = tokio::fs::read(entry.path()).await { - if let Ok(cached) = serde_json::from_slice::(&data) { + if let Ok(mut cached) = serde_json::from_slice::(&data) { + if cached.domain.is_empty() + || cached.domain.len() > 255 + || !cached.domain.chars().all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') + { + warn!(file = %name, "Skipping TLS cache entry with invalid domain"); + continue; + } + // fetched_at is skipped during deserialization; approximate with file mtime if available. + if let Ok(meta) = entry.metadata().await { + if let Ok(modified) = meta.modified() { + cached.fetched_at = modified; + } + } + // Drop entries older than 72h + if let Ok(age) = cached.fetched_at.elapsed() { + if age > Duration::from_secs(72 * 3600) { + warn!(domain = %cached.domain, "Skipping stale TLS cache entry (>72h)"); + continue; + } + } let domain = cached.domain.clone(); self.set(&domain, cached).await; loaded += 1;