From b92403b4c99ec5eed62a68d576380a483907a575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9D=D0=B8=D0=BA=D0=B8=D1=82=D0=B0=20Sonic?= Date: Wed, 19 Aug 2026 14:32:34 +0300 Subject: [PATCH] =?UTF-8?q?fix(proxy):=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD?= =?UTF-8?q?=D1=82,=20=D1=81=20=D0=BA=D0=BE=D1=82=D0=BE=D1=80=D1=8B=D0=BC?= =?UTF-8?q?=20=D0=BD=D0=B5=20=D0=B4=D0=BE=D0=B3=D0=BE=D0=B2=D0=BE=D1=80?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=D1=81=D1=8C,=20=D0=B7=D0=B0=D0=BA=D1=80?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D0=BB=D1=81=D1=8F=20=D0=BC=D0=BE=D0=BB=D1=87?= =?UTF-8?q?=D0=B0=20(#42)=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Диагностика от @alexsagaidak в #42 показала третий случай, которого ни один счётчик не различал. У него ноль отклонённых и живые туннели, то есть оба показателя говорят «всё хорошо»: соединений 17 · туннелей 6 · DC4 · Запасной Telegram IP · сбоев 0 · падений маршрутов 26 · отклонено 0 Клиент, который дошёл до прокси, но не сумел договориться, не попадал ни в blocked, ни в ws_failures. Ошибка из handle() выбрасывалась в `let _ =`, соединение закрывалось, и наружу это выглядело как active, дёрнувшийся вверх и обратно. По диагностике неотличимо от клиента, который подключился и работает. Теперь такие клиенты считает unknown_clients, а журнал называет адрес и причину. Причин две: MTProto-init не разбирается под текущим секретом. Почти всегда это ссылка tg://proxy от прошлого запуска: секрет — её половина, и клиент с сохранённой старой ссылкой попадает ровно сюда. Со стороны Telegram это и есть «прокси настроен неверно и будет отключён» — то, с чем пришли в #37 и что до сих пор нельзя было подтвердить со стороны прокси. SOCKS5-приветствие не разбирается. Сюда же попадает MTProto-соединение, ушедшее в SOCKS5-ветку по неоднозначному первому байту, если полный init не успел прийти за PROTOCOL_PROBE_TIMEOUT. На loopback этого не бывает, а через Wi-Fi с телефона — уже вопрос задержки. От ws_failures отличается тем, что тот растёт после успешного рукопожатия с клиентом: там договорились с клиентом, но не смогли с Telegram. Различать их важно, иначе непонятно, в какую сторону смотреть. В интерфейсе — метрика «Не опознаны» с пояснением, в строке статуса CLI — поле «не опознано N». Co-authored-by: by-sonic <171230345+by-sonic@users.noreply.github.com> --- README.md | 3 +- docs/ARCHITECTURE_V2.md | 19 ++++++++ src/bin/cli.rs | 5 +- src/main.rs | 4 ++ src/proxy.rs | 101 ++++++++++++++++++++++++++++++++++++++-- ui/main.ts | 14 ++++++ 6 files changed, 139 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 8fd127a..cb57ef8 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,7 @@ Telegram → Настройки → **Продвинутые** → Тип сое > > - **Соединения `0` и Отклонено `0`** — телефон до компьютера не дошёл. Дело не в TGLock: проверь, что оба устройства в одной сети (телефон может сидеть на гостевом Wi-Fi или в мобильном интернете), что в роутере не включена изоляция клиентов, и что брандмауэр пускает входящие на порт TGLock. > - **Соединения растут, Отклонено растёт** — телефон дошёл, но просит адрес, который LAN-режим не пропускает. Конкретный адрес назван в журнале событий ниже — пришли эту строку в issue. +> - **Не опознаны растёт** — телефон дошёл, но договориться не вышло. Почти всегда в Telegram на телефоне вписана ссылка от прошлого запуска, то есть другой секрет. Сверь её с той, что показана в окне сейчас. > - **Соединения растут, Туннели `0`** — до Telegram не доходит уже сам компьютер. Это [Cloudflare Worker](docs/CLOUDFLARE_WORKER.md), а не проблема LAN. ### 🖥 Без графического интерфейса: `tglock-cli` @@ -207,7 +208,7 @@ worker = ["my-name.workers.dev"] Файл с секретом внутри держите с правами `600`: это доступ к вашему прокси. -При запуске печатается готовая `tg://proxy`-ссылка — её можно открыть на любом устройстве в сети, чтобы Telegram настроился сам. Дальше в лог идёт по строке на каждое изменение состояния: сколько соединений, какой дата-центр, какой маршрут живой, сколько сбоев и сколько запросов отклонено политикой «только Telegram». Отдельными строками отмечаются подключившиеся устройства и адреса, из-за которых был отказ, — по ним видно, дошёл ли телефон до сервиса вообще. +При запуске печатается готовая `tg://proxy`-ссылка — её можно открыть на любом устройстве в сети, чтобы Telegram настроился сам. Дальше в лог идёт по строке на каждое изменение состояния: сколько соединений, какой дата-центр, какой маршрут живой, сколько сбоев, сколько запросов отклонено политикой «только Telegram» и сколько клиентов не опознано. Отдельными строками отмечаются подключившиеся устройства, адреса, из-за которых был отказ, и клиенты, с которыми не удалось договориться, — по ним видно, дошёл ли телефон до сервиса вообще и не вписан ли в нём устаревший секрет. Прав администратора не нужно: TGLock не правит ни системный DNS, ни файл `hosts` — нужные адреса Telegram зашиты в маршрутах, а TLS SNI остаётся настоящим. diff --git a/docs/ARCHITECTURE_V2.md b/docs/ARCHITECTURE_V2.md index 9bcafd3..2772799 100644 --- a/docs/ARCHITECTURE_V2.md +++ b/docs/ARCHITECTURE_V2.md @@ -97,6 +97,25 @@ LAN-режим превращал бы машину в открытый прок и дошёл, но попросил адрес, который мы не пропускаем. Первый виден как ноль соединений и ноль отказов, второй — как соединения есть, отказы растут. +Третий случай нашёлся, когда репортёр #42 прислал диагностику: у него было ноль +отказов и работающие туннели, то есть оба счётчика говорили «всё хорошо». +Клиент, который дошёл до прокси, но не сумел договориться, не попадал ни в +один из них. Соединение просто закрывалось: `active` дёргался вверх и обратно. + +Теперь такие клиенты считает `unknown_clients`, и журнал называет адрес и +причину. Их две: + +- MTProto-init не разбирается под текущим секретом. Почти всегда это ссылка + `tg://proxy` от прошлого запуска: секрет — её половина, и клиент со + сохранённой старой ссылкой попадает ровно сюда. Со стороны Telegram это и + есть «прокси настроен неверно и будет отключён» (#37). +- SOCKS5-приветствие не разбирается. Сюда же попадает MTProto-соединение, + ушедшее в SOCKS5-ветку по неоднозначному первому байту, если полный init не + успел прийти за `PROTOCOL_PROBE_TIMEOUT`. + +Счётчик `ws_failures` от них отличается тем, что растёт после успешного +рукопожатия с клиентом: там договорились с клиентом, но не смогли с Telegram. + ## Учёт состояния `Stats::ws` считает **установленные** туннели: счётчик поднимается после diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 608de7c..e76fe48 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -223,14 +223,15 @@ async fn watch_status(stats: Arc) { stats.ws_failures.load(Ordering::Relaxed), stats.route_failures(), stats.blocked.load(Ordering::Relaxed), + stats.unknown_clients.load(Ordering::Relaxed), ); if previous.as_ref() == Some(¤t) { continue; } - let (active, tunnels, dc, route, failures, route_failures, blocked) = current; + let (active, tunnels, dc, route, failures, route_failures, blocked, unknown) = current; let line = format!( "соединений {active} · туннелей {tunnels} · {} · {} · сбоев {failures} · \ - падений маршрутов {route_failures} · отклонено {blocked}", + падений маршрутов {route_failures} · отклонено {blocked} · не опознано {unknown}", if dc > 0 { format!("DC{dc}") } else { diff --git a/src/main.rs b/src/main.rs index 5f4154f..ee4ab42 100644 --- a/src/main.rs +++ b/src/main.rs @@ -53,6 +53,9 @@ struct StatusSnapshot { /// этой машины; не ноль — что дотянулся, и разбираться надо с адресами /// (by-sonic/tglock#42). blocked: u32, + /// Клиенты, которые дошли, но не сумели договориться. Почти всегда это + /// ссылка `tg://proxy` от прошлого запуска, то есть другой секрет. + unknown_clients: u32, uptime_seconds: u64, port: u16, /// Адрес, который нужно вписать в Telegram на другом устройстве. @@ -124,6 +127,7 @@ impl AppState { failures: self.stats.ws_failures.load(Ordering::Relaxed), route_failures: self.stats.route_failures(), blocked: self.stats.blocked.load(Ordering::Relaxed), + unknown_clients: self.stats.unknown_clients.load(Ordering::Relaxed), uptime_seconds: self .started_at .lock() diff --git a/src/proxy.rs b/src/proxy.rs index 98eedaa..199e738 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -31,6 +31,12 @@ pub struct Stats { /// Раньше отказ происходил молча, и снаружи оба случая выглядели одинаково /// (by-sonic/tglock#42). pub blocked: AtomicU32, + /// Сколько клиентов дошло до прокси, но не сумело договориться. + /// + /// Почти всегда это несовпадение секрета: в Telegram вписана ссылка от + /// прошлого запуска. Такое соединение закрывалось молча, и по диагностике + /// отличить его от рабочего было нельзя. + pub unknown_clients: AtomicU32, /// See `transport::RouteKind::ui_code`. pub last_route: AtomicU8, transport: crate::transport::TransportEngine, @@ -91,6 +97,7 @@ impl Stats { last_dc: AtomicU16::new(0), ws_failures: AtomicU32::new(0), blocked: AtomicU32::new(0), + unknown_clients: AtomicU32::new(0), last_route: AtomicU8::new(0), transport: crate::transport::TransportEngine::new(), secret, @@ -122,6 +129,20 @@ impl Stats { )); } + /// Клиент дошёл, но договориться с ним не удалось. + /// + /// Раньше такое соединение закрывалось молча: `active` дёргался вверх и + /// обратно, и всё. По диагностике это неотличимо от «клиент подключился и + /// работает», хотя означает противоположное (by-sonic/tglock#42). + fn note_unknown_client(&self, peer: Option, reason: &str) { + self.unknown_clients.fetch_add(1, Ordering::Relaxed); + let who = match peer { + Some(peer) => peer.ip().to_string(), + None => "неизвестный адрес".to_owned(), + }; + self.note(format!("Клиент {who}: {reason}")); + } + /// Отметить, что до прокси дотянулось устройство из сети, а не с этой машины. /// /// Это первое, что нужно знать при разборе LAN-режима: если строки нет, @@ -335,10 +356,19 @@ async fn handle_socks5( allow_direct: bool, ) -> Result<(), Box> { s.set_nodelay(true)?; + let peer = s.peer_addr().ok(); - let (addr, port) = tokio::time::timeout(IO_TIMEOUT, read_socks5_request(&mut s)) + let request = tokio::time::timeout(IO_TIMEOUT, read_socks5_request(&mut s)) .await - .map_err(|_| "SOCKS5 handshake timeout")??; + .map_err(|_| "SOCKS5 handshake timeout".to_owned()) + .and_then(|result| result.map_err(|error| error.to_string())); + let (addr, port) = match request { + Ok(request) => request, + Err(error) => { + stats.note_unknown_client(peer, &format!("SOCKS5-приветствие не разобрано ({error})")); + return Err(error.into()); + } + }; let destination = classify(&addr); if destination == Destination::Elsewhere && !allow_direct { stats.note_blocked(&addr, port); @@ -436,12 +466,25 @@ async fn handle_mtproto( stats: &Stats, ) -> Result<(), Box> { stream.set_nodelay(true)?; + let peer = stream.peer_addr().ok(); let mut init = [0; 64]; tokio::time::timeout(IO_TIMEOUT, stream.read_exact(&mut init)) .await .map_err(|_| "MTProto init timeout")??; - let parsed = crate::mtproto::parse_client_init(&init, &stats.secret) - .ok_or("invalid MTProto init or secret")?; + let parsed = match crate::mtproto::parse_client_init(&init, &stats.secret) { + Some(parsed) => parsed, + None => { + // Секрет — половина ссылки `tg://proxy`. Клиент с сохранённой + // ссылкой от прошлого запуска попадает ровно сюда, и Telegram + // показывает ему «прокси настроен неверно». + stats.note_unknown_client( + peer, + "MTProto-init не разобран. Скорее всего в Telegram вписан другой \ + секрет — сверьте ссылку tg://proxy с той, что показана сейчас", + ); + return Err("invalid MTProto init or secret".into()); + } + }; stats.last_dc.store(parsed.dc, Ordering::Relaxed); let result = ws_tunnel( @@ -859,6 +902,56 @@ mod tests { ); } + /// Клиент с сохранённой ссылкой от прошлого запуска. Раньше его соединение + /// закрывалось молча, и по диагностике это было неотличимо от рабочего. + #[tokio::test] + async fn a_client_with_the_wrong_secret_gets_named_instead_of_dropped_in_silence() { + let stats = Stats::new(); + let (port, server) = start_proxy(stats.clone(), true).await; + + let stranger = crate::mtproto::generate_secret(); + let init = unambiguous_client_init(&stranger, 2); + let mut client = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); + client.write_all(&init).await.unwrap(); + + wait_until("отказ по секрету", || { + stats.unknown_clients.load(Ordering::Relaxed) > 0 + }) + .await; + let events = stats.drain_events(); + assert!( + events + .iter() + .any(|event| event.contains("MTProto-init не разобран")), + "в журнале должно быть сказано, что init не разобран: {events:?}" + ); + + stats.stop(); + let _ = server.await.unwrap(); + } + + #[tokio::test] + async fn a_client_that_speaks_neither_protocol_is_counted_too() { + let stats = Stats::new(); + let (port, server) = start_proxy(stats.clone(), true).await; + + // Приветствие SOCKS5 с нулём методов: разбор обязан провалиться. + let mut client = TcpStream::connect(("127.0.0.1", port)).await.unwrap(); + client.write_all(&[0x05, 0x00]).await.unwrap(); + + wait_until("отказ по рукопожатию", || { + stats.unknown_clients.load(Ordering::Relaxed) > 0 + }) + .await; + assert!(stats + .drain_events() + .iter() + .any(|event| event.contains("SOCKS5-приветствие не разобрано"))); + + stats.stop(); + let _ = server.await.unwrap(); + } + #[tokio::test] async fn stops_listener_and_active_tasks_cleanly() { let reservation = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/ui/main.ts b/ui/main.ts index 87042d7..debfd08 100644 --- a/ui/main.ts +++ b/ui/main.ts @@ -13,6 +13,8 @@ type Status = { routeFailures: number; /// Отклонено политикой «в LAN-режиме только Telegram». blocked: number; + /// Клиенты, которые дошли, но не сумели договориться о рукопожатии. + unknownClients: number; uptimeSeconds: number; port: number; /// Адрес для других устройств. Приходит только в LAN-режиме. @@ -44,6 +46,7 @@ let status: Status = { failures: 0, routeFailures: 0, blocked: 0, + unknownClients: 0, uptimeSeconds: 0, port: 1080, shareAddress: null, @@ -290,6 +293,10 @@ function renderDiagnostics(): void { Отклонено ${status.blocked} +
+ Не опознаны + ${status.unknownClients} +

@@ -305,6 +312,13 @@ function renderDiagnostics(): void { дело в сети или брандмауэре. Какие именно адреса отклонены, видно ниже.

+

+ «Не опознаны» — клиенты, которые дошли до прокси, но договориться с ними + не удалось. Почти всегда это старая ссылка: секрет в Telegram остался от + прошлого запуска и больше не совпадает. Тогда Telegram пишет «прокси + настроен неверно», а адрес такого клиента появится в журнале ниже. +

+
Последние события