Files
tglock/docs/ARCHITECTURE_V2.md
T
by-sonic a87e26c581 feat(android): приложение под Android и переход на rustls
Android грузит приложение как нативную библиотеку и входит через JNI-символ, а
не через main. Поэтому Tauri-приложение переехало из src/main.rs в src/gui.rs
внутри библиотеки, с #[cfg_attr(mobile, tauri::mobile_entry_point)], а main.rs
стал обёрткой. Модуль объявлен как #[cfg(feature = "gui")], так что свойство
«--no-default-features даёт ядро без Tauri и WebView» сохранилось — проверено
сборкой и 51 тестом headless-варианта.

Переход на rustls — вынужденный, но выгодный. native-tls на Linux и Android
тянет OpenSSL, а openssl-sys не кросскомпилируется под aarch64-linux-android:
сборка падала на нём. rustls на чистом Rust, корневые сертификаты webpki вшиты
в бинарь. Побочно: на Linux исчезла зависимость от системного libssl, а образу
Docker больше не нужен даже ca-certificates.

На этом переходе тест живой сети поймал баг, который прошёл бы в релиз: rustls
0.23 отказывается угадывать криптопровайдер и ПАНИКУЕТ на первом TLS-
рукопожатии. Компиляция чистая, все офлайновые тесты зелёные — они ходят через
локальный маршрут без TLS. То есть в сборку ушло бы приложение, не способное
подключиться ни к чему. Провайдер (ring, кросскомпилируется под Android)
устанавливается в ensure_crypto_provider, регрессию держит тест
a_crypto_provider_is_available_for_tls. Живой тест против шести боевых
дата-центров Telegram проходит.

Прочее:
- open::that заменён на tauri-plugin-opener: у крейта open нет реализации под
  Android, а плагин работает на обеих платформах.
- Библиотека переименована в tglock_lib: одинаковые имена lib и bin давали
  коллизию выходных файлов, которую cargo обещает сделать ошибкой.
- Программный рендер WebView не применяется на Android — там GPU есть всегда,
  и переопределение только замедлило бы интерфейс.
- .gitignore: строка /gen скрывала весь сгенерированный Android-проект. Теперь
  игнорируются только артефакты сборки, local.properties и keystore.

Foreground service. Прокси — поток в процессе приложения, и без сервиса система
выгрузит его через минуты после сворачивания. Сервис стартует из MainActivity,
а не по команде из ядра: JNI-мост между Rust и Kotlin осознанно не делался,
чтобы не добавлять слой, который нельзя проверить. Плата — уведомление висит,
пока открыто приложение, даже при выключенной защите.

НЕ ПРОВЕРЕНО: вся Android-часть в рантайме. Устройств не подключено,
эмулятора и системных образов в SDK нет. APK собирается, но приложение никто
ни разу не запускал: ни интерфейс, ни сервис, ни тип specialUse на Android 14+,
ни запрос разрешения на уведомления. Поэтому Android-артефакт сознательно НЕ
добавлен в release.yml — публиковать нечего, пока никто не запустил это на
живом устройстве.
2026-07-30 15:19:40 +03:00

11 KiB
Raw Blame History

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 — headless tglock-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.

Учёт состояния

Stats::ws считает установленные туннели: счётчик поднимается после успешного transport.connect, а не перед попыткой. Иначе интерфейс рапортовал бы «Telegram на связи», пока рукопожатие ещё перебирает маршруты по несколько секунд каждый. Состояния «порт открыт», «идёт перебор маршрутов» и «туннель установлен» различимы и в GUI, и в выводе CLI.

Секрет прокси — половина ссылки tg://proxy. Для сервиса его нужно закрепить файлом (--secret-file): под DynamicUser и ProtectHome домашней папки нет, путь по умолчанию не определяется, и секрет генерировался бы заново при каждом старте, отключая всех уже настроенных клиентов.

Transport cascade

Для каждого DC и отдельно для media-соединений строится список маршрутов:

  1. сохранённый успешный маршрут;
  2. точный Telegram IP с kwsN или kwsN-1 в TLS SNI и WebSocket Host;
  3. дополнительный Telegram IP, если он определён;
  4. системный DNS;
  5. явно настроенный пользователем Cloudflare Worker.

Поддерживаются DC15 и 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.

Стек — rustls с корневыми сертификатами webpki, вшитыми в бинарь. Выбран не из вкуса: native-tls на Linux и Android тянет OpenSSL, а openssl-sys не кросскомпилируется под aarch64-linux-android без сборки OpenSSL вручную. Побочные выгоды: на Linux исчезла зависимость от системного libssl, а образу Docker больше не нужен даже ca-certificates.

Криптопровайдер rustls выбирается явно (ring). Без этого rustls 0.23 отказывается угадывать и паникует на первом TLS-рукопожатии — это не ошибка компиляции и её не видят офлайновые тесты, потому что они ходят через локальный маршрут без TLS. Провайдер устанавливается в ensure_crypto_provider перед подключением, а тест a_crypto_provider_is_available_for_tls не даёт регрессии вернуться.

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-соединений будут добавлены после измерения, что они не создают лишнюю нагрузку и не ухудшают стабильность.