Middle Proxy is so real
This commit is contained in:
178
src/transport/middle_proxy/codec.rs
Normal file
178
src/transport/middle_proxy/codec.rs
Normal file
@@ -0,0 +1,178 @@
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
|
||||
use crate::crypto::{AesCbc, crc32};
|
||||
use crate::error::{ProxyError, Result};
|
||||
use crate::protocol::constants::*;
|
||||
|
||||
pub(crate) fn build_rpc_frame(seq_no: i32, payload: &[u8]) -> Vec<u8> {
|
||||
let total_len = (4 + 4 + payload.len() + 4) as u32;
|
||||
let mut frame = Vec::with_capacity(total_len as usize);
|
||||
frame.extend_from_slice(&total_len.to_le_bytes());
|
||||
frame.extend_from_slice(&seq_no.to_le_bytes());
|
||||
frame.extend_from_slice(payload);
|
||||
let c = crc32(&frame);
|
||||
frame.extend_from_slice(&c.to_le_bytes());
|
||||
frame
|
||||
}
|
||||
|
||||
pub(crate) async fn read_rpc_frame_plaintext(
|
||||
rd: &mut (impl AsyncReadExt + Unpin),
|
||||
) -> Result<(i32, Vec<u8>)> {
|
||||
let mut len_buf = [0u8; 4];
|
||||
rd.read_exact(&mut len_buf).await.map_err(ProxyError::Io)?;
|
||||
let total_len = u32::from_le_bytes(len_buf) as usize;
|
||||
|
||||
if !(12..=(1 << 24)).contains(&total_len) {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Bad RPC frame length: {total_len}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut rest = vec![0u8; total_len - 4];
|
||||
rd.read_exact(&mut rest).await.map_err(ProxyError::Io)?;
|
||||
|
||||
let mut full = Vec::with_capacity(total_len);
|
||||
full.extend_from_slice(&len_buf);
|
||||
full.extend_from_slice(&rest);
|
||||
|
||||
let crc_offset = total_len - 4;
|
||||
let expected_crc = u32::from_le_bytes(full[crc_offset..crc_offset + 4].try_into().unwrap());
|
||||
let actual_crc = crc32(&full[..crc_offset]);
|
||||
if expected_crc != actual_crc {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"CRC mismatch: 0x{expected_crc:08x} vs 0x{actual_crc:08x}"
|
||||
)));
|
||||
}
|
||||
|
||||
let seq_no = i32::from_le_bytes(full[4..8].try_into().unwrap());
|
||||
let payload = full[8..crc_offset].to_vec();
|
||||
Ok((seq_no, payload))
|
||||
}
|
||||
|
||||
pub(crate) fn build_nonce_payload(key_selector: u32, crypto_ts: u32, nonce: &[u8; 16]) -> [u8; 32] {
|
||||
let mut p = [0u8; 32];
|
||||
p[0..4].copy_from_slice(&RPC_NONCE_U32.to_le_bytes());
|
||||
p[4..8].copy_from_slice(&key_selector.to_le_bytes());
|
||||
p[8..12].copy_from_slice(&RPC_CRYPTO_AES_U32.to_le_bytes());
|
||||
p[12..16].copy_from_slice(&crypto_ts.to_le_bytes());
|
||||
p[16..32].copy_from_slice(nonce);
|
||||
p
|
||||
}
|
||||
|
||||
pub(crate) fn parse_nonce_payload(d: &[u8]) -> Result<(u32, u32, [u8; 16])> {
|
||||
if d.len() < 32 {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Nonce payload too short: {} bytes",
|
||||
d.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let t = u32::from_le_bytes(d[0..4].try_into().unwrap());
|
||||
if t != RPC_NONCE_U32 {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Expected RPC_NONCE 0x{RPC_NONCE_U32:08x}, got 0x{t:08x}"
|
||||
)));
|
||||
}
|
||||
|
||||
let schema = u32::from_le_bytes(d[8..12].try_into().unwrap());
|
||||
let ts = u32::from_le_bytes(d[12..16].try_into().unwrap());
|
||||
let mut nonce = [0u8; 16];
|
||||
nonce.copy_from_slice(&d[16..32]);
|
||||
Ok((schema, ts, nonce))
|
||||
}
|
||||
|
||||
pub(crate) fn build_handshake_payload(
|
||||
our_ip: [u8; 4],
|
||||
our_port: u16,
|
||||
peer_ip: [u8; 4],
|
||||
peer_port: u16,
|
||||
) -> [u8; 32] {
|
||||
let mut p = [0u8; 32];
|
||||
p[0..4].copy_from_slice(&RPC_HANDSHAKE_U32.to_le_bytes());
|
||||
|
||||
// Keep C memory layout compatibility for PID IPv4 bytes.
|
||||
p[8..12].copy_from_slice(&our_ip);
|
||||
p[12..14].copy_from_slice(&our_port.to_le_bytes());
|
||||
let pid = (std::process::id() & 0xffff) as u16;
|
||||
p[14..16].copy_from_slice(&pid.to_le_bytes());
|
||||
let utime = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as u32;
|
||||
p[16..20].copy_from_slice(&utime.to_le_bytes());
|
||||
|
||||
p[20..24].copy_from_slice(&peer_ip);
|
||||
p[24..26].copy_from_slice(&peer_port.to_le_bytes());
|
||||
p
|
||||
}
|
||||
|
||||
pub(crate) fn cbc_encrypt_padded(
|
||||
key: &[u8; 32],
|
||||
iv: &[u8; 16],
|
||||
plaintext: &[u8],
|
||||
) -> Result<(Vec<u8>, [u8; 16])> {
|
||||
let pad = (16 - (plaintext.len() % 16)) % 16;
|
||||
let mut buf = plaintext.to_vec();
|
||||
let pad_pattern: [u8; 4] = [0x04, 0x00, 0x00, 0x00];
|
||||
for i in 0..pad {
|
||||
buf.push(pad_pattern[i % 4]);
|
||||
}
|
||||
|
||||
let cipher = AesCbc::new(*key, *iv);
|
||||
cipher
|
||||
.encrypt_in_place(&mut buf)
|
||||
.map_err(|e| ProxyError::Crypto(format!("CBC encrypt: {e}")))?;
|
||||
|
||||
let mut new_iv = [0u8; 16];
|
||||
if buf.len() >= 16 {
|
||||
new_iv.copy_from_slice(&buf[buf.len() - 16..]);
|
||||
}
|
||||
Ok((buf, new_iv))
|
||||
}
|
||||
|
||||
pub(crate) fn cbc_decrypt_inplace(
|
||||
key: &[u8; 32],
|
||||
iv: &[u8; 16],
|
||||
data: &mut [u8],
|
||||
) -> Result<[u8; 16]> {
|
||||
let mut new_iv = [0u8; 16];
|
||||
if data.len() >= 16 {
|
||||
new_iv.copy_from_slice(&data[data.len() - 16..]);
|
||||
}
|
||||
|
||||
AesCbc::new(*key, *iv)
|
||||
.decrypt_in_place(data)
|
||||
.map_err(|e| ProxyError::Crypto(format!("CBC decrypt: {e}")))?;
|
||||
Ok(new_iv)
|
||||
}
|
||||
|
||||
pub(crate) struct RpcWriter {
|
||||
pub(crate) writer: tokio::io::WriteHalf<tokio::net::TcpStream>,
|
||||
pub(crate) key: [u8; 32],
|
||||
pub(crate) iv: [u8; 16],
|
||||
pub(crate) seq_no: i32,
|
||||
}
|
||||
|
||||
impl RpcWriter {
|
||||
pub(crate) async fn send(&mut self, payload: &[u8]) -> Result<()> {
|
||||
let frame = build_rpc_frame(self.seq_no, payload);
|
||||
self.seq_no += 1;
|
||||
|
||||
let pad = (16 - (frame.len() % 16)) % 16;
|
||||
let mut buf = frame;
|
||||
let pad_pattern: [u8; 4] = [0x04, 0x00, 0x00, 0x00];
|
||||
for i in 0..pad {
|
||||
buf.push(pad_pattern[i % 4]);
|
||||
}
|
||||
|
||||
let cipher = AesCbc::new(self.key, self.iv);
|
||||
cipher
|
||||
.encrypt_in_place(&mut buf)
|
||||
.map_err(|e| ProxyError::Crypto(format!("{e}")))?;
|
||||
|
||||
if buf.len() >= 16 {
|
||||
self.iv.copy_from_slice(&buf[buf.len() - 16..]);
|
||||
}
|
||||
self.writer.write_all(&buf).await.map_err(ProxyError::Io)
|
||||
}
|
||||
}
|
||||
38
src/transport/middle_proxy/health.rs
Normal file
38
src/transport/middle_proxy/health.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::crypto::SecureRandom;
|
||||
use crate::protocol::constants::TG_MIDDLE_PROXIES_FLAT_V4;
|
||||
|
||||
use super::MePool;
|
||||
|
||||
pub async fn me_health_monitor(pool: Arc<MePool>, rng: Arc<SecureRandom>, min_connections: usize) {
|
||||
loop {
|
||||
tokio::time::sleep(Duration::from_secs(30)).await;
|
||||
let current = pool.connection_count();
|
||||
if current < min_connections {
|
||||
warn!(
|
||||
current,
|
||||
min = min_connections,
|
||||
"ME pool below minimum, reconnecting..."
|
||||
);
|
||||
let addrs = TG_MIDDLE_PROXIES_FLAT_V4.clone();
|
||||
for &(ip, port) in addrs.iter() {
|
||||
let needed = min_connections.saturating_sub(pool.connection_count());
|
||||
if needed == 0 {
|
||||
break;
|
||||
}
|
||||
for _ in 0..needed {
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
match pool.connect_one(addr, &rng).await {
|
||||
Ok(()) => info!(%addr, "ME reconnected"),
|
||||
Err(e) => debug!(%addr, error = %e, "ME reconnect failed"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/transport/middle_proxy/mod.rs
Normal file
24
src/transport/middle_proxy/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
//! Middle Proxy RPC transport.
|
||||
|
||||
mod codec;
|
||||
mod health;
|
||||
mod pool;
|
||||
mod reader;
|
||||
mod registry;
|
||||
mod secret;
|
||||
mod wire;
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
pub use health::me_health_monitor;
|
||||
pub use pool::MePool;
|
||||
pub use registry::ConnRegistry;
|
||||
pub use secret::fetch_proxy_secret;
|
||||
pub use wire::proto_flags_for_tag;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MeResponse {
|
||||
Data { flags: u32, data: Bytes },
|
||||
Ack(u32),
|
||||
Close,
|
||||
}
|
||||
431
src/transport/middle_proxy/pool.rs
Normal file
431
src/transport/middle_proxy/pool.rs
Normal file
@@ -0,0 +1,431 @@
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use bytes::BytesMut;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
use tokio::time::{Instant, timeout};
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::crypto::{SecureRandom, derive_middleproxy_keys};
|
||||
use crate::error::{ProxyError, Result};
|
||||
use crate::protocol::constants::*;
|
||||
|
||||
use super::ConnRegistry;
|
||||
use super::codec::{
|
||||
RpcWriter, build_handshake_payload, build_nonce_payload, build_rpc_frame, cbc_decrypt_inplace,
|
||||
cbc_encrypt_padded, parse_nonce_payload, read_rpc_frame_plaintext,
|
||||
};
|
||||
use super::reader::reader_loop;
|
||||
use super::wire::{IpMaterial, build_proxy_req_payload, extract_ip_material};
|
||||
|
||||
pub struct MePool {
|
||||
registry: Arc<ConnRegistry>,
|
||||
writers: Arc<RwLock<Vec<Arc<Mutex<RpcWriter>>>>>,
|
||||
rr: AtomicU64,
|
||||
proxy_tag: Option<Vec<u8>>,
|
||||
proxy_secret: Vec<u8>,
|
||||
nat_ip: Option<IpAddr>,
|
||||
pool_size: usize,
|
||||
}
|
||||
|
||||
impl MePool {
|
||||
pub fn new(
|
||||
proxy_tag: Option<Vec<u8>>,
|
||||
proxy_secret: Vec<u8>,
|
||||
nat_ip: Option<IpAddr>,
|
||||
) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
registry: Arc::new(ConnRegistry::new()),
|
||||
writers: Arc::new(RwLock::new(Vec::new())),
|
||||
rr: AtomicU64::new(0),
|
||||
proxy_tag,
|
||||
proxy_secret,
|
||||
nat_ip,
|
||||
pool_size: 2,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn has_proxy_tag(&self) -> bool {
|
||||
self.proxy_tag.is_some()
|
||||
}
|
||||
|
||||
pub fn translate_our_addr(&self, addr: SocketAddr) -> SocketAddr {
|
||||
let ip = self.translate_ip_for_nat(addr.ip());
|
||||
SocketAddr::new(ip, addr.port())
|
||||
}
|
||||
|
||||
pub fn registry(&self) -> &Arc<ConnRegistry> {
|
||||
&self.registry
|
||||
}
|
||||
|
||||
fn translate_ip_for_nat(&self, ip: IpAddr) -> IpAddr {
|
||||
let Some(nat_ip) = self.nat_ip else {
|
||||
return ip;
|
||||
};
|
||||
|
||||
match (ip, nat_ip) {
|
||||
(IpAddr::V4(src), IpAddr::V4(dst))
|
||||
if src.is_private() || src.is_loopback() || src.is_unspecified() =>
|
||||
{
|
||||
IpAddr::V4(dst)
|
||||
}
|
||||
(IpAddr::V6(src), IpAddr::V6(dst)) if src.is_loopback() || src.is_unspecified() => {
|
||||
IpAddr::V6(dst)
|
||||
}
|
||||
(orig, _) => orig,
|
||||
}
|
||||
}
|
||||
|
||||
fn writers_arc(&self) -> Arc<RwLock<Vec<Arc<Mutex<RpcWriter>>>>> {
|
||||
self.writers.clone()
|
||||
}
|
||||
|
||||
fn key_selector(&self) -> u32 {
|
||||
if self.proxy_secret.len() >= 4 {
|
||||
u32::from_le_bytes([
|
||||
self.proxy_secret[0],
|
||||
self.proxy_secret[1],
|
||||
self.proxy_secret[2],
|
||||
self.proxy_secret[3],
|
||||
])
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn init(self: &Arc<Self>, pool_size: usize, rng: &SecureRandom) -> Result<()> {
|
||||
let addrs = &*TG_MIDDLE_PROXIES_FLAT_V4;
|
||||
let ks = self.key_selector();
|
||||
info!(
|
||||
me_servers = addrs.len(),
|
||||
pool_size,
|
||||
key_selector = format_args!("0x{ks:08x}"),
|
||||
secret_len = self.proxy_secret.len(),
|
||||
"Initializing ME pool"
|
||||
);
|
||||
|
||||
for &(ip, port) in addrs.iter() {
|
||||
for i in 0..pool_size {
|
||||
let addr = SocketAddr::new(ip, port);
|
||||
match self.connect_one(addr, rng).await {
|
||||
Ok(()) => info!(%addr, idx = i, "ME connected"),
|
||||
Err(e) => warn!(%addr, idx = i, error = %e, "ME connect failed"),
|
||||
}
|
||||
}
|
||||
if self.writers.read().await.len() >= pool_size {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if self.writers.read().await.is_empty() {
|
||||
return Err(ProxyError::Proxy("No ME connections".into()));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn connect_one(
|
||||
self: &Arc<Self>,
|
||||
addr: SocketAddr,
|
||||
rng: &SecureRandom,
|
||||
) -> Result<()> {
|
||||
let secret = &self.proxy_secret;
|
||||
if secret.len() < 32 {
|
||||
return Err(ProxyError::Proxy(
|
||||
"proxy-secret too short for ME auth".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let stream = timeout(
|
||||
Duration::from_secs(ME_CONNECT_TIMEOUT_SECS),
|
||||
TcpStream::connect(addr),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ProxyError::ConnectionTimeout {
|
||||
addr: addr.to_string(),
|
||||
})?
|
||||
.map_err(ProxyError::Io)?;
|
||||
stream.set_nodelay(true).ok();
|
||||
|
||||
let local_addr = stream.local_addr().map_err(ProxyError::Io)?;
|
||||
let peer_addr = stream.peer_addr().map_err(ProxyError::Io)?;
|
||||
let local_addr_nat = self.translate_our_addr(local_addr);
|
||||
let peer_addr_nat =
|
||||
SocketAddr::new(self.translate_ip_for_nat(peer_addr.ip()), peer_addr.port());
|
||||
let (mut rd, mut wr) = tokio::io::split(stream);
|
||||
|
||||
let my_nonce: [u8; 16] = rng.bytes(16).try_into().unwrap();
|
||||
let crypto_ts = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs() as u32;
|
||||
|
||||
let ks = self.key_selector();
|
||||
let nonce_payload = build_nonce_payload(ks, crypto_ts, &my_nonce);
|
||||
let nonce_frame = build_rpc_frame(-2, &nonce_payload);
|
||||
wr.write_all(&nonce_frame).await.map_err(ProxyError::Io)?;
|
||||
wr.flush().await.map_err(ProxyError::Io)?;
|
||||
|
||||
let (srv_seq, srv_nonce_payload) = timeout(
|
||||
Duration::from_secs(ME_HANDSHAKE_TIMEOUT_SECS),
|
||||
read_rpc_frame_plaintext(&mut rd),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| ProxyError::TgHandshakeTimeout)??;
|
||||
|
||||
if srv_seq != -2 {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Expected seq=-2, got {srv_seq}"
|
||||
)));
|
||||
}
|
||||
|
||||
let (schema, srv_ts, srv_nonce) = parse_nonce_payload(&srv_nonce_payload)?;
|
||||
if schema != RPC_CRYPTO_AES_U32 {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Unsupported crypto schema: 0x{schema:x}"
|
||||
)));
|
||||
}
|
||||
|
||||
let skew = crypto_ts.abs_diff(srv_ts);
|
||||
if skew > 30 {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"nonce crypto_ts skew too large: client={crypto_ts}, server={srv_ts}, skew={skew}s"
|
||||
)));
|
||||
}
|
||||
|
||||
let ts_bytes = crypto_ts.to_le_bytes();
|
||||
let server_port_bytes = peer_addr_nat.port().to_le_bytes();
|
||||
let client_port_bytes = local_addr_nat.port().to_le_bytes();
|
||||
|
||||
let server_ip = extract_ip_material(peer_addr_nat);
|
||||
let client_ip = extract_ip_material(local_addr_nat);
|
||||
|
||||
let (srv_ip_opt, clt_ip_opt, clt_v6_opt, srv_v6_opt, hs_our_ip, hs_peer_ip) =
|
||||
match (server_ip, client_ip) {
|
||||
(IpMaterial::V4(srv), IpMaterial::V4(clt)) => {
|
||||
(Some(srv), Some(clt), None, None, clt, srv)
|
||||
}
|
||||
(IpMaterial::V6(srv), IpMaterial::V6(clt)) => {
|
||||
let zero = [0u8; 4];
|
||||
(None, None, Some(clt), Some(srv), zero, zero)
|
||||
}
|
||||
_ => {
|
||||
return Err(ProxyError::InvalidHandshake(
|
||||
"mixed IPv4/IPv6 endpoints are not supported for ME key derivation"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let (wk, wi) = derive_middleproxy_keys(
|
||||
&srv_nonce,
|
||||
&my_nonce,
|
||||
&ts_bytes,
|
||||
srv_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&client_port_bytes,
|
||||
b"CLIENT",
|
||||
clt_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&server_port_bytes,
|
||||
secret,
|
||||
clt_v6_opt.as_ref(),
|
||||
srv_v6_opt.as_ref(),
|
||||
);
|
||||
let (rk, ri) = derive_middleproxy_keys(
|
||||
&srv_nonce,
|
||||
&my_nonce,
|
||||
&ts_bytes,
|
||||
srv_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&client_port_bytes,
|
||||
b"SERVER",
|
||||
clt_ip_opt.as_ref().map(|x| &x[..]),
|
||||
&server_port_bytes,
|
||||
secret,
|
||||
clt_v6_opt.as_ref(),
|
||||
srv_v6_opt.as_ref(),
|
||||
);
|
||||
|
||||
let hs_payload =
|
||||
build_handshake_payload(hs_our_ip, local_addr.port(), hs_peer_ip, peer_addr.port());
|
||||
let hs_frame = build_rpc_frame(-1, &hs_payload);
|
||||
let (encrypted_hs, write_iv) = cbc_encrypt_padded(&wk, &wi, &hs_frame)?;
|
||||
wr.write_all(&encrypted_hs).await.map_err(ProxyError::Io)?;
|
||||
wr.flush().await.map_err(ProxyError::Io)?;
|
||||
|
||||
let deadline = Instant::now() + Duration::from_secs(ME_HANDSHAKE_TIMEOUT_SECS);
|
||||
let mut enc_buf = BytesMut::with_capacity(256);
|
||||
let mut dec_buf = BytesMut::with_capacity(256);
|
||||
let mut read_iv = ri;
|
||||
let mut handshake_ok = false;
|
||||
|
||||
while Instant::now() < deadline && !handshake_ok {
|
||||
let remaining = deadline - Instant::now();
|
||||
let mut tmp = [0u8; 256];
|
||||
let n = match timeout(remaining, rd.read(&mut tmp)).await {
|
||||
Ok(Ok(0)) => {
|
||||
return Err(ProxyError::Io(std::io::Error::new(
|
||||
std::io::ErrorKind::UnexpectedEof,
|
||||
"ME closed during handshake",
|
||||
)));
|
||||
}
|
||||
Ok(Ok(n)) => n,
|
||||
Ok(Err(e)) => return Err(ProxyError::Io(e)),
|
||||
Err(_) => return Err(ProxyError::TgHandshakeTimeout),
|
||||
};
|
||||
|
||||
enc_buf.extend_from_slice(&tmp[..n]);
|
||||
|
||||
let blocks = enc_buf.len() / 16 * 16;
|
||||
if blocks > 0 {
|
||||
let mut chunk = vec![0u8; blocks];
|
||||
chunk.copy_from_slice(&enc_buf[..blocks]);
|
||||
read_iv = cbc_decrypt_inplace(&rk, &read_iv, &mut chunk)?;
|
||||
dec_buf.extend_from_slice(&chunk);
|
||||
let _ = enc_buf.split_to(blocks);
|
||||
}
|
||||
|
||||
while dec_buf.len() >= 4 {
|
||||
let fl = u32::from_le_bytes(dec_buf[0..4].try_into().unwrap()) as usize;
|
||||
|
||||
if fl == 4 {
|
||||
let _ = dec_buf.split_to(4);
|
||||
continue;
|
||||
}
|
||||
if !(12..=(1 << 24)).contains(&fl) {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Bad HS response frame len: {fl}"
|
||||
)));
|
||||
}
|
||||
if dec_buf.len() < fl {
|
||||
break;
|
||||
}
|
||||
|
||||
let frame = dec_buf.split_to(fl);
|
||||
let pe = fl - 4;
|
||||
let ec = u32::from_le_bytes(frame[pe..pe + 4].try_into().unwrap());
|
||||
let ac = crate::crypto::crc32(&frame[..pe]);
|
||||
if ec != ac {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"HS CRC mismatch: 0x{ec:08x} vs 0x{ac:08x}"
|
||||
)));
|
||||
}
|
||||
|
||||
let hs_type = u32::from_le_bytes(frame[8..12].try_into().unwrap());
|
||||
if hs_type == RPC_HANDSHAKE_ERROR_U32 {
|
||||
let err_code = if frame.len() >= 16 {
|
||||
i32::from_le_bytes(frame[12..16].try_into().unwrap())
|
||||
} else {
|
||||
-1
|
||||
};
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"ME rejected handshake (error={err_code})"
|
||||
)));
|
||||
}
|
||||
if hs_type != RPC_HANDSHAKE_U32 {
|
||||
return Err(ProxyError::InvalidHandshake(format!(
|
||||
"Expected HANDSHAKE 0x{RPC_HANDSHAKE_U32:08x}, got 0x{hs_type:08x}"
|
||||
)));
|
||||
}
|
||||
|
||||
handshake_ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if !handshake_ok {
|
||||
return Err(ProxyError::TgHandshakeTimeout);
|
||||
}
|
||||
|
||||
info!(%addr, "RPC handshake OK");
|
||||
|
||||
let rpc_w = Arc::new(Mutex::new(RpcWriter {
|
||||
writer: wr,
|
||||
key: wk,
|
||||
iv: write_iv,
|
||||
seq_no: 0,
|
||||
}));
|
||||
self.writers.write().await.push(rpc_w.clone());
|
||||
|
||||
let reg = self.registry.clone();
|
||||
let w_pong = rpc_w.clone();
|
||||
let w_pool = self.writers_arc();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
reader_loop(rd, rk, read_iv, reg, enc_buf, dec_buf, w_pong.clone()).await
|
||||
{
|
||||
warn!(error = %e, "ME reader ended");
|
||||
}
|
||||
let mut ws = w_pool.write().await;
|
||||
ws.retain(|w| !Arc::ptr_eq(w, &w_pong));
|
||||
info!(remaining = ws.len(), "Dead ME writer removed from pool");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_proxy_req(
|
||||
&self,
|
||||
conn_id: u64,
|
||||
client_addr: SocketAddr,
|
||||
our_addr: SocketAddr,
|
||||
data: &[u8],
|
||||
proto_flags: u32,
|
||||
) -> Result<()> {
|
||||
let payload = build_proxy_req_payload(
|
||||
conn_id,
|
||||
client_addr,
|
||||
our_addr,
|
||||
data,
|
||||
self.proxy_tag.as_deref(),
|
||||
proto_flags,
|
||||
);
|
||||
|
||||
loop {
|
||||
let ws = self.writers.read().await;
|
||||
if ws.is_empty() {
|
||||
return Err(ProxyError::Proxy("All ME connections dead".into()));
|
||||
}
|
||||
|
||||
let idx = self.rr.fetch_add(1, Ordering::Relaxed) as usize % ws.len();
|
||||
let w = ws[idx].clone();
|
||||
drop(ws);
|
||||
|
||||
match w.lock().await.send(&payload).await {
|
||||
Ok(()) => return Ok(()),
|
||||
Err(e) => {
|
||||
warn!(error = %e, "ME write failed, removing dead conn");
|
||||
let mut ws = self.writers.write().await;
|
||||
ws.retain(|o| !Arc::ptr_eq(o, &w));
|
||||
if ws.is_empty() {
|
||||
return Err(ProxyError::Proxy("All ME connections dead".into()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_close(&self, conn_id: u64) -> Result<()> {
|
||||
let ws = self.writers.read().await;
|
||||
if !ws.is_empty() {
|
||||
let w = ws[0].clone();
|
||||
drop(ws);
|
||||
let mut p = Vec::with_capacity(12);
|
||||
p.extend_from_slice(&RPC_CLOSE_EXT_U32.to_le_bytes());
|
||||
p.extend_from_slice(&conn_id.to_le_bytes());
|
||||
if let Err(e) = w.lock().await.send(&p).await {
|
||||
debug!(error = %e, "ME close write failed");
|
||||
let mut ws = self.writers.write().await;
|
||||
ws.retain(|o| !Arc::ptr_eq(o, &w));
|
||||
}
|
||||
}
|
||||
|
||||
self.registry.unregister(conn_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn connection_count(&self) -> usize {
|
||||
self.writers.try_read().map(|w| w.len()).unwrap_or(0)
|
||||
}
|
||||
}
|
||||
141
src/transport/middle_proxy/reader.rs
Normal file
141
src/transport/middle_proxy/reader.rs
Normal file
@@ -0,0 +1,141 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Mutex;
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use crate::crypto::{AesCbc, crc32};
|
||||
use crate::error::{ProxyError, Result};
|
||||
use crate::protocol::constants::*;
|
||||
|
||||
use super::codec::RpcWriter;
|
||||
use super::{ConnRegistry, MeResponse};
|
||||
|
||||
pub(crate) async fn reader_loop(
|
||||
mut rd: tokio::io::ReadHalf<TcpStream>,
|
||||
dk: [u8; 32],
|
||||
mut div: [u8; 16],
|
||||
reg: Arc<ConnRegistry>,
|
||||
enc_leftover: BytesMut,
|
||||
mut dec: BytesMut,
|
||||
writer: Arc<Mutex<RpcWriter>>,
|
||||
) -> Result<()> {
|
||||
let mut raw = enc_leftover;
|
||||
|
||||
loop {
|
||||
let mut tmp = [0u8; 16_384];
|
||||
let n = rd.read(&mut tmp).await.map_err(ProxyError::Io)?;
|
||||
if n == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
raw.extend_from_slice(&tmp[..n]);
|
||||
|
||||
let blocks = raw.len() / 16 * 16;
|
||||
if blocks > 0 {
|
||||
let mut new_iv = [0u8; 16];
|
||||
new_iv.copy_from_slice(&raw[blocks - 16..blocks]);
|
||||
|
||||
let mut chunk = vec![0u8; blocks];
|
||||
chunk.copy_from_slice(&raw[..blocks]);
|
||||
AesCbc::new(dk, div)
|
||||
.decrypt_in_place(&mut chunk)
|
||||
.map_err(|e| ProxyError::Crypto(format!("{e}")))?;
|
||||
div = new_iv;
|
||||
dec.extend_from_slice(&chunk);
|
||||
let _ = raw.split_to(blocks);
|
||||
}
|
||||
|
||||
while dec.len() >= 12 {
|
||||
let fl = u32::from_le_bytes(dec[0..4].try_into().unwrap()) as usize;
|
||||
if fl == 4 {
|
||||
let _ = dec.split_to(4);
|
||||
continue;
|
||||
}
|
||||
if !(12..=(1 << 24)).contains(&fl) {
|
||||
warn!(frame_len = fl, "Invalid RPC frame len");
|
||||
dec.clear();
|
||||
break;
|
||||
}
|
||||
if dec.len() < fl {
|
||||
break;
|
||||
}
|
||||
|
||||
let frame = dec.split_to(fl);
|
||||
let pe = fl - 4;
|
||||
let ec = u32::from_le_bytes(frame[pe..pe + 4].try_into().unwrap());
|
||||
if crc32(&frame[..pe]) != ec {
|
||||
warn!("CRC mismatch in data frame");
|
||||
continue;
|
||||
}
|
||||
|
||||
let payload = &frame[8..pe];
|
||||
if payload.len() < 4 {
|
||||
continue;
|
||||
}
|
||||
|
||||
let pt = u32::from_le_bytes(payload[0..4].try_into().unwrap());
|
||||
let body = &payload[4..];
|
||||
|
||||
if pt == RPC_PROXY_ANS_U32 && body.len() >= 12 {
|
||||
let flags = u32::from_le_bytes(body[0..4].try_into().unwrap());
|
||||
let cid = u64::from_le_bytes(body[4..12].try_into().unwrap());
|
||||
let data = Bytes::copy_from_slice(&body[12..]);
|
||||
trace!(cid, flags, len = data.len(), "RPC_PROXY_ANS");
|
||||
|
||||
let routed = reg.route(cid, MeResponse::Data { flags, data }).await;
|
||||
if !routed {
|
||||
reg.unregister(cid).await;
|
||||
send_close_conn(&writer, cid).await;
|
||||
}
|
||||
} else if pt == RPC_SIMPLE_ACK_U32 && body.len() >= 12 {
|
||||
let cid = u64::from_le_bytes(body[0..8].try_into().unwrap());
|
||||
let cfm = u32::from_le_bytes(body[8..12].try_into().unwrap());
|
||||
trace!(cid, cfm, "RPC_SIMPLE_ACK");
|
||||
|
||||
let routed = reg.route(cid, MeResponse::Ack(cfm)).await;
|
||||
if !routed {
|
||||
reg.unregister(cid).await;
|
||||
send_close_conn(&writer, cid).await;
|
||||
}
|
||||
} else if pt == RPC_CLOSE_EXT_U32 && body.len() >= 8 {
|
||||
let cid = u64::from_le_bytes(body[0..8].try_into().unwrap());
|
||||
debug!(cid, "RPC_CLOSE_EXT from ME");
|
||||
reg.route(cid, MeResponse::Close).await;
|
||||
reg.unregister(cid).await;
|
||||
} else if pt == RPC_CLOSE_CONN_U32 && body.len() >= 8 {
|
||||
let cid = u64::from_le_bytes(body[0..8].try_into().unwrap());
|
||||
debug!(cid, "RPC_CLOSE_CONN from ME");
|
||||
reg.route(cid, MeResponse::Close).await;
|
||||
reg.unregister(cid).await;
|
||||
} else if pt == RPC_PING_U32 && body.len() >= 8 {
|
||||
let ping_id = i64::from_le_bytes(body[0..8].try_into().unwrap());
|
||||
trace!(ping_id, "RPC_PING -> RPC_PONG");
|
||||
let mut pong = Vec::with_capacity(12);
|
||||
pong.extend_from_slice(&RPC_PONG_U32.to_le_bytes());
|
||||
pong.extend_from_slice(&ping_id.to_le_bytes());
|
||||
if let Err(e) = writer.lock().await.send(&pong).await {
|
||||
warn!(error = %e, "PONG send failed");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
rpc_type = format_args!("0x{pt:08x}"),
|
||||
len = body.len(),
|
||||
"Unknown RPC"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_close_conn(writer: &Arc<Mutex<RpcWriter>>, conn_id: u64) {
|
||||
let mut p = Vec::with_capacity(12);
|
||||
p.extend_from_slice(&RPC_CLOSE_CONN_U32.to_le_bytes());
|
||||
p.extend_from_slice(&conn_id.to_le_bytes());
|
||||
|
||||
if let Err(e) = writer.lock().await.send(&p).await {
|
||||
debug!(conn_id, error = %e, "Failed to send RPC_CLOSE_CONN");
|
||||
}
|
||||
}
|
||||
40
src/transport/middle_proxy/registry.rs
Normal file
40
src/transport/middle_proxy/registry.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use tokio::sync::{RwLock, mpsc};
|
||||
|
||||
use super::MeResponse;
|
||||
|
||||
pub struct ConnRegistry {
|
||||
map: RwLock<HashMap<u64, mpsc::Sender<MeResponse>>>,
|
||||
next_id: AtomicU64,
|
||||
}
|
||||
|
||||
impl ConnRegistry {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: RwLock::new(HashMap::new()),
|
||||
next_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn register(&self) -> (u64, mpsc::Receiver<MeResponse>) {
|
||||
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
|
||||
let (tx, rx) = mpsc::channel(256);
|
||||
self.map.write().await.insert(id, tx);
|
||||
(id, rx)
|
||||
}
|
||||
|
||||
pub async fn unregister(&self, id: u64) {
|
||||
self.map.write().await.remove(&id);
|
||||
}
|
||||
|
||||
pub async fn route(&self, id: u64, resp: MeResponse) -> bool {
|
||||
let m = self.map.read().await;
|
||||
if let Some(tx) = m.get(&id) {
|
||||
tx.send(resp).await.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
76
src/transport/middle_proxy/secret.rs
Normal file
76
src/transport/middle_proxy/secret.rs
Normal file
@@ -0,0 +1,76 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use tracing::{debug, info, warn};
|
||||
|
||||
use crate::error::{ProxyError, Result};
|
||||
|
||||
/// Fetch Telegram proxy-secret binary.
|
||||
pub async fn fetch_proxy_secret(cache_path: Option<&str>) -> Result<Vec<u8>> {
|
||||
let cache = cache_path.unwrap_or("proxy-secret");
|
||||
|
||||
if let Ok(metadata) = tokio::fs::metadata(cache).await {
|
||||
if let Ok(modified) = metadata.modified() {
|
||||
let age = std::time::SystemTime::now()
|
||||
.duration_since(modified)
|
||||
.unwrap_or(Duration::from_secs(u64::MAX));
|
||||
if age < Duration::from_secs(86_400) {
|
||||
if let Ok(data) = tokio::fs::read(cache).await {
|
||||
if data.len() >= 32 {
|
||||
info!(
|
||||
path = cache,
|
||||
len = data.len(),
|
||||
age_hours = age.as_secs() / 3600,
|
||||
"Loaded proxy-secret from cache"
|
||||
);
|
||||
return Ok(data);
|
||||
}
|
||||
warn!(
|
||||
path = cache,
|
||||
len = data.len(),
|
||||
"Cached proxy-secret too short"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Downloading proxy-secret from core.telegram.org...");
|
||||
let data = download_proxy_secret().await?;
|
||||
|
||||
if let Err(e) = tokio::fs::write(cache, &data).await {
|
||||
warn!(error = %e, "Failed to cache proxy-secret (non-fatal)");
|
||||
} else {
|
||||
debug!(path = cache, len = data.len(), "Cached proxy-secret");
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
async fn download_proxy_secret() -> Result<Vec<u8>> {
|
||||
let resp = reqwest::get("https://core.telegram.org/getProxySecret")
|
||||
.await
|
||||
.map_err(|e| ProxyError::Proxy(format!("Failed to download proxy-secret: {e}")))?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
return Err(ProxyError::Proxy(format!(
|
||||
"proxy-secret download HTTP {}",
|
||||
resp.status()
|
||||
)));
|
||||
}
|
||||
|
||||
let data = resp
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| ProxyError::Proxy(format!("Read proxy-secret body: {e}")))?
|
||||
.to_vec();
|
||||
|
||||
if data.len() < 32 {
|
||||
return Err(ProxyError::Proxy(format!(
|
||||
"proxy-secret too short: {} bytes (need >= 32)",
|
||||
data.len()
|
||||
)));
|
||||
}
|
||||
|
||||
info!(len = data.len(), "Downloaded proxy-secret OK");
|
||||
Ok(data)
|
||||
}
|
||||
106
src/transport/middle_proxy/wire.rs
Normal file
106
src/transport/middle_proxy/wire.rs
Normal file
@@ -0,0 +1,106 @@
|
||||
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
|
||||
|
||||
use crate::protocol::constants::*;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum IpMaterial {
|
||||
V4([u8; 4]),
|
||||
V6([u8; 16]),
|
||||
}
|
||||
|
||||
pub(crate) fn extract_ip_material(addr: SocketAddr) -> IpMaterial {
|
||||
match addr.ip() {
|
||||
IpAddr::V4(v4) => IpMaterial::V4(v4.octets()),
|
||||
IpAddr::V6(v6) => {
|
||||
if let Some(v4) = v6.to_ipv4_mapped() {
|
||||
IpMaterial::V4(v4.octets())
|
||||
} else {
|
||||
IpMaterial::V6(v6.octets())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ipv4_to_mapped_v6_c_compat(ip: Ipv4Addr) -> [u8; 16] {
|
||||
let mut buf = [0u8; 16];
|
||||
|
||||
// Matches tl_store_long(0) + tl_store_int(-0x10000).
|
||||
buf[8..12].copy_from_slice(&(-0x10000i32).to_le_bytes());
|
||||
|
||||
// Matches tl_store_int(htonl(remote_ip_host_order)).
|
||||
let host_order = u32::from_ne_bytes(ip.octets());
|
||||
let network_order = host_order.to_be();
|
||||
buf[12..16].copy_from_slice(&network_order.to_le_bytes());
|
||||
|
||||
buf
|
||||
}
|
||||
|
||||
fn append_mapped_addr_and_port(buf: &mut Vec<u8>, addr: SocketAddr) {
|
||||
match addr.ip() {
|
||||
IpAddr::V4(v4) => buf.extend_from_slice(&ipv4_to_mapped_v6_c_compat(v4)),
|
||||
IpAddr::V6(v6) => buf.extend_from_slice(&v6.octets()),
|
||||
}
|
||||
buf.extend_from_slice(&(addr.port() as u32).to_le_bytes());
|
||||
}
|
||||
|
||||
pub(crate) fn build_proxy_req_payload(
|
||||
conn_id: u64,
|
||||
client_addr: SocketAddr,
|
||||
our_addr: SocketAddr,
|
||||
data: &[u8],
|
||||
proxy_tag: Option<&[u8]>,
|
||||
proto_flags: u32,
|
||||
) -> Vec<u8> {
|
||||
let mut b = Vec::with_capacity(128 + data.len());
|
||||
|
||||
b.extend_from_slice(&RPC_PROXY_REQ_U32.to_le_bytes());
|
||||
b.extend_from_slice(&proto_flags.to_le_bytes());
|
||||
b.extend_from_slice(&conn_id.to_le_bytes());
|
||||
|
||||
append_mapped_addr_and_port(&mut b, client_addr);
|
||||
append_mapped_addr_and_port(&mut b, our_addr);
|
||||
|
||||
if proto_flags & 12 != 0 {
|
||||
let extra_start = b.len();
|
||||
b.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
if let Some(tag) = proxy_tag {
|
||||
b.extend_from_slice(&TL_PROXY_TAG_U32.to_le_bytes());
|
||||
|
||||
if tag.len() < 254 {
|
||||
b.push(tag.len() as u8);
|
||||
b.extend_from_slice(tag);
|
||||
let pad = (4 - ((1 + tag.len()) % 4)) % 4;
|
||||
b.extend(std::iter::repeat_n(0u8, pad));
|
||||
} else {
|
||||
b.push(0xfe);
|
||||
let len_bytes = (tag.len() as u32).to_le_bytes();
|
||||
b.extend_from_slice(&len_bytes[..3]);
|
||||
b.extend_from_slice(tag);
|
||||
let pad = (4 - (tag.len() % 4)) % 4;
|
||||
b.extend(std::iter::repeat_n(0u8, pad));
|
||||
}
|
||||
}
|
||||
|
||||
let extra_bytes = (b.len() - extra_start - 4) as u32;
|
||||
b[extra_start..extra_start + 4].copy_from_slice(&extra_bytes.to_le_bytes());
|
||||
}
|
||||
|
||||
b.extend_from_slice(data);
|
||||
b
|
||||
}
|
||||
|
||||
pub fn proto_flags_for_tag(tag: crate::protocol::constants::ProtoTag, has_proxy_tag: bool) -> u32 {
|
||||
use crate::protocol::constants::ProtoTag;
|
||||
|
||||
let mut flags = RPC_FLAG_MAGIC | RPC_FLAG_EXTMODE2;
|
||||
if has_proxy_tag {
|
||||
flags |= RPC_FLAG_HAS_AD_TAG;
|
||||
}
|
||||
|
||||
match tag {
|
||||
ProtoTag::Abridged => flags | RPC_FLAG_ABRIDGED,
|
||||
ProtoTag::Intermediate => flags | RPC_FLAG_INTERMEDIATE,
|
||||
ProtoTag::Secure => flags | RPC_FLAG_PAD | RPC_FLAG_INTERMEDIATE,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user