Some checks failed
Published content check / validate (push) Failing after 6s
Мелкие хвосты после перепроверки раздела: - у всех пяти изображений раздела появился альтернативный текст — он нужен и для доступности, и для поиска; - режим доставки (carrier_mode) везде называется одинаково: варианты «способ доставки» и «режим транспорта» из заметки про бота убраны; - ссылка [[ZaStoGram]] в обзоре раздела MTProxy вела на заметку, которой в хранилище нет, и отдавала 404 на сайте: заменена на внешнюю ссылку на исходники клиента в Forgejo.
278 lines
44 KiB
Markdown
278 lines
44 KiB
Markdown
---
|
||
date: 2026-08-22
|
||
tags:
|
||
- tproxy
|
||
- telegram
|
||
- протокол
|
||
- web-прокси
|
||
- мультиплексирование
|
||
- техническое
|
||
aliases:
|
||
- WEB proxy protocol v1 разбор
|
||
- tproxy кадры OPEN DATA WINDOW CLOSE
|
||
- bridge capability как вычисляется
|
||
- carrier mode https-lanes websocket-lanes
|
||
- tproxy-server лимиты и окна
|
||
- Как устроен WEB-прокси Telegram внутри
|
||
link: https://github.com/telegramdesktop/tproxy-server/blob/master/PROTOCOL.md
|
||
---
|
||
|
||
# 🔬 Как устроен WEB-прокси Telegram изнутри: пропуск, кадры и четыре режима доставки
|
||
|
||
![[tproxy-protocol-header.png|Обложка: устройство протокола WEB-прокси — кадры, окна и режимы доставки]]
|
||
|
||
> [!info] О чём заметка
|
||
> Технический разбор протокола WEB-прокси Telegram (*tproxy, WEB proxy protocol v1*) — того самого нового типа прокси, где трафик мессенджера едет внутри обычных запросов к сайту. Что это вообще такое и зачем — в обзорной заметке [[tproxy/tproxy|WEB-прокси Telegram: трафик внутри обычного сайта]]; здесь предполагается, что общая идея уже понятна, и разбирается сам контракт: как выводится пропуск, как выглядят кадры на проводе, как работает управление потоком и чем четыре режима доставки отличаются на уровне HTTP-запросов. Практическая установка — в [[tproxy/tproxy-server-setup|отдельной заметке]].
|
||
|
||
> [!warning] Откуда взяты данные
|
||
> Всё ниже — разбор исходников серверной части `tproxy-server` и спецификации `PROTOCOL.md` из того же репозитория по состоянию на 22 августа 2026 года, а не официальная документация Telegram. Проект помечен автором как доказательство работоспособности (`proof-of-concept`) — демонстрация, что идея работает, а не готовый продукт. Там, где спецификация и код расходятся, расхождение отмечено явно — в таких местах поведение определяет код, а не текст.
|
||
|
||
## TL;DR
|
||
|
||
1. Пропуск на вход (*bridge capability*) — это `HMAC-SHA256`, где ключ — байты секрета MTProxy, а сообщение — строка `tdesktop-web-proxy-bridge-v1\n` плюс имя хоста. Результат — 32 байта, в URL это 43 символа base64url.
|
||
2. Страница-мост — скрытая веб-страница, внутри которой работает скрипт-переносчик, — отдаётся **только** на точный запрос `GET /?bridge=<43 символа>`. Любое отклонение — лишний параметр, дубль, процентное кодирование — возвращает обычную главную страницу сайта.
|
||
3. Кадр на проводе: `тип (1 байт) | номер потока (3 байта) | длина (4 байта) | данные`. Типов девять; реле не отправляет `PING` и `BYE`, так что в штатном обмене остаются семь — считая `PONG`, который реле принимает, но спровоцировать не может.
|
||
4. Управление потоком — кредитное, по 4 МиБ в каждую сторону на поток. Реле возвращает кредит **только после того, как байты реально записаны в сокет MTProxy**.
|
||
5. Очередь сессии разделена на три части с разными потолками, чтобы служебные кадры всегда могли встать в очередь, даже когда буфер данных забит под завязку.
|
||
6. Отказ по одному потоку никогда не убивает сессию: клиент получает `CLOSE` для этого номера, остальные потоки работают дальше.
|
||
|
||
## Два значения на входе и что из них получается
|
||
|
||
Пользователь вводит имя хоста и секрет MTProxy — те самые 32 шестнадцатеричных символа. Дальше клиент вычисляет производное значение, которое и уходит в сеть.
|
||
|
||
```text
|
||
S = байты декодированного секрета, включая ведущий байт dd, если он есть
|
||
H = имя хоста в нижнем регистре, ASCII/IDNA A-label, без завершающей точки
|
||
context = UTF-8("tdesktop-web-proxy-bridge-v1\n" + H)
|
||
capability = base64url-без-выравнивания(HMAC-SHA256(ключ = S, сообщение = context))
|
||
URL = https://H/?bridge=capability
|
||
```
|
||
|
||
Здесь `HMAC-SHA256` — стандартный способ получить из ключа и сообщения 32-байтовый отпечаток, который невозможно подделать, не зная ключа. Строка `tdesktop-web-proxy-bridge-v1` — метка разделения контекстов: к доменному имени сервера она отношения не имеет, а гарантирует, что тот же секрет, использованный в другой задаче, даст другой результат. В спецификации отдельно оговорено, что имя метки заморожено ради совместимости и **не делает протокол специфичным для Telegram Desktop**.
|
||
|
||
Из этой формулы следуют три вещи.
|
||
|
||
**Секрет не уходит в сеть.** В запросе едет только производная. Скрипт на странице моста исходного секрета никогда не видит; в коде байты секрета обнуляются в памяти сразу после вычисления пропуска, а буфер файла профилей затирается после разбора.
|
||
|
||
**Пропуск привязан к домену.** Смена домена при том же секрете даёт другой пропуск, а значит, все ранее выданные пары «домен плюс секрет» становятся бесполезны на новом адресе. Заодно: один секрет, использованный на двух доменах, не связывает их через наблюдаемое в адресе значение.
|
||
|
||
**Ротация секрета MTProxy автоматически ротирует пропуск.** Отдельный механизм отзыва не нужен — но и выборочного отзыва в этой схеме не существует.
|
||
|
||
Проверочные векторы из спецификации (оба проверены пересчётом):
|
||
|
||
| Имя хоста | Байты секрета (hex) | Пропуск |
|
||
|---|---|---|
|
||
| `proxy.example.com` | `000102030405060708090a0b0c0d0e0f` | `MHLEY5PmW1GWqJkSrlmJpvJUiLhBH_QKy6yKg8a0JPk` |
|
||
| `proxy.example.com` | `dd000102030405060708090a0b0c0d0e0f` | `IpJrt3e7sKtzPyoXy6w-Zj6GGEvsvclN66JzQEfPYLA` |
|
||
|
||
Обратите внимание на вторую строку: ведущий байт `dd` — унаследованный от MTProxy признак режима случайного дополнения — входит в ключ HMAC. Поэтому обычный и `dd`-вариант одного и того же секрета дают **разные** пропуски, и это сделано намеренно. А вот префикс `ee`, то есть режим маскировки FakeTLS, здесь не поддерживается вообще: в архитектурном документе он запрещён прямым текстом, потому что защищённое соединение уже обеспечивает связка «браузер — веб-сервер». Шестнадцатибайтовый секрет, случайно начинающийся с `ee`, при этом принимается как самый обычный — проверка касается только 17-байтового варианта, который обязан начинаться с `dd`.
|
||
|
||
### Требования к имени хоста
|
||
|
||
Проверка строгая, и её стоит знать заранее, потому что она отсекает привычные варианты: имя обязано быть длиной до 253 символов, содержать хотя бы одну точку (однословные имена вроде `localhost` отвергаются), не быть IP-адресом, состоять только из строчных ASCII-символов, цифр и дефисов, не иметь завершающей точки. Интернационализированные домены нужно записывать в ASCII-форме `xn--…`.
|
||
|
||
Причина последнего требования не косметическая. Telegram Desktop собран на графической библиотеке Qt, и её разные версии по-разному приводят к каноническому виду отдельные символы — немецкую `ß`, греческую `ς`, невидимые соединители. Домен, набранный вручную такими символами, на Windows и на Linux может дать разные пропуски, и подключение молча не состоится.
|
||
|
||
## Как сервер решает, кому отдать мост
|
||
|
||
Реле — единственный обработчик всех запросов: веб-сервер перед ним проксирует **каждый** путь без исключений, никакой отдельной раздачи статики нет.
|
||
|
||
Мост отдаётся только при выполнении всех условий сразу: метод строго `GET`, путь строго `/`, в строке запроса ровно один параметр с одним значением, его имя `bridge`, длина значения ровно 43 символа, а сама строка запроса побайтно равна `bridge=<значение>`. Дальше значение декодируется как base64url без выравнивания, обязано дать ровно 32 байта, и обратное кодирование обязано совпасть с исходным текстом.
|
||
|
||
Проще говоря: подойдёт ровно один вид адреса. Лишний параметр, продублированный `bridge`, один символ, записанный процентной последовательностью, — любое отклонение возвращает обычную главную страницу с кодом 200.
|
||
|
||
Сравнение пропуска с настроенными профилями идёт **в постоянное время**: цикл проходит по всем профилям до конца, без досрочного выхода при совпадении. Более того, если параметра `bridge` в запросе не было вовсе, сервер всё равно прогоняет то же сравнение по нулевому «манекену» — чтобы по времени ответа нельзя было отличить запрос с пропуском от запроса без него.
|
||
|
||
### Единообразие ответов как отдельная инженерная задача
|
||
|
||
Любой неаутентифицированный запрос — неизвестный токен, неверный метод, битые заголовки — получает **ровно тот же ответ, что и запрос несуществующей страницы**: тот же код, тот же набор заголовков, то же тело. На публичной поверхности вообще нет ответов вида «метод не разрешён» или «не авторизован».
|
||
|
||
В проекте это закреплено отдельным тестом, который перебирает декартово произведение из пяти методов, шести вариантов оформления запроса (среди них — с заголовком источника, с кукой, с попыткой перейти на веб-сокет и с корректно оформленным токеном) и всех транспортных путей, и требует **побайтового** совпадения кода, набора заголовков и тела с эталонным промахом по несуществующему адресу.
|
||
|
||
Отдельная деталь, которую легко упустить: правило кэширования тоже единое. Любой ответ с кодом 4xx и 5xx, как и любой запрос с непустой строкой параметров, получает `Cache-Control: no-store`, всё остальное — `public, max-age=300`. Это закрывает утечку, при которой страница с пропуском не кэшировалась бы, а обычная кэшировалась.
|
||
|
||
Даже исчерпание внутренних лимитов замаскировано: если сервер не смог выдать одноразовый стартовый токен из-за ограничения скорости, он отдаёт обычную главную страницу, а не ошибку.
|
||
|
||
## Стартовый токен и создание сессии
|
||
|
||
Вместе со страницей моста выдаётся стартовый токен (*bootstrap*) — 32 случайных байта, срок жизни две минуты. Он встраивается прямо в HTML и нужен ровно для одной операции: обменять его на сессию.
|
||
|
||
```text
|
||
POST /api/v1/session
|
||
Authorization: Bearer <стартовый токен>
|
||
Content-Type: application/octet-stream
|
||
Тело: ровно один кадр HELLO
|
||
|
||
200 OK
|
||
X-Session-Token: <токен сессии>
|
||
X-Carrier-Mode: https | https-lanes | websocket | websocket-lanes
|
||
X-Down-Cursor: 0
|
||
Тело: ровно один кадр WELCOME
|
||
```
|
||
|
||
Обмен атомарный и идемпотентный. Идемпотентность реализована через отпечаток тела: у использованного токена сохраняется `SHA-256` от того, что прислали в первый раз, и повтор с **тем же** телом вернёт тот же токен сессии. Повтор с другим телом — отказ. Это позволяет безопасно повторять создание сессии, когда ответ потерялся в сети.
|
||
|
||
Тонкость, которая объясняет одно проектное решение: стартовый токен **не привязывается к IP-адресу**. Браузер может загрузить страницу через один выходной адрес, а создать сессию через другой — из-за VPN, смены оператора или двойного стека IPv4/IPv6. В коде есть отдельный тест, где токен выдан с одного адреса, использован со второго и переспрошен с третьего, и всё это должно давать одну сессию.
|
||
|
||
Если после успешной аутентификации сервер упирается в лимит, он отвечает `503` с заголовком `Retry-After: 1`, **не сжигая стартовый токен** — побайтно идентичный запрос можно повторить.
|
||
|
||
Токены нигде не хранятся в открытом виде: в памяти лежат только их `SHA-256`. Проверка требует канонической кодировки, то есть неканонический base64 отвергается сразу.
|
||
|
||
## Формат кадров
|
||
|
||
Все целые — беззнаковые, старшим байтом вперёд (порядок big-endian).
|
||
|
||
```text
|
||
тип: 1 байт | номер потока: 3 байта | длина данных: 4 байта | данные
|
||
```
|
||
|
||
| Код | Имя | Направление | Поток | Данные |
|
||
|---|---|---|---|---|
|
||
| `0x01` | `OPEN` | клиент → реле | ненулевой | пусто |
|
||
| `0x02` | `DATA` | обе стороны | ненулевой | непрозрачные, непустые |
|
||
| `0x03` | `CLOSE` | обе стороны | ненулевой | пусто |
|
||
| `0x04` | `WINDOW` | обе стороны | ненулевой | ненулевая 4-байтовая дельта |
|
||
| `0x05` | `PING` | реле → клиент | 0 | произвольный маркер |
|
||
| `0x06` | `PONG` | клиент → реле | 0 | точное эхо маркера |
|
||
| `0x10` | `HELLO` | клиент → реле | 0 | один байт `01` |
|
||
| `0x11` | `WELCOME` | реле → клиент | 0 | пусто |
|
||
| `0x1f` | `BYE` | реле → клиент | 0 | необязательная причина |
|
||
|
||
Ключевые константы: заголовок 8 байт, максимум данных в кадре 1 МиБ, номер потока — 24 бита (до 16 777 215), в одном теле запроса не более 4096 кадров. Из сокета бэкенда реле читает кусками не больше 64 КиБ, но соседние кадры `DATA` одного потока склеиваются в очереди, поэтому кадр на проводе может дорасти до полного максимума в 1 МиБ — спецификация в этом месте обещает 64 КиБ и расходится с кодом.
|
||
|
||
Важное практическое замечание: **`PING` и `BYE` в первой версии реле не отправляет вообще** — это оговорено в спецификации и подтверждается кодом. Приёмная сторона к `PONG` при этом готова, и его данные ограничены 64 байтами (ограничение есть только в коде, в спецификации его нет).
|
||
|
||
Валидация формы кадров от клиента жёсткая. На нулевом потоке разрешён **только** `PONG`; всё остальное — фатальная ошибка. На ненулевом потоке: у `OPEN` и `CLOSE` данные обязаны быть пустыми, у `DATA` — непустыми, у `WINDOW` — ровно четыре байта с ненулевым значением. Пустое тело запроса тоже считается ошибкой, а не пустой операцией.
|
||
|
||
Фатальные ошибки, которые обрывают всю сессию: неизвестный номер живого потока, неизвестный тип, обрезанный кадр в конце тела, нулевая дельта `WINDOW`, `DATA` сверх выданного кредита, кадр не в том направлении. Общее правило: **некорректный кадр никогда не доходит до MTProxy**.
|
||
|
||
### Жизненный цикл потока
|
||
|
||
Каждому TCP-соединению, которое приложение хочет открыть, соответствует один номер потока и один кадр `OPEN`. Номера **не переиспользуются** в пределах сессии.
|
||
|
||
```text
|
||
OPEN s=17 → реле открывает одно TCP-соединение к настроенному бэкенду
|
||
DATA s=17 → ограниченная кредитом запись в этот сокет
|
||
чтение бэкенда → DATA s=17 в очередь на отдачу вниз
|
||
CLOSE s=17 → закрытие соединения
|
||
EOF бэкенда → CLOSE s=17 клиенту
|
||
неудача дозвона → CLOSE s=17, сессия продолжает работать
|
||
```
|
||
|
||
Подтверждения у `OPEN` нет: клиент может слать `DATA` сразу, в том же теле запроса. Пока идёт дозвон, данные копятся, но не больше начального окна.
|
||
|
||
`CLOSE` — это именно обрыв, а не полузакрытие: данные, стоявшие в очереди для этого потока, отбрасываются. В спецификации это объясняется тем, что штатный путь Telegram Desktop тоже никогда не делает полузакрытие на сокете MTProto.
|
||
|
||
Отдельно стоит отметить механизм памяти о закрытых потоках. Сервер помнит до 4096 недавно закрытых номеров и **молча игнорирует** корректно оформленные запоздалые `DATA`, `WINDOW` и `CLOSE` для них — иначе гонка при одновременном закрытии с двух сторон убивала бы сессию. Побочное следствие: когда номер вытесняется из этой памяти, реле уже не отличает запоздалый кадр от нового потока — поэтому запрет на переиспользование номеров лежит целиком на клиенте.
|
||
|
||
## Управление потоком: кредиты и обратное давление
|
||
|
||
Каждый поток начинается с **4 МиБ кредита в каждую сторону**. Единица — байты.
|
||
|
||
Механика в направлении «клиент к серверу»: клиент вправе прислать не больше, чем есть кредита. Реле возвращает кредит кадром `WINDOW` **только после того, как байты реально ушли в сокет MTProxy**. Это и есть настоящее обратное давление: медленный сервер назначения означает, что запись блокируется, кредит не возвращается, клиент упирается в свои 4 МиБ на поток и вынужден остановиться.
|
||
|
||
В обратную сторону симметрично: чтения из сокета бэкенда расходуют кредит, выданный клиентом. Когда кредит исчерпан, поток чтения внутри реле (в языке Go это горутина) **паркуется** и физически перестаёт читать из сокета — TCP-окно к бэкенду схлопывается само. Никакого чтения «в память про запас» нет ни на одном участке; в архитектурном документе прямо записано требование не использовать неограниченное копирование.
|
||
|
||
Тут есть асимметрия, которую стоит запомнить. Кредит, который реле возвращает в свою сторону, не может превысить начальные 4 МиБ — попытка выдать больше отвергается. А вот кредит, который выдаёт клиент, потолком в 4 МиБ не ограничен: сложение насыщающее и упирается только в максимум 32-битного числа. Само окно в 4 МиБ выбрано намеренно больше одного тела запроса (2 МиБ), чтобы поток мог продолжать передачу, пока возвращаемый кредит едет во встречном направлении.
|
||
|
||
### Три раздела очереди: почему служебные кадры не делят буфер с данными
|
||
|
||
Очередь сессии на отдачу вниз разделена на три класса с **разными потолками**, и это решает конкретную проблему: если бы служебные кадры делили общий буфер с данными, то забитая под завязку очередь данных не дала бы отправить даже `CLOSE`, и пришлось бы рвать сессию.
|
||
|
||
| Класс | Потолок при настройках по умолчанию |
|
||
|---|---|
|
||
| Служебные кадры (`CLOSE`, `WINDOW`) | 32 МиБ, 16 384 элемента |
|
||
| Приём тела запроса | 31,9 МиБ, 15 984 элемента |
|
||
| Данные вниз (`DATA`) | 28,9 МиБ, 11 888 элементов |
|
||
|
||
Резерв под служебные кадры считается как `16 + число_потоков_на_сессию × 3` элементов, при стандартных 128 потоках это 400 элементов, то есть около 105 КиБ по внутреннему учёту. Второй резерв гарантирует, что целое тело запроса на 2 МиБ всегда найдёт куда встать.
|
||
|
||
Дополнительно есть предохранитель на старте: если резерв под служебные кадры, помноженный на максимальное число сессий, съедает весь глобальный буфер, процесс **отказывается запускаться** с внятным сообщением. Логика в комментарии кода честная: иначе это была бы тихая мина, которая рванёт под нагрузкой.
|
||
|
||
Учёт ведётся и в байтах, и в штуках, причём каждый элемент очереди дополнительно «стоит» 256 байт накладных расходов — консервативная оценка реальной стоимости выделения памяти.
|
||
|
||
### Что происходит при упоре в лимиты
|
||
|
||
Реакция зависит от того, где именно упёрлись:
|
||
|
||
| Где упёрлись | Реакция |
|
||
|---|---|
|
||
| Тело запроса не помещается в бюджет | `503` с `Retry-After: 1`, номер последовательности не подтверждён, **ни один кадр не применён**, сессия жива |
|
||
| Данные вниз не помещаются | Чтение из бэкенда паркуется; если совсем никак — закрывается только этот поток |
|
||
| `OPEN` сверх лимита потоков | `CLOSE` только для этого номера, сессия и остальные потоки живы |
|
||
| Неудача дозвона до бэкенда | `CLOSE` этого потока, сессия жива |
|
||
| Нарушение протокола | Сессия закрывается целиком |
|
||
|
||
Отдельно проверено тестами, что отказ по лимиту профиля не расходует глобальную ёмкость и что упор в потолок потоков не рвёт родительскую сессию.
|
||
|
||
## Четыре режима доставки
|
||
|
||
Режим задаётся **на сервере**, в профиле, и вшивается в сгенерированную страницу моста. Клиенту не нужно ни настроек, ни нового кода: он получает от сервера значение в заголовке `X-Carrier-Mode` и сверяет его с тем, что вшито в страницу.
|
||
|
||
Создание сессии во всех четырёх режимах одинаковое — обычным запросом по HTTPS. Дальше пути расходятся.
|
||
|
||
**`https`** — базовый и самый консервативный. Один запрос на отправку и один долгий ожидающий запрос на приём, оба сериализованы. Потолок в каждую сторону — размер тела, делённый на время оборота, то есть около 2 МиБ на RTT.
|
||
|
||
**`https-lanes`** — те же два эндпоинта, но по независимой паре запросов на каждый логический поток; поток указывается заголовком `X-Lane-ID`. Каждая полоса имеет собственные счётчики и собственное состояние повторов, поэтому две сессии Telegram могут обе идти с первого номера и не мешать друг другу. Режим убирает взаимную блокировку между логическими соединениями, но **предполагает нормально работающий HTTP/2**: на старом HTTP/1.1 браузер ограничен шестью соединениями к одному адресу, и множество одновременных ожидающих запросов туда просто не поместится.
|
||
|
||
**`websocket`** — один постоянный двусторонний канал на всё. Убирает цикл «запрос — ответ» и его потолок по задержке, но все потоки делят одно соединение со всеми последствиями: общая очередь, общий контроль перегрузки, общая точка отказа. Обрыв канала закрывает сессию и все потоки, и возобновления частично доставленной сессии в эталонной реализации **нет**.
|
||
|
||
**`websocket-lanes`** — отдельный канал на каждый поток, номер потока указывается прямо в имени подпротокола. Изолирует очереди: загрузка большого файла не подвешивает интерактивную переписку. Плата — больше соединений и рукопожатий. Изоляция контроля перегрузки при этом **не гарантирована**: если движок браузера сложит несколько каналов в одно HTTP/2-соединение, они снова окажутся в общей очереди.
|
||
|
||
Разница в реакции на ошибку между двумя полосными режимами существенна и легко упускается: в `websocket-lanes` нарушение протокола в одной полосе убивает **только её**, а в `https-lanes` — **всю сессию**.
|
||
|
||
| Свойство | `https` | `https-lanes` | `websocket` | `websocket-lanes` |
|
||
|---|---|---|---|---|
|
||
| Соединений | 0 постоянных | 0 постоянных | 1 | по одному на поток |
|
||
| Состояние повторов | одно на сессию | своё на полосу | заменено гарантиями канала | заменено гарантиями канала |
|
||
| Головная блокировка | есть | нет | нет на уровне HTTP | нет |
|
||
| Ошибка в одной полосе | полос нет | рвёт всю сессию | полос нет | рвёт только полосу |
|
||
| Требует HTTP/2 | нет | фактически да | нет | желательно |
|
||
|
||
## Долгий ожидающий запрос и его тайминги
|
||
|
||
В режимах на HTTP приём устроен через долгое ожидание: клиент отправляет запрос и сервер держит его до 25 секунд, ожидая появления данных. Если данных не появилось, приходит пустой ответ, и клиент спрашивает снова.
|
||
|
||
Политика конкуренции здесь нетипичная: **побеждает новый запрос**. Когда приходит второй ожидающий запрос при уже припаркованном первом, новый забирает роль, а старый немедленно возвращается пустым ответом с неизменным курсором. Мотивация в комментарии кода: старое соединение, скорее всего, уже молча умерло, и отказывать новому было бы хуже. Для отправки правило противоположное — два одновременных запроса на отправку не допускаются, второй получает `503`.
|
||
|
||
Надёжность обеспечивается двумя счётчиками. Отправка нумеруется последовательностью, начиная с единицы; сервер принимает либо следующий номер, либо побайтно идентичный повтор последнего подтверждённого. Приём использует курсор: повтор старого курсора **воспроизводит ту же порцию байт в байт**. Клиент продвигает курсор только после того, как полностью передал полученное приложению, поэтому потерянный ответ просто запрашивается заново.
|
||
|
||
Выдержки времени связаны жёстко, и при ручной настройке фронтенда их легко порвать: ожидание держится 25 секунд, поэтому таймауты веб-сервера перед реле обязаны быть заметно больше — конкретные значения и настройки nginx приведены в [[tproxy/tproxy-server-setup|заметке про установку]]. Сессия переживает потерю транспорта в течение двух минут — это настраиваемый период, после которого фоновая уборка её закрывает; проверка идёт раз в 30 секунд, так что реальное окно составляет от двух до двух с половиной минут.
|
||
|
||
Две минуты выбраны не случайно: живой мост обновляет сессию каждым ожидающим запросом, его собственное окно повторов укладывается в этот срок, а протокольный слой Telegram Desktop и так сбрасывает соединения после 30–45 секунд тишины. Увеличивать это значение имеет смысл для мобильных клиентов, которые уходят в фон на минуты.
|
||
|
||
## Что защищает страницу моста
|
||
|
||
Страница моста генерируется сервером целиком и содержит только один встроенный скрипт — ни внешних файлов, ни картинок, ни стилей. Политика безопасности содержимого перечисляет шестнадцать директив, из которых почти все запрещающие: сеть разрешена только к собственному адресу и веб-сокету того же хоста, скрипты — только с одноразовым случайным маркером, встраивание страницы разрешено только с числового локального адреса.
|
||
|
||
Скрипт намеренно не использует ничего из того, что урезают в ужесточённых встроенных браузерах: ни куки, ни хранилища, ни фоновых обработчиков, ни фреймов, ни доступа к устройствам. В проекте есть тест, который прямо ищет в сгенерированной странице упоминания десятков таких возможностей и падает, если находит хоть одно.
|
||
|
||
Адрес с пропуском стирается из истории браузера сразу после загрузки: перезагрузка страницы даст обычный сайт. Все запросы моста идут с явным указанием не отправлять учётные данные и не следовать перенаправлениям.
|
||
|
||
Эксплуатационное требование, которое важнее прочих: **логирование адресов и заголовков должно быть выключено и на веб-сервере, и на реле**. Пропуск едет в строке запроса, а в режимах веб-сокета токен сессии едет в заголовке подпротокола. Включённый журнал доступа с сырыми адресами равносилен записи паролей в открытом виде. В самом реле логирования запросов нет вообще: единственные записи в журнале — события старта, остановки и падения слушателя, причём даже текст ошибки намеренно не печатается, только её класс.
|
||
|
||
## Расхождения между спецификацией и кодом
|
||
|
||
Полезно знать, если планируете писать свою реализацию:
|
||
|
||
- **Ограничение в 64 байта на данные `PONG`** есть в коде, но не в спецификации.
|
||
- **Длительность долгого ожидания и период жизни сессии после обрыва** числами не названы в самой спецификации протокола, хотя протокольно значимы: их приходится вычитывать из кода и примера конфигурации.
|
||
- **Зарезервированные коды типов не определены**: правила обработки неизвестного типа в спецификации нет, разбор пачки кадров такие кадры пропускает, отсеивает их только проверка формы.
|
||
- **Веб-сокет одновременно числится и в списке того, что не входит в первую версию, и среди поддерживаемых режимов** — противоречие внутри архитектурного документа, скорее всего след более ранней редакции.
|
||
- **Период тишины перед закрытием сессии** в двух местах архитектурного документа назван десятиминутным, ещё в двух — двухминутным; в коде, в README и в примере конфигурации реализовано второе.
|
||
- **Арифметическое переполнение окна** архитектурный документ относит к фатальным ошибкам, но код вместо этого насыщает сумму до максимума 32-битного числа и сессию не рвёт.
|
||
- **Размер кадра `DATA` от реле** спецификация ограничивает 64 КиБ, но склейка соседних кадров в очереди позволяет ему дорасти до максимума кадра в 1 МиБ.
|
||
- **Служебные адреса состояния и метрик** в спецификации отсутствуют — они живут на отдельном локальном слушателе.
|
||
|
||
## 📚 См. также
|
||
|
||
- [[tproxy/tproxy|WEB-прокси Telegram: трафик внутри обычного сайта]] — обзорная заметка: что это, зачем и можно ли пользоваться сейчас.
|
||
- [[tproxy/tproxy-server-setup|Установка tproxy-server: свой WEB-прокси на своём домене]] — практика развёртывания, требования к фронтенду и подводные камни установщика.
|
||
- [[tproxy/tproxy-in-bot|Выдача WEB-прокси из Telegram-бота]] — что из описанного здесь придётся хранить на стороне бота.
|
||
- [[mtproxy/telegram-wss-transport|WSS для Telegram: MTProto внутри WebSocket]] — другой транспорт с похожей идеей, но без мультиплексирования и без своего сервера.
|
||
- 🔗 [PROTOCOL.md в репозитории проекта](https://github.com/telegramdesktop/tproxy-server/blob/master/PROTOCOL.md) — первоисточник спецификации.
|
||
|
||
---
|
||
|
||
> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
|
||
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/tproxy/tproxy-protocol.md) · [скачать весь репозиторий одним zip-архивом](https://git.zapret.moe/zapretdiscordyoutube/todo/archive/main.zip).
|