mirror of https://github.com/telemt/telemt.git
ME Frame too large Fixes
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
This commit is contained in:
parent
7da062e448
commit
c9a043d8d5
|
|
@ -110,6 +110,14 @@ pub(crate) fn default_reconnect_backoff_cap_ms() -> u64 {
|
||||||
30_000
|
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
|
// Custom deserializer helpers
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
|
||||||
|
|
@ -172,6 +172,15 @@ pub struct GeneralConfig {
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub me_keepalive_payload_random: bool,
|
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.
|
/// Enable staggered warmup of extra ME writers.
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
pub me_warmup_stagger_enabled: bool,
|
pub me_warmup_stagger_enabled: bool,
|
||||||
|
|
@ -251,6 +260,8 @@ impl Default for GeneralConfig {
|
||||||
log_level: LogLevel::Normal,
|
log_level: LogLevel::Normal,
|
||||||
disable_colors: false,
|
disable_colors: false,
|
||||||
links: LinksConfig::default(),
|
links: LinksConfig::default(),
|
||||||
|
crypto_pending_buffer: default_crypto_pending_buffer(),
|
||||||
|
max_client_frame: default_max_client_frame(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
//! Protocol constants and datacenter addresses
|
//! Protocol constants and datacenter addresses
|
||||||
|
|
||||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
|
||||||
|
|
||||||
|
use crate::crypto::SecureRandom;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
// ============= Telegram Datacenters =============
|
// ============= Telegram Datacenters =============
|
||||||
|
|
@ -151,7 +153,18 @@ pub const TLS_RECORD_ALERT: u8 = 0x15;
|
||||||
/// Maximum TLS record size
|
/// Maximum TLS record size
|
||||||
pub const MAX_TLS_RECORD_SIZE: usize = 16384;
|
pub const MAX_TLS_RECORD_SIZE: usize = 16384;
|
||||||
/// Maximum TLS chunk size (with overhead)
|
/// 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 =============
|
// ============= Timeouts =============
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -376,13 +376,9 @@ pub fn build_server_hello(
|
||||||
app_data_record.push(TLS_RECORD_APPLICATION);
|
app_data_record.push(TLS_RECORD_APPLICATION);
|
||||||
app_data_record.extend_from_slice(&TLS_VERSION);
|
app_data_record.extend_from_slice(&TLS_VERSION);
|
||||||
app_data_record.extend_from_slice(&(fake_cert_len as u16).to_be_bytes());
|
app_data_record.extend_from_slice(&(fake_cert_len as u16).to_be_bytes());
|
||||||
if fake_cert_len > 17 {
|
// Fill ApplicationData with fully random bytes of desired length to avoid
|
||||||
app_data_record.extend_from_slice(&fake_cert[..fake_cert_len - 17]);
|
// deterministic DPI fingerprints (fixed inner content type markers).
|
||||||
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);
|
app_data_record.extend_from_slice(&fake_cert);
|
||||||
}
|
|
||||||
|
|
||||||
// Combine all records
|
// Combine all records
|
||||||
let mut response = Vec::with_capacity(
|
let mut response = Vec::with_capacity(
|
||||||
|
|
|
||||||
|
|
@ -178,8 +178,9 @@ async fn do_tg_handshake_static(
|
||||||
|
|
||||||
let (read_half, write_half) = stream.into_split();
|
let (read_half, write_half) = stream.into_split();
|
||||||
|
|
||||||
|
let max_pending = config.general.crypto_pending_buffer;
|
||||||
Ok((
|
Ok((
|
||||||
CryptoReader::new(read_half, tg_decryptor),
|
CryptoReader::new(read_half, tg_decryptor),
|
||||||
CryptoWriter::new(write_half, tg_encryptor),
|
CryptoWriter::new(write_half, tg_encryptor, max_pending),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -264,9 +264,10 @@ where
|
||||||
"MTProto handshake successful"
|
"MTProto handshake successful"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let max_pending = config.general.crypto_pending_buffer;
|
||||||
return HandshakeResult::Success((
|
return HandshakeResult::Success((
|
||||||
CryptoReader::new(reader, decryptor),
|
CryptoReader::new(reader, decryptor),
|
||||||
CryptoWriter::new(writer, encryptor),
|
CryptoWriter::new(writer, encryptor, max_pending),
|
||||||
success,
|
success,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,13 @@ use std::net::SocketAddr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
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::config::ProxyConfig;
|
||||||
use crate::crypto::SecureRandom;
|
use crate::crypto::SecureRandom;
|
||||||
use crate::error::{ProxyError, Result};
|
use crate::error::{ProxyError, Result};
|
||||||
use crate::protocol::constants::*;
|
use crate::protocol::constants::{*, secure_padding_len};
|
||||||
use crate::proxy::handshake::HandshakeSuccess;
|
use crate::proxy::handshake::HandshakeSuccess;
|
||||||
use crate::stats::Stats;
|
use crate::stats::Stats;
|
||||||
use crate::stream::{BufferPool, CryptoReader, CryptoWriter};
|
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<R, W>(
|
pub(crate) async fn handle_via_middle_proxy<R, W>(
|
||||||
mut crypto_reader: CryptoReader<R>,
|
mut crypto_reader: CryptoReader<R>,
|
||||||
mut crypto_writer: CryptoWriter<W>,
|
crypto_writer: CryptoWriter<W>,
|
||||||
success: HandshakeSuccess,
|
success: HandshakeSuccess,
|
||||||
me_pool: Arc<MePool>,
|
me_pool: Arc<MePool>,
|
||||||
stats: Arc<Stats>,
|
stats: Arc<Stats>,
|
||||||
_config: Arc<ProxyConfig>,
|
config: Arc<ProxyConfig>,
|
||||||
_buffer_pool: Arc<BufferPool>,
|
_buffer_pool: Arc<BufferPool>,
|
||||||
local_addr: SocketAddr,
|
local_addr: SocketAddr,
|
||||||
rng: Arc<SecureRandom>,
|
rng: Arc<SecureRandom>,
|
||||||
|
|
@ -41,7 +42,7 @@ where
|
||||||
"Routing via Middle-End"
|
"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_connects(&user);
|
||||||
stats.increment_user_curr_connects(&user);
|
stats.increment_user_curr_connects(&user);
|
||||||
|
|
@ -56,10 +57,49 @@ where
|
||||||
|
|
||||||
let translated_local_addr = me_pool.translate_our_addr(local_addr);
|
let translated_local_addr = me_pool.translate_our_addr(local_addr);
|
||||||
|
|
||||||
let result: Result<()> = loop {
|
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! {
|
tokio::select! {
|
||||||
client_frame = read_client_payload(&mut crypto_reader, proto_tag) => {
|
msg = me_rx_task.recv() => {
|
||||||
match client_frame {
|
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?;
|
||||||
|
}
|
||||||
|
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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = &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))) => {
|
Ok(Some((payload, quickack))) => {
|
||||||
trace!(conn_id, bytes = payload.len(), "C->ME frame");
|
trace!(conn_id, bytes = payload.len(), "C->ME frame");
|
||||||
stats.add_user_octets_from(&user, payload.len() as u64);
|
stats.add_user_octets_from(&user, payload.len() as u64);
|
||||||
|
|
@ -70,45 +110,37 @@ where
|
||||||
if payload.len() >= 8 && payload[..8].iter().all(|b| *b == 0) {
|
if payload.len() >= 8 && payload[..8].iter().all(|b| *b == 0) {
|
||||||
flags |= RPC_FLAG_NOT_ENCRYPTED;
|
flags |= RPC_FLAG_NOT_ENCRYPTED;
|
||||||
}
|
}
|
||||||
me_pool.send_proxy_req(
|
if let Err(e) = me_pool.send_proxy_req(
|
||||||
conn_id,
|
conn_id,
|
||||||
success.dc_idx,
|
success.dc_idx,
|
||||||
peer,
|
peer,
|
||||||
translated_local_addr,
|
translated_local_addr,
|
||||||
&payload,
|
&payload,
|
||||||
flags,
|
flags,
|
||||||
).await?;
|
).await {
|
||||||
|
main_result = Err(e);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(None) => {
|
Ok(None) => {
|
||||||
debug!(conn_id, "Client EOF");
|
debug!(conn_id, "Client EOF");
|
||||||
let _ = me_pool.send_close(conn_id).await;
|
let _ = me_pool.send_close(conn_id).await;
|
||||||
break Ok(());
|
break;
|
||||||
}
|
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
|
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");
|
debug!(user = %user, conn_id, "ME relay cleanup");
|
||||||
|
|
@ -120,6 +152,8 @@ where
|
||||||
async fn read_client_payload<R>(
|
async fn read_client_payload<R>(
|
||||||
client_reader: &mut CryptoReader<R>,
|
client_reader: &mut CryptoReader<R>,
|
||||||
proto_tag: ProtoTag,
|
proto_tag: ProtoTag,
|
||||||
|
max_frame: usize,
|
||||||
|
user: &str,
|
||||||
) -> Result<Option<(Vec<u8>, bool)>>
|
) -> Result<Option<(Vec<u8>, bool)>>
|
||||||
where
|
where
|
||||||
R: AsyncRead + Unpin + Send + 'static,
|
R: AsyncRead + Unpin + Send + 'static,
|
||||||
|
|
@ -162,8 +196,15 @@ where
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if len > 16 * 1024 * 1024 {
|
if len > max_frame {
|
||||||
return Err(ProxyError::Proxy(format!("Frame too large: {len}")));
|
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];
|
let mut payload = vec![0u8; len];
|
||||||
|
|
@ -237,7 +278,7 @@ where
|
||||||
}
|
}
|
||||||
ProtoTag::Intermediate | ProtoTag::Secure => {
|
ProtoTag::Intermediate | ProtoTag::Secure => {
|
||||||
let padding_len = if proto_tag == ProtoTag::Secure {
|
let padding_len = if proto_tag == ProtoTag::Secure {
|
||||||
(rng.bytes(1)[0] % 4) as usize
|
secure_padding_len(data.len(), rng)
|
||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@
|
||||||
//! └────────────────────────────────────────┘
|
//! └────────────────────────────────────────┘
|
||||||
//!
|
//!
|
||||||
//! Backpressure
|
//! Backpressure
|
||||||
//! - pending ciphertext buffer is bounded (MAX_PENDING_WRITE)
|
//! - pending ciphertext buffer is bounded (configurable per connection)
|
||||||
//! - pending is full and upstream is pending
|
//! - pending is full and upstream is pending
|
||||||
//! -> poll_write returns Poll::Pending
|
//! -> poll_write returns Poll::Pending
|
||||||
//! -> do not accept any plaintext
|
//! -> do not accept any plaintext
|
||||||
|
|
@ -62,10 +62,9 @@ use super::state::{StreamState, YieldBuffer};
|
||||||
|
|
||||||
// ============= Constants =============
|
// ============= Constants =============
|
||||||
|
|
||||||
/// Maximum size for pending ciphertext buffer (bounded backpressure).
|
/// Default size for pending ciphertext buffer (bounded backpressure).
|
||||||
/// Reduced to 64KB to prevent bufferbloat on mobile networks.
|
/// Actual limit is supplied at runtime from configuration.
|
||||||
/// 512KB was causing high latency on 3G/LTE connections.
|
const DEFAULT_MAX_PENDING_WRITE: usize = 64 * 1024;
|
||||||
const MAX_PENDING_WRITE: usize = 64 * 1024;
|
|
||||||
|
|
||||||
/// Default read buffer capacity (reader mostly decrypts in-place into caller buffer).
|
/// Default read buffer capacity (reader mostly decrypts in-place into caller buffer).
|
||||||
const DEFAULT_READ_CAPACITY: usize = 16 * 1024;
|
const DEFAULT_READ_CAPACITY: usize = 16 * 1024;
|
||||||
|
|
@ -427,15 +426,22 @@ pub struct CryptoWriter<W> {
|
||||||
encryptor: AesCtr,
|
encryptor: AesCtr,
|
||||||
state: CryptoWriterState,
|
state: CryptoWriterState,
|
||||||
scratch: BytesMut,
|
scratch: BytesMut,
|
||||||
|
max_pending_write: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<W> CryptoWriter<W> {
|
impl<W> CryptoWriter<W> {
|
||||||
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 {
|
Self {
|
||||||
upstream,
|
upstream,
|
||||||
encryptor,
|
encryptor,
|
||||||
state: CryptoWriterState::Idle,
|
state: CryptoWriterState::Idle,
|
||||||
scratch: BytesMut::with_capacity(16 * 1024),
|
scratch: BytesMut::with_capacity(16 * 1024),
|
||||||
|
max_pending_write: max_pending.max(4 * 1024),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -484,10 +490,10 @@ impl<W> CryptoWriter<W> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure we are in Flushing state and return mutable pending buffer.
|
/// 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) {
|
if matches!(state, CryptoWriterState::Idle) {
|
||||||
*state = CryptoWriterState::Flushing {
|
*state = CryptoWriterState::Flushing {
|
||||||
pending: PendingCiphertext::new(MAX_PENDING_WRITE),
|
pending: PendingCiphertext::new(max_pending),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -498,14 +504,14 @@ impl<W> CryptoWriter<W> {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Select how many plaintext bytes can be accepted in buffering path
|
/// 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 {
|
if buf_len == 0 {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
match state {
|
match state {
|
||||||
CryptoWriterState::Flushing { pending } => buf_len.min(pending.remaining_capacity()),
|
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,
|
CryptoWriterState::Poisoned { .. } => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -603,7 +609,7 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for CryptoWriter<W> {
|
||||||
Poll::Pending => {
|
Poll::Pending => {
|
||||||
// Upstream blocked. Apply ideal backpressure
|
// Upstream blocked. Apply ideal backpressure
|
||||||
let to_accept =
|
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 {
|
if to_accept == 0 {
|
||||||
trace!(
|
trace!(
|
||||||
|
|
@ -618,7 +624,7 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for CryptoWriter<W> {
|
||||||
|
|
||||||
// Disjoint borrows
|
// Disjoint borrows
|
||||||
let encryptor = &mut this.encryptor;
|
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 let Err(e) = pending.push_encrypted(encryptor, plaintext) {
|
||||||
if e.kind() == ErrorKind::WouldBlock {
|
if e.kind() == ErrorKind::WouldBlock {
|
||||||
|
|
@ -635,7 +641,7 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for CryptoWriter<W> {
|
||||||
// 2) Fast path: pending empty -> write-through
|
// 2) Fast path: pending empty -> write-through
|
||||||
debug_assert!(matches!(this.state, CryptoWriterState::Idle));
|
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];
|
let plaintext = &buf[..to_accept];
|
||||||
|
|
||||||
Self::encrypt_into_scratch(&mut this.encryptor, &mut this.scratch, plaintext);
|
Self::encrypt_into_scratch(&mut this.encryptor, &mut this.scratch, plaintext);
|
||||||
|
|
@ -645,7 +651,7 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for CryptoWriter<W> {
|
||||||
// Upstream blocked: buffer FULL ciphertext for accepted bytes.
|
// Upstream blocked: buffer FULL ciphertext for accepted bytes.
|
||||||
let ciphertext = std::mem::take(&mut this.scratch);
|
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);
|
pending.replace_with(ciphertext);
|
||||||
|
|
||||||
Poll::Ready(Ok(to_accept))
|
Poll::Ready(Ok(to_accept))
|
||||||
|
|
@ -672,7 +678,7 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for CryptoWriter<W> {
|
||||||
let remainder = this.scratch.split_off(n);
|
let remainder = this.scratch.split_off(n);
|
||||||
this.scratch.clear();
|
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);
|
pending.replace_with(remainder);
|
||||||
|
|
||||||
Poll::Ready(Ok(to_accept))
|
Poll::Ready(Ok(to_accept))
|
||||||
|
|
|
||||||
|
|
@ -267,8 +267,8 @@ impl<W: AsyncWrite + Unpin> SecureIntermediateFrameWriter<W> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add random padding (0-3 bytes)
|
// Add padding so total length is never divisible by 4 (MTProto Secure)
|
||||||
let padding_len = self.rng.range(4);
|
let padding_len = secure_padding_len(data.len(), &self.rng);
|
||||||
let padding = self.rng.bytes(padding_len);
|
let padding = self.rng.bytes(padding_len);
|
||||||
|
|
||||||
let total_len = data.len() + padding_len;
|
let total_len = data.len() + padding_len;
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@
|
||||||
//! - However, the on-the-wire record length can exceed 16384 because TLS 1.3
|
//! - However, the on-the-wire record length can exceed 16384 because TLS 1.3
|
||||||
//! uses AEAD and can include tag/overhead/padding.
|
//! uses AEAD and can include tag/overhead/padding.
|
||||||
//! - Telegram FakeTLS clients (notably iOS) may send Application Data records
|
//! - 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:
|
//! If you reject those (e.g. validate length <= 16384), you will see errors like:
|
||||||
//! "TLS record too large: 16408 bytes"
|
//! "TLS record too large: 16408 bytes"
|
||||||
|
|
@ -52,9 +53,8 @@ use super::state::{StreamState, HeaderBuffer, YieldBuffer, WriteBuffer};
|
||||||
const TLS_HEADER_SIZE: usize = 5;
|
const TLS_HEADER_SIZE: usize = 5;
|
||||||
|
|
||||||
/// Maximum TLS fragment size we emit for Application Data.
|
/// Maximum TLS fragment size we emit for Application Data.
|
||||||
/// Real TLS 1.3 ciphertexts often add ~16-24 bytes AEAD overhead, so to mimic
|
/// Real TLS 1.3 allows up to 16384 + 256 bytes of ciphertext (incl. tag).
|
||||||
/// on-the-wire record sizes we allow up to 16384 + 24 bytes of plaintext.
|
const MAX_TLS_PAYLOAD: usize = 16384 + 256;
|
||||||
const MAX_TLS_PAYLOAD: usize = 16384 + 24;
|
|
||||||
|
|
||||||
/// Maximum pending write buffer for one record remainder.
|
/// Maximum pending write buffer for one record remainder.
|
||||||
/// Note: we never queue unlimited amount of data here; state holds at most one record.
|
/// 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),
|
/// - 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).
|
/// 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
|
/// - 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.
|
/// - For other record types we keep stricter bounds to avoid memory abuse.
|
||||||
fn validate(&self) -> Result<()> {
|
fn validate(&self) -> Result<()> {
|
||||||
// Version: accept TLS 1.0 header (ClientHello quirk) and TLS_VERSION (0x0303).
|
// Version: accept TLS 1.0 header (ClientHello quirk) and TLS_VERSION (0x0303).
|
||||||
|
|
@ -105,7 +105,7 @@ impl TlsRecordHeader {
|
||||||
let len = self.length as usize;
|
let len = self.length as usize;
|
||||||
|
|
||||||
// Length checks depend on record type.
|
// 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 {
|
match self.record_type {
|
||||||
TLS_RECORD_APPLICATION => {
|
TLS_RECORD_APPLICATION => {
|
||||||
if len > MAX_TLS_CHUNK_SIZE {
|
if len > MAX_TLS_CHUNK_SIZE {
|
||||||
|
|
@ -755,9 +755,6 @@ impl<W: AsyncWrite + Unpin> AsyncWrite for FakeTlsWriter<W> {
|
||||||
payload_size: chunk_size,
|
payload_size: chunk_size,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Wake to retry flushing soon.
|
|
||||||
cx.waker().wake_by_ref();
|
|
||||||
|
|
||||||
Poll::Ready(Ok(chunk_size))
|
Poll::Ready(Ok(chunk_size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,27 @@ impl TlsFrontCache {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Ok(data) = tokio::fs::read(entry.path()).await {
|
if let Ok(data) = tokio::fs::read(entry.path()).await {
|
||||||
if let Ok(cached) = serde_json::from_slice::<CachedTlsData>(&data) {
|
if let Ok(mut cached) = serde_json::from_slice::<CachedTlsData>(&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();
|
let domain = cached.domain.clone();
|
||||||
self.set(&domain, cached).await;
|
self.set(&domain, cached).await;
|
||||||
loaded += 1;
|
loaded += 1;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue