mirror of
https://github.com/by-sonic/tglock.git
synced 2026-11-05 15:21:55 +03:00
fix: медиа и «прокси не настроен»: пакет на кадр, настоящие фронты, kwsN-1 только для медиа; beta.19 (#75)
* fix(tunnel): медиа и «прокси не настроен»: один пакет на кадр, настоящие фронты, kwsN-1 только для медиа Три причины, каждая проверена на живом Telegram запросом req_DH_params: сервер сверяет номер DC внутри него с собой и отвечает -444 на чужой. - WebSocket-фронт разбирает один пакет на входящий кадр, остаток ждёт следующего. Клиент пишет запросы пачками, TGLock слал их одним кадром, и всё, что шло вторым, застревало: текст проходил, файлы вставали. Теперь поток режется на пакеты, как в Flowseal/tg-ws-proxy. Пачка через beta.18 висит на всех DC, через эту сборку принимается на всех. - Кластер kwsN-1 отвергает обычное соединение с -444, а Telegram Desktop в ответ выключает прокси как настроенный неверно. При сбое kwsN основное соединение откатывалось именно туда. Теперь kwsN-1 только для медиа. - Адреса из списка DC не говорят на TLS ни из какой сети (проверено с чистой сети), поэтому первый маршрут каждого DC не работал никогда. Закреплены настоящие фронты, и DC1, DC3, DC5 стали доступны по WebSocket, чего не было. У CDN203 своего фронта нет: остались TCP, Worker и CF-прокси. Обрыв туннеля, через который уже шли данные, больше не ставит маршрут на паузу. Таймаут соединения 8 с, как в tg-ws-proxy. probe_proxy.mjs получил --identity и --batched: они и нашли эти баги. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat: журнал ошибок в диагностике, --log-file для роутеров; 2.0.0-beta.19 «Скопировать диагностику» отдавала только счётчики, и по ней нельзя было понять, какой маршрут не ответил. Теперь в отчёте есть строки сбоев туннелей, а домены Worker и CF-прокси в них заменены на <домен>. --log-file и --log-max-mb (log_file, log_max_mb в настройках), как в tg-ws-proxy: журнал идёт в файл с ротацией, а не в системный журнал роутера. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(routes): запасной WebSocket на порту 80 родных адресов, включая CDN203 Родные адреса Telegram принимают WebSocket без TLS на порту 80. Измерено req_DH_params: DC определяется адресом, имя хоста не важно, а 91.105.192.100 отдаёт ключи CDN. Для CDN203, откуда часто идёт видео, это первый прямой WebSocket: на 443 фронта у него нет, и там, где провайдер закрыл только 443, до него раньше было не достать без Worker. Защита та же, что у родного TCP: внутри тот же обфусцированный MTProto. В Host идёт адрес, имени Telegram в открытом виде нет. Маршрут разрешён только до закреплённых адресов и пробуется последним. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(routes): паузы маршрутов по результатам ревью, пределы и журнал - Обрыв не наказывает маршрут, только если туннель работал и получал ответы 15 с. Одного ответа мало: маршрут, который ответил раз и умер, оставался предпочтительным навсегда. - Клиент спросил, не дождался ответа за 5 с и ушёл: маршрут считается неработающим. Иначе CDN, принявший TCP и молчащий, держал первое место, и очередь до порта 80 не доходила. - Предел пакета 2 МиБ вместо 16: чужое устройство в LAN-режиме не заставит роутер копить десятки мегабайт на соединение. - Журнал: разовая ошибка записи его больше не выключает, неудачная ротация не теряет бэкап и держит предел, права 0600 и на уже существующем файле. - В отчёт диагностики не попадает Worker, заданный адресом. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: babin <Thartewerner536e@engineer.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+244
@@ -354,6 +354,138 @@ fn build_crypto_context(
|
||||
})
|
||||
}
|
||||
|
||||
/// Длина, больше которой пакет считается не настоящим. Файлы грузятся частями
|
||||
/// по 512 КиБ. Предел держит и память: в LAN-режиме чужое устройство могло бы
|
||||
/// заставить копить «пакет» на роутере. Если настоящий пакет окажется длиннее,
|
||||
/// поток просто пойдёт дальше без нарезки.
|
||||
const MAX_PACKET: usize = 2 * 1024 * 1024;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Framing {
|
||||
Abridged,
|
||||
Intermediate,
|
||||
}
|
||||
|
||||
/// Режет поток клиента на транспортные пакеты MTProto.
|
||||
///
|
||||
/// WebSocket-фронт Telegram разбирает один пакет на входящий кадр, а остаток
|
||||
/// кадра держит до следующего. Клиент пишет запросы пачками, и всё, что шло
|
||||
/// вторым в той же записи TCP, застревало на фронте: текст проходил, а файлы
|
||||
/// вставали. Проверено на живом фронте: `req_DH_params` вторым пакетом в кадре
|
||||
/// остаётся без ответа, пока не придёт следующий кадр, а отдельным кадром
|
||||
/// принимается сразу. Так же режет поток Flowseal/tg-ws-proxy.
|
||||
///
|
||||
/// Заголовок читается по копии: ключи `init` открыты, поэтому режим с секретом
|
||||
/// и SOCKS5 разбираются одинаково, а наружу уходят исходные байты.
|
||||
pub struct PacketSplitter {
|
||||
framing: Option<Framing>,
|
||||
decrypt: AesCtr,
|
||||
wire: Vec<u8>,
|
||||
plain: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PacketSplitter {
|
||||
/// Разбирать поток, начатый этим `init`. Неизвестный протокол не режется.
|
||||
pub fn from_init(init: &[u8; INIT_LEN]) -> Self {
|
||||
let key: [u8; 32] = init[KEY_START..KEY_END].try_into().unwrap();
|
||||
let iv: [u8; 16] = init[KEY_END..IV_END].try_into().unwrap();
|
||||
let mut decrypt = AesCtr::new((&key).into(), (&iv).into());
|
||||
let mut plain_init = *init;
|
||||
decrypt.apply_keystream(&mut plain_init);
|
||||
let framing = match [
|
||||
plain_init[TAG_START],
|
||||
plain_init[TAG_START + 1],
|
||||
plain_init[TAG_START + 2],
|
||||
plain_init[TAG_START + 3],
|
||||
] {
|
||||
ABRIDGED => Some(Framing::Abridged),
|
||||
INTERMEDIATE | PADDED_INTERMEDIATE => Some(Framing::Intermediate),
|
||||
_ => None,
|
||||
};
|
||||
Self {
|
||||
framing,
|
||||
decrypt,
|
||||
wire: Vec::new(),
|
||||
plain: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Принять зашифрованные байты, вернуть готовые к отправке пакеты.
|
||||
pub fn push(&mut self, wire: &[u8]) -> Vec<Vec<u8>> {
|
||||
let Some(framing) = self.framing else {
|
||||
return vec![wire.to_vec()];
|
||||
};
|
||||
let start = self.plain.len();
|
||||
self.wire.extend_from_slice(wire);
|
||||
self.plain.extend_from_slice(wire);
|
||||
self.decrypt.apply_keystream(&mut self.plain[start..]);
|
||||
|
||||
let mut packets = Vec::new();
|
||||
let mut offset = 0;
|
||||
while offset < self.plain.len() {
|
||||
match packet_len(framing, &self.plain[offset..]) {
|
||||
PacketLen::Incomplete => break,
|
||||
PacketLen::Complete(length) => {
|
||||
if self.plain.len() - offset < length {
|
||||
break;
|
||||
}
|
||||
packets.push(self.wire[offset..offset + length].to_vec());
|
||||
offset += length;
|
||||
}
|
||||
// Длина не похожа на настоящую: поток не наш. Дальше он идёт
|
||||
// как есть, иначе данные встанут в ожидании несуществующего конца.
|
||||
PacketLen::Invalid => {
|
||||
packets.push(self.wire[offset..].to_vec());
|
||||
offset = self.plain.len();
|
||||
self.framing = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.wire.drain(..offset);
|
||||
self.plain.drain(..offset);
|
||||
packets
|
||||
}
|
||||
|
||||
/// Хвост незаконченного пакета: клиент закрыл соединение на середине.
|
||||
pub fn finish(&mut self) -> Option<Vec<u8>> {
|
||||
self.plain.clear();
|
||||
(!self.wire.is_empty()).then(|| std::mem::take(&mut self.wire))
|
||||
}
|
||||
}
|
||||
|
||||
enum PacketLen {
|
||||
Incomplete,
|
||||
Complete(usize),
|
||||
Invalid,
|
||||
}
|
||||
|
||||
fn packet_len(framing: Framing, plain: &[u8]) -> PacketLen {
|
||||
let (header, payload) = match framing {
|
||||
Framing::Abridged => match plain {
|
||||
[] => return PacketLen::Incomplete,
|
||||
[0x7f | 0xff, length @ ..] => match length {
|
||||
[a, b, c, ..] => (4, u32::from_le_bytes([*a, *b, *c, 0]) as usize * 4),
|
||||
_ => return PacketLen::Incomplete,
|
||||
},
|
||||
[first, ..] => (1, (*first & 0x7f) as usize * 4),
|
||||
},
|
||||
Framing::Intermediate => match plain {
|
||||
[a, b, c, d, ..] => {
|
||||
// Старший бит просит quick ack и к длине не относится.
|
||||
(
|
||||
4,
|
||||
(u32::from_le_bytes([*a, *b, *c, *d]) & 0x7fff_ffff) as usize,
|
||||
)
|
||||
}
|
||||
_ => return PacketLen::Incomplete,
|
||||
},
|
||||
};
|
||||
if payload == 0 || payload > MAX_PACKET {
|
||||
return PacketLen::Invalid;
|
||||
}
|
||||
PacketLen::Complete(header + payload)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn test_client_init(secret: &[u8; 16], dc_index: i16) -> [u8; INIT_LEN] {
|
||||
tests::generate_client_init(secret, PADDED_INTERMEDIATE, dc_index)
|
||||
@@ -423,6 +555,118 @@ pub(crate) fn test_relay_peer(relay_init: &[u8; INIT_LEN]) -> TestPeer {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Поток, каким его увидел бы фронт Telegram, и разделитель для него.
|
||||
fn splitter_for(tag: [u8; 4], plain: &[u8]) -> (PacketSplitter, Vec<u8>) {
|
||||
let init = generate_relay_init(tag, 2);
|
||||
let mut wire = plain.to_vec();
|
||||
test_relay_peer(&init).decrypt(&mut wire);
|
||||
(PacketSplitter::from_init(&init), wire)
|
||||
}
|
||||
|
||||
fn intermediate(payload: &[u8]) -> Vec<u8> {
|
||||
let mut packet = (payload.len() as u32).to_le_bytes().to_vec();
|
||||
packet.extend_from_slice(payload);
|
||||
packet
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glued_packets_leave_one_per_frame() {
|
||||
let first = intermediate(&[1; 40]);
|
||||
let second = intermediate(&[2; 340]);
|
||||
let plain = [first.clone(), second.clone()].concat();
|
||||
for tag in [INTERMEDIATE, PADDED_INTERMEDIATE] {
|
||||
let (mut splitter, wire) = splitter_for(tag, &plain);
|
||||
let packets = splitter.push(&wire);
|
||||
assert_eq!(packets.len(), 2);
|
||||
assert_eq!(packets[0], wire[..first.len()]);
|
||||
assert_eq!(packets[1], wire[first.len()..]);
|
||||
assert_eq!(splitter.finish(), None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_packet_is_held_until_it_is_complete() {
|
||||
let packet = intermediate(&[7; 300]);
|
||||
let (mut splitter, wire) = splitter_for(INTERMEDIATE, &packet);
|
||||
// Границы TCP не совпадают с пакетами: режем в том числе внутри длины.
|
||||
assert!(splitter.push(&wire[..2]).is_empty());
|
||||
assert!(splitter.push(&wire[2..150]).is_empty());
|
||||
assert_eq!(splitter.push(&wire[150..]), vec![wire.clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tail_of_one_packet_and_the_start_of_the_next_stay_separate() {
|
||||
let first = intermediate(&[1; 40]);
|
||||
let second = intermediate(&[2; 80]);
|
||||
let plain = [first.clone(), second.clone()].concat();
|
||||
let (mut splitter, wire) = splitter_for(INTERMEDIATE, &plain);
|
||||
let packets = splitter.push(&wire[..first.len() + 10]);
|
||||
assert_eq!(packets, vec![wire[..first.len()].to_vec()]);
|
||||
let packets = splitter.push(&wire[first.len() + 10..]);
|
||||
assert_eq!(packets, vec![wire[first.len()..].to_vec()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quick_ack_bit_is_not_part_of_the_length() {
|
||||
let mut packet = intermediate(&[3; 16]);
|
||||
packet[3] |= 0x80;
|
||||
let (mut splitter, wire) = splitter_for(INTERMEDIATE, &packet);
|
||||
assert_eq!(splitter.push(&wire), vec![wire.clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abridged_lengths_count_in_words_with_either_header() {
|
||||
let short = [vec![2], vec![9; 8]].concat();
|
||||
let long = [vec![0x7f, 0x80, 0, 0], vec![9; 512]].concat();
|
||||
let quick_ack = [vec![0x81], vec![9; 4]].concat();
|
||||
let plain = [short.clone(), long.clone(), quick_ack.clone()].concat();
|
||||
let (mut splitter, wire) = splitter_for(ABRIDGED, &plain);
|
||||
let lengths: Vec<_> = splitter.push(&wire).iter().map(Vec::len).collect();
|
||||
assert_eq!(lengths, [short.len(), long.len(), quick_ack.len()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_length_that_cannot_be_real_stops_the_splitting_without_losing_bytes() {
|
||||
// Если длина мусорная, ждать конца пакета значит заморозить соединение.
|
||||
let mut garbage = 0x7fff_fff0_u32.to_le_bytes().to_vec();
|
||||
garbage.extend_from_slice(&[5; 32]);
|
||||
let (mut splitter, wire) = splitter_for(INTERMEDIATE, &garbage);
|
||||
assert_eq!(splitter.push(&wire), vec![wire.clone()]);
|
||||
assert_eq!(splitter.push(b"more"), vec![b"more".to_vec()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_length_over_the_limit_is_never_buffered() {
|
||||
let mut oversized = ((MAX_PACKET + 1) as u32).to_le_bytes().to_vec();
|
||||
oversized.extend_from_slice(&[5; 32]);
|
||||
let (mut splitter, wire) = splitter_for(INTERMEDIATE, &oversized);
|
||||
assert_eq!(splitter.push(&wire), vec![wire.clone()]);
|
||||
assert_eq!(splitter.finish(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_largest_real_upload_part_still_arrives_whole() {
|
||||
let packet = intermediate(&vec![7; 512 * 1024 + 256]);
|
||||
let (mut splitter, wire) = splitter_for(INTERMEDIATE, &packet);
|
||||
assert!(splitter.push(&wire[..65536]).is_empty());
|
||||
assert_eq!(splitter.push(&wire[65536..]), vec![wire.clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_unknown_protocol_passes_through_untouched() {
|
||||
let (mut splitter, wire) = splitter_for([0x11; 4], &[0; 16]);
|
||||
assert_eq!(splitter.push(&wire), vec![wire.clone()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_unfinished_tail_is_returned_once() {
|
||||
let packet = intermediate(&[1; 40]);
|
||||
let (mut splitter, wire) = splitter_for(INTERMEDIATE, &packet);
|
||||
assert!(splitter.push(&wire[..10]).is_empty());
|
||||
assert_eq!(splitter.finish(), Some(wire[..10].to_vec()));
|
||||
assert_eq!(splitter.finish(), None);
|
||||
}
|
||||
|
||||
pub(super) fn generate_client_init(
|
||||
secret: &[u8; 16],
|
||||
protocol_tag: [u8; 4],
|
||||
|
||||
Reference in New Issue
Block a user