В диагностике из #42 встречаются строки вида соединений 9 · туннелей 9 · DC5 · Запасной Telegram IP · сбоев 12 Такого сочетания не бывает: у DC1, DC3, DC5 и DC203 закреплённый адрес ровно один, и маршрута «запасной адрес» у них не существует в принципе. Значит номер и маршрут пришли из разных соединений. Так и было. `last_dc` писало соединение при разборе init, `last_route` — другое соединение после рукопожатия, двумя независимыми атомиками. У репортёра от пяти до двадцати шести одновременных соединений, поэтому пара складывалась случайно. Читается она как «до этого DC шли этим маршрутом» и в этом качестве врала — ровно тот класс дефектов, ради которого затевалась честная диагностика в #38. Теперь пара пишется одним значением в момент, когда туннель поднялся: `dc << 8 | route`. Пока туннеля не было, показывается разобранный DC и «маршрут ещё не выбран» — это состояние тоже настоящее и его терять не надо. Поля стали приватными, наружу выведены `last_dc()` и `last_route()`, чтобы рассогласовать их снаружи было нельзя. Co-authored-by: by-sonic <171230345+by-sonic@users.noreply.github.com>
15 KiB
TGLock 2.0 architecture
Сборка: ядро, GUI и headless
Крейт собирается в библиотеку и два бинаря:
src/lib.rs— ядро:mtproto,transport,proxy,config. Не зависит ни от Tauri, ни от оконной системы;src/main.rs— GUI, доступен только при включённой фичеgui(required-features);src/bin/cli.rs— headlesstglock-cli.
Фича gui включена по умолчанию и подтягивает tauri, tauri-build и open.
При --no-default-features ни Tauri, ни системный WebView, ни фронтенд в сборку
не попадают, и build.rs не вызывает tauri_build. Так TGLock собирается и
работает там, где WebView недоступен в принципе: сервер без графического
окружения, контейнер, машина без монитора. Для случаев, где WebView есть, но нет
3D-ускорения, GUI дополнительно просит программный рендер — см. Current
limitations.
Local protocols
Один TCP-порт автоматически принимает два типа клиентов:
- MTProto proxy с постоянным 16-байтовым secret — основной режим;
- SOCKS5 без авторизации — режим совместимости.
MTProto init проверяется по secret и transport tag. Из него извлекаются DC, признак media-соединения и тип транспорта. Для upstream создаётся новый стандартный obfuscated2 init, а последующий поток пере-шифровывается между локальным secret и Telegram.
Различение протоколов
Определять протокол по первому байту нельзя. SOCKS5-приветствие начинается с
0x05, но MTProto init — это 64 случайных байта, и is_reserved_init
исключает только 0xef, 0xee, 0xdd, HTTP-глаголы и заголовок TLS-записи.
Значение 0x05 попадается примерно в одном init из 256, и такое соединение
уходило в SOCKS5-ветку и умирало — снаружи это выглядит как «Telegram
отправляет сообщения через раз».
Поэтому неоднозначный первый байт разрешается так: не потребляя данные, ожидается полный 64-байтовый init и делается попытка разобрать его под текущим secret. Успешный разбор означает MTProto. Настоящий SOCKS5-клиент присылает короткое приветствие и блокируется на ответе, поэтому 64 байта у него не появятся и по истечении короткого таймаута он корректно уходит в SOCKS5-ветку.
Политика прямого релея
Не-Telegram адреса релеятся напрямую, без шифрования, и пользы для обхода в
этом нет. Поэтому такой релей разрешён только когда прокси слушает loopback,
где до него дотягиваются лишь локальные процессы. На 0.0.0.0 и любом другом
сетевом адресе не-Telegram запросы отклоняются кодом SOCKS5 0x02, иначе
LAN-режим превращал бы машину в открытый прокси. Правило выражено в типе
config::ListenConfig, а не в условиях по месту вызова; переопределяется
только явным --allow-direct в CLI.
Что считается адресом Telegram
Список сетей — опубликованный самим Telegram
(https://core.telegram.org/resources/cidr.txt), он лежит в telegram_net и
проверяется по маске префикса. До 2.0.0-beta.9 сравнивались два первых октета,
то есть «телеграмом» считались целиком 149.154.0.0/16, 91.108.0.0/16,
91.105.0.0/16 и 185.76.0.0/16, а IPv6 не распознавался вовсе. Ошибка была в
обе стороны:
- чужие адреса внутри этих
/16уходили в MTProto-туннель и умирали там; - настоящие адреса дата-центров по IPv6 отклонялись как посторонние.
Второе и давало «на компьютере работает, с телефона нет» (#42): на loopback неопознанный адрес всё равно релеился напрямую, поэтому там дефект не проявлялся, а на сетевом слушателе тот же адрес получал отказ.
Назначение делится на три вида:
| Вид | Что это | Что делаем |
|---|---|---|
| Дата-центр | IP из опубликованных сетей, v4 или v6 | заворачиваем в WebSocket |
| Веб Telegram | имя из telegram.org, t.me, telegram.me, telesco.pe, cdn-telegram.org |
пропускаем как есть — это обычный HTTPS, а не MTProto |
| Всё остальное | — | напрямую на loopback, отказ на сетевом адресе |
Имена сопоставляются по границе метки, поэтому telegram.org.example.com —
посторонний домен. В ограниченном режиме имя разрешается заранее, и адреса
внутри локальной сети (127.0.0.0/8, 10/8, 172.16/12, 192.168/16,
100.64/10, fc00::/7, fe80::/10) отбрасываются: назначение выбирает чужое
устройство, и DNS-ответ не должен превращать TGLock в дверь во внутреннюю сеть
этой машины.
Отказ перестаёт быть молчаливым
Отклонённый запрос увеличивает счётчик blocked и один раз называет адрес в
журнале; повторы того же адреса склеиваются, чтобы не забить журнал одной
строкой. Отдельно отмечается первое подключение с каждого сетевого адреса.
Без этого «с телефона не работает» неразличимо распадалось на два случая: телефон не дошёл до машины (сеть, брандмауэр, изоляция клиентов на роутере) — и дошёл, но попросил адрес, который мы не пропускаем. Первый виден как ноль соединений и ноль отказов, второй — как соединения есть, отказы растут.
Третий случай нашёлся, когда репортёр #42 прислал диагностику: у него было ноль
отказов и работающие туннели, то есть оба счётчика говорили «всё хорошо».
Клиент, который дошёл до прокси, но не сумел договориться, не попадал ни в
один из них. Соединение просто закрывалось: active дёргался вверх и обратно.
Теперь такие клиенты считает unknown_clients, и журнал называет адрес и
причину. Их две:
- MTProto-init не разбирается под текущим секретом. Почти всегда это ссылка
tg://proxyот прошлого запуска: секрет — её половина, и клиент со сохранённой старой ссылкой попадает ровно сюда. Со стороны Telegram это и есть «прокси настроен неверно и будет отключён» (#37). - SOCKS5-приветствие не разбирается. Сюда же попадает MTProto-соединение,
ушедшее в SOCKS5-ветку по неоднозначному первому байту, если полный init не
успел прийти за
PROTOCOL_PROBE_TIMEOUT.
Счётчик ws_failures от них отличается тем, что растёт после успешного
рукопожатия с клиентом: там договорились с клиентом, но не смогли с Telegram.
Учёт состояния
Stats::ws считает установленные туннели: счётчик поднимается после
успешного transport.connect, а не перед попыткой. Иначе интерфейс
рапортовал бы «Telegram на связи», пока рукопожатие ещё перебирает маршруты по
несколько секунд каждый. Состояния «порт открыт», «идёт перебор маршрутов» и
«туннель установлен» различимы и в GUI, и в выводе CLI.
Дата-центр и маршрут пишутся одним значением, в момент, когда туннель
поднялся. Пока это были два независимых поля, номер писало соединение при
разборе init, а маршрут — другое соединение после рукопожатия, и при десятках
одновременных соединений в строку статуса попадала пара из разных из них.
Читалась она как «до этого DC шли этим маршрутом», хотя означала другое: в
диагностике #42 встречались строки DC5 · Запасной Telegram IP, а у DC5
закреплённый адрес всего один и запасного у него не бывает вовсе.
Секрет прокси — половина ссылки tg://proxy. Для сервиса его нужно закрепить
файлом (--secret-file): под DynamicUser и ProtectHome домашней папки нет,
путь по умолчанию не определяется, и секрет генерировался бы заново при каждом
старте, отключая всех уже настроенных клиентов.
Transport cascade
Для каждого DC и отдельно для media-соединений строится список маршрутов:
- сохранённый успешный маршрут;
- точный Telegram IP с
kwsNилиkwsN-1в TLS SNI и WebSocket Host; - дополнительный Telegram IP, если он определён;
- системный DNS;
- явно настроенный пользователем Cloudflare Worker.
Поддерживаются DC1–5 и media/CDN DC203. DC203 использует WebSocket-host DC2, но подключается к собственному IP.
После ошибки маршрут получает exponential cooldown от 30 секунд до 30 минут. Успешный маршрут становится первым для следующего соединения того же DC и типа трафика.
TLS policy
Проверка сертификатов и hostname никогда не отключается. При подключении к
заданному Telegram IP TCP destination отделён от URI host: TLS продолжает
проверять сертификат настоящего kws*.web.telegram.org.
Cloudflare Worker принимается только как пользовательская настройка. TGLock не загружает и не скрывает публичные списки чужих доменов.
Cloudflare Worker contract
Worker должен принимать WebSocket на:
/apiws?dst=<telegram-ip>&dc=<dc-id>
и проксировать бинарные frames в TCP <telegram-ip>:443. Рекомендуется
добавить собственную авторизацию до стабильного релиза; поэтому Worker
остаётся расширенной опцией alpha-версии.
Current limitations
- SNI camouflage не включена: небезопасное отключение hostname verification из референсной реализации не переносится.
- Голосовые звонки по UDP не поддерживаются.
- GUI зависит от WebView, а тот — от 3D-ускорения. Начиная с
2.0.0-beta.2 приложение перед стартом Tauri само просит программный рендер:
на Windows через
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS, на Linux черезWEBKIT_DISABLE_COMPOSITING_MODEиWEBKIT_DISABLE_DMABUF_RENDERER. Уже заданные оператором значения не перезаписываются, аTGLOCK_FORCE_GPU=1возвращает аппаратное ускорение. Для машин без монитора остаётсяtglock-cli, которому WebView не нужен вовсе (issues #10, #17). - Windows-инсталлятор не подписан, macOS-сборка не нотарифицирована, и
подписывать их не планируется: сертификат — ежегодный платёж, а проект
бесплатный. Часть антивирусов будет реагировать на неподписанный установщик,
который открывает локальный сокет и прописывается прокси-сервером — это тот
же профиль, по которому ищут прокси-трояны. Вместо доверия предлагаются
проверяемые пути:
sha256каждого артефакта публикуется GitHub на странице релиза, сборка идёт в GitHub Actions из публичного коммита с открытым логом, CLI собирается одной командой. Подробно — в разделе README про антивирус. - Работоспособность медиа зависит от конкретного DC аккаунта и доступности Telegram/Cloudflare у провайдера.
- Пулы заранее открытых WebSocket-соединений будут добавлены после измерения, что они не создают лишнюю нагрузку и не ухудшают стабильность.