---
date: 2026-08-06
tags:
  - mtproto
  - telegram
  - wss
  - websocket
  - обход-блокировок
  - zapret
  - tdesktop
  - android
aliases:
  - WSS прокси для Telegram
  - Telegram через WebSocket
  - tg-ws-proxy что это
  - Как работает WSS в Telegram
  - kws2.web.telegram.org и apiws
  - WSS транспорт Zastogram
  - Что лучше ZaStoGram Android или Desktop
  - Почему WSS работает только для DC2 и DC4
link: https://github.com/Flowseal/tg-ws-proxy
---

# 🕸️ WSS для Telegram: MTProto внутри WebSocket

![[telegram-wss-transport-header.webp]]

> [!info] О чём заметка
> Разбор транспорта, который в 2026 году появился сразу в нескольких инструментах для Telegram: обычный MTProto-поток кладут внутрь **WebSocket поверх TLS** (WSS) и отправляют на веб-эндпоинты Telegram вида `kws2.web.telegram.org/apiws` — те же, по которым работает браузерный Telegram Web. Здесь — что это такое, как устроено рукопожатие, что именно едет внутри кадров и чем четыре известные реализации отличаются друг от друга. Про то, почему у части пользователей не грузятся стикеры и почему звонки этот транспорт не переносит, — в парной заметке [[mtproxy/telegram-wss-limits|Ограничения WSS: что не работает и почему]].

> [!warning] Откуда взяты данные
> Всё ниже — разбор исходного кода четырёх проектов по состоянию на **6 августа 2026** (коммиты указаны в разделе о каждой реализации), а не официальная документация Telegram. Telegram не публиковал спецификацию своих веб-релеев: адреса, путь `/apiws` и правила выбора датацентра восстановлены по коду клиентов и прокси. Любая деталь может измениться на стороне Telegram без предупреждения — сверяйся с актуальным кодом, прежде чем строить на этом что-то серьёзное.

> [!note] Обновление 8–9 августа 2026: измерения вместо чтения кода
> Разделы [[#Правило первого кадра]], [[#Мифа про «только DC2 и DC4» не существует]] и [[#Рукопожатие: место, где всё ломается тихо]] опираются на живые соединения с релеями, а не на исходники. Итог:
> - **длина первого кадра важна**: меньше 64 байт — соединение молча умирает (порог измерен с точностью до байта);
> - **склейка пакетов запрещена**: релей разбирает только первый MTProto-пакет кадра, остальные выбрасывает без ошибки. Разрезать один пакет на несколько кадров при этом можно;
> - «веб-релеи существуют только для DC2 и DC4» **неверно** — работают все DC1–DC5, а редирект `302` возникает при обращении к чужому ingress-адресу.
>
> Промежуточный вывод от 8 августа, будто границы пакетов серверу безразличны, был **ошибочным**: он опирался на проверки, где в кадре был ровно один пакет, и потеря второго не проявлялась. Полное рукопожатие 9 августа это опровергло.
>
> Отдельно добавлен раздел [[#Релей может быть закрыт: главное практическое ограничение]] — о том, что чаще всего мешает не протокол, а недоступность самого релея из конкретной сети, и почему `ping` этого не показывает.

---

## TL;DR

1. **WSS-транспорт для Telegram** — это тот же обфусцированный MTProto, но упакованный в бинарные кадры WebSocket поверх настоящего TLS и отправленный на порт 443 доменов `kwsN.web.telegram.org` (`kwsN-1` — для медиа-трафика).
2. Задача, которую он решает, — **не DPI-фингерпринт, а блокировка по IP**: если провайдер режет диапазоны датацентров Telegram, соединение к веб-релею на 443 внешне неотличимо от «пользователь открыл web.telegram.org в браузере».
3. Внутри ничего не поменялось: остались **64-байтный obfuscated2-init и AES-256-CTR**, те же теги протокола `0xefefefef` / `0xdddddddd` / `0xeeeeeeee` и номер датацентра в байтах 60–61.
4. Требований к нарезке ровно два, и оба нарушаются молча: **первый кадр после апгрейда должен содержать не меньше 64 байт** (весь init), и **в одном кадре не должно быть больше одного MTProto-пакета** — остальные релей выбрасывает. Разрезать пакет на несколько кадров при этом можно свободно. См. [[#Правило первого кадра]].
5. Реализации делятся на два класса: **мост рядом с клиентом** (`tg-ws-proxy` от Flowseal, модуль `telegram_proxy` в ZapretGUI — оба на Python, слушают локальный порт как SOCKS5/MTProxy) и **нативный транспорт внутри клиента** (форки ZaStoGram для Android и для Desktop, C++ прямо в сетевом слое).
6. **Веб-релеи есть у всех датацентров DC1–DC5, а не только у DC2/DC4** — это проверено живыми соединениями 8 августа 2026. Распространённое «работают только DC2 и DC4» родилось из ошибки метода: у каждого датацентра свой адрес ingress, и если стучаться на адрес DC2 с именем `kws5`, приходит редирект `302` — сервер так и говорит, что вы пришли не туда. Подробно — в [[#Мифа про «только DC2 и DC4» не существует]].
7. **Чаще всего мешает не протокол, а доступность релея.** В измеренной сети открыт был ровно один ingress из четырёх проверенных путей: релей DC2 работал, релей DC1 не отвечал ни по IPv4, ни по IPv6, и прямые адреса DC1 тоже были закрыты. Пользователь при этом видит «фото грузятся, эмодзи нет». `ping` во всех случаях проходил и ничего не доказывал — см. [[#Релей может быть закрыт: главное практическое ограничение]].
8. Звонки через WSS не идут ни в одной реализации — WebSocket живёт поверх TCP, а голос и видео у Telegram ходят по UDP; их трафик проходит мимо туннеля напрямую, и закрывается он пакетным обходом, а не прокси.
9. Два клиентских форка **по протоколу больше не расходятся**: оба соблюдают правила фрейминга, покрывают DC1–DC5 и откатываются на прямое соединение при закрытом релее. Различия остались вокруг транспорта: Android строже валидирует кадры и ограничивает очереди, Desktop даёт свой релей и живой индикатор с пингом.

---

## Зачем понадобился ещё один транспорт

Обход блокировок Telegram распадается на две разные задачи, и их постоянно путают. Первая — когда провайдер видит **как** выглядит соединение: анализирует TLS-рукопожатие, сравнивает почерк клиента с известными, ловит характерный первый пакет. Против этого работают инструменты вроде [[Zapret2/Zapret2|zapret]], подменяющие поведение пакетов, и клиентские правки TLS-почерка (подробно — в [[mtproxy/ja4-sni-client-side|заметке про JA4 и SNI на стороне клиента]]).

Вторая задача — когда провайдеру **всё равно, как выглядит трафик**, потому что он режет сами адреса. Диапазоны датацентров Telegram (`149.154.160.0/20`, `91.105.192.0/23` и соседние) известны и компактны; заблокировать их целиком дешевле, чем разбирать протокол. В таком случае никакая фрагментация пакета не помогает: пакет просто не доезжает.

WSS-транспорт бьёт именно во вторую задачу. Идея простая: у Telegram, кроме «обычных» адресов датацентров, есть веб-инфраструктура, обслуживающая браузерную версию мессенджера, — она живёт на 443 порту, за нормальными TLS-сертификатами и доменами `web.telegram.org`. Блокировать её тем же топорным способом дороже: это тот же домен, что и сайт, которым пользуются миллионы людей. Если завернуть MTProto в WebSocket и отправить туда, то на уровне IP и SNI трафик выглядит как визит на сайт Telegram.

Проще говоря: не меняем «почерк» соединения, а меняем **адрес, куда стучимся** — на такой, который провайдеру неудобно резать целиком.

---

## Как выглядит соединение: пять слоёв

Готовое WSS-подключение — это матрёшка из пяти уровней, и путаница обычно начинается с того, что два из них шифруют одни и те же байты.

```
TCP :443                       обычное TCP-соединение к IP релея
 └─ TLS 1.2+                   настоящий TLS, SNI = kws2.web.telegram.org
     └─ HTTP/1.1 Upgrade       GET /apiws → ответ 101 Switching Protocols
         └─ WebSocket frames   бинарные кадры (opcode 0x2), клиент маскирует их
             └─ MTProto obfuscated2   64-байтный init + AES-256-CTR
                 └─ сам MTProto       шифрование клиент↔Telegram на auth_key
```

Внешний слой — TLS — нужен для маскировки: наблюдателю виден HTTPS к домену Telegram и больше ничего. Внутренний слой — obfuscated2 — остался ровно тем же, что и в обычном TCP-подключении Telegram: он превращает MTProto-поток в равномерный шум без заголовков, чтобы DPI не опознал протокол по сигнатуре. То, что шум едет внутри TLS и внутри WebSocket, ему не мешает.

Самый нижний слой — собственно MTProto с ключом авторизации — не трогает никто. Это важно для понимания рисков: **мост между клиентом и Telegram видит транспортную обёртку, но не содержимое переписки**; переписка расшифровывается только на устройстве и на серверах Telegram.

---

## Рукопожатие по шагам

Все четыре реализации делают одно и то же, различаясь мелочами. Порядок такой.

**Шаг 1. TCP к IP релея.** Соединение открывается не по DNS-имени, а по зашитому в код адресу — чаще всего `149.154.167.220` (для DC2/DC4), в Android-форке к нему добавлены `149.154.174.100` (DC1/DC3) и `149.154.170.100` (DC5). Имя `kwsN.web.telegram.org` при этом всё равно передаётся — но только выше, в TLS и HTTP.

**Шаг 2. TLS.** SNI и проверка имени сертификата выставляются в доменное имя релея. Здесь реализации расходятся: клиентские форки проверяют цепочку сертификатов по-настоящему (`SSL_VERIFY_PEER` в Android, `QSslSocket::VerifyPeer` в Desktop), а `tg-ws-proxy` проверку отключает намеренно — для него TLS не граница доверия, а обёртка, потому что полезная нагрузка и так зашифрована между клиентом и Telegram.

**Шаг 3. HTTP Upgrade.** Отправляется обычный запрос апгрейда до WebSocket:

```http
GET /apiws HTTP/1.1
Host: kws2.web.telegram.org
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: <16 случайных байт в base64>
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: binary
Origin: https://web.telegram.org
User-Agent: Mozilla/5.0 ... Chrome/131.0.0.0 ...
```

Заголовки `Origin` и браузерный `User-Agent` — часть маскировки: клиентские форки притворяются вкладкой браузера. `tg-ws-proxy` в этом месте скромнее и `Origin` не шлёт вовсе.

**Шаг 4. Проверка ответа.** Сервер отвечает `101 Switching Protocols`. Оба клиентских форка честно считают `Sec-WebSocket-Accept` — SHA-1 от отправленного ключа и константы `258EAFA5-E914-47DA-95CA-C5AB0DC85B11` из RFC 6455 — и рвут соединение при несовпадении. `tg-ws-proxy` довольствуется кодом 101.

**Шаг 5. Обмен кадрами.** Дальше идут бинарные кадры (opcode `0x2`), исходящие — с обязательной 4-байтной маской, как требует RFC 6455 от клиента. Служебные `ping` получают ответ `pong`, `close` считается обрывом.

> [!note] Почему адрес и имя расходятся
> Соединение открывается на IP-адрес, а имя `kwsN.web.telegram.org` фигурирует только в SNI и в заголовке `Host`. Это не хитрость ради хитрости: реализации не хотят зависеть от DNS, который может не отвечать или быть подменён. Android-форк дополнительно умеет откатиться на резолв доменного имени, если жёстко зашитый IP не отвечает, и запоминает удачный вариант на 30 минут. Модуль в ZapretGUI идёт дальше и прописывает нужные имена прямо в `hosts` Windows — приём рабочий, но он же однажды сломал их собственную диагностику, которая после этого проверяла не DNS, а собственную запись в `hosts`.

---

## Что едет внутри кадров

Внутри WebSocket-кадров лежит обычный обфусцированный MTProto — тот же, что уходил бы в голый TCP. Первым отправляется **64-байтный init-пакет**: 8 байт пропускаются, следующие 48 дают ключ и IV для AES-256-CTR (в обратном порядке — ключ для встречного направления), байты 56–59 содержат тег протокола, байты 60–61 — номер датацентра со знаком, где минус означает медиа-подключение. Затем весь пакет шифруется сам собой, и наружу уходит структура, статистически неотличимая от случайных байт.

Теги протокола те же, что в обычном MTProxy: `0xefefefef` — abridged (минимальный заголовок длины), `0xeeeeeeee` — intermediate, `0xdddddddd` — padded intermediate с добавлением случайного мусора к каждому пакету. Подробный разбор режимов — в [[Zapret/mtproto/01-protocol|описании протокола MTProxy]].

При генерации init-пакета отбраковываются «плохие» случайные значения: если первые байты совпадут с сигнатурой TLS-рукопожатия (`0x16030102`), с текстом `GET `, `POST`, `HEAD` или с чужим тегом протокола, пакет генерируется заново. Иначе DPI опознал бы поток по случайному совпадению с известным заголовком. Эта проверка живёт в коде всех реализаций и в WSS-режиме тоже работает.

Все четыре проекта отправляют один MTProto-пакет одним WebSocket-сообщением, а 64-байтный init — отдельным сообщением перед ним. Объясняли это обычно похожестью на браузерный Telegram Web, и объяснение было неверным, но само правило — верным: релей действительно разбирает лишь первый пакет кадра. Подробности и цифры — в следующем разделе.

---

## Правило первого кадра

> [!tip] Два правила, и оба нарушаются молча
> **Первое:** первый бинарный WebSocket-кадр после апгрейда обязан содержать не менее 64 байт — весь obfuscated2-заголовок.
> **Второе:** в одном кадре должно ехать **не больше одного MTProto-пакета**. Релей разбирает только первый пакет кадра и выбрасывает всё, что идёт следом, — без ошибки и без закрытия соединения.
>
> Разрезать пакет на несколько кадров при этом можно как угодно, хоть по байту. Ограничение только на склейку.

Порог измерен с точностью до байта на `kws2.web.telegram.org` (три независимых серии, суммарно около 350 попыток, 8 августа 2026; сетевые сбои отделены от протокольного вердикта повторами, иначе результат превращается в шум):

| Как нарезан исходящий поток | Доля успеха |
|---|---|
| `[init 64][пакет]` — «канонический» способ | 100% |
| `[init 64 + пакет]` одним кадром | 100% |
| `[init 64][полпакета][полпакета]` | 100% |
| `[init 64][дальше по 1 байту в кадре]` | 100% |
| `[init 65 = 64 + первый байт пакета][остаток]` | 100% |
| `[init 63][недостающий байт][пакет]` | **0%** |
| `[init 30]…`, `[init 40]…`, весь поток кусками по 7–8 байт | **0%** |

Точка перелома — ровно между 63 и 64 байтами: 63 → 0 из 10 успешных, 64 → 8 из 8.

Второе правило измерено отдельно, полным рукопожатием (9 августа 2026). Клиент перед вторым шагом отправляет подтверждение предыдущего сообщения, то есть подряд идут два пакета — `msgs_ack` и `req_DH_params`:

| Как отправлены два пакета | Ответ сервера |
|---|---|
| двумя кадрами | `server_DH_params_ok`, 652 байта |
| склеены в один кадр | **тишина** |

Воспроизведено на `kws2-1` и `kws4-1` одинаково. Первый пакет кадра (`msgs_ack`) сервер принимает, второй просто не существует для него.

Три следствия, важных для того, кто пишет свою реализацию:

**Правило действует на уровне отдельного кадра, а не сообщения и не TCP-записи.** Отправить все кадры одной записью в сокет не помогает. Настоящая WebSocket-фрагментация (`FIN=0` плюс continuation-кадры, то есть одно логическое сообщение) — тоже не помогает. Релей достаёт init из полезной нагрузки **первого кадра** и больше к этому вопросу не возвращается: досылка недостающего байта следующим кадром соединение не спасает.

**Отказ молчаливый — и это худшая часть.** Если первый кадр имеет длину 48–63 байта, сервер не отвечает ничего: соединение просто висит до таймаута. Со стороны клиента это неотличимо от блокировки провайдером или потери пакетов, поэтому баг легко списать на сеть и искать не там. Только при первом кадре в 32 байта и меньше приходит явный `Close` с текстом `404` — релей не смог выбрать датацентр.

**Практический вывод: «отдавать поток как получится» не работает.** Кадр — это единица разбора на стороне релея, а не просто способ нарезки. Транспорт волен резать пакет на сколько угодно кадров, но обязан выпускать кадр ровно на границе пакета и никогда не класть в один кадр два пакета. Плюс отдельная гарантия для самой первой записи: не меньше 64 байт.

Обе ошибки не диагностируются по поведению сети: соединение живо, TLS в порядке, кадры уходят — просто ответа нет. Ровно поэтому их легко принять за блокировку у провайдера.

---

## Рукопожатие: место, где всё ломается тихо

Отдельно стоит разобрать создание ключа авторизации, потому что именно здесь наблюдается отказ, который по логам легко принять за проблему сети.

Ключ создаётся обычным MTProto-рукопожатием: клиент шлёт `req_pq_multi` (около 100 байт вместе с init), получает `resPQ`, затем отправляет `req_DH_params` — а это уже около 340 байт, потому что внутри лежит 256-байтовый блок, зашифрованный RSA. Дальше сервер отвечает `server_DH_params_ok`, и стороны договариваются о ключе.

**Наблюдение (Android-форк, версия 1.1.15, 9 августа 2026):** для датацентра, ключа к которому у клиента ещё нет, рукопожатие по WSS не проходит дальше первого шага. В сетевом логе это выглядит так:

```
upgrade_ok domain=kws4-1.web.telegram.org
frame_queued                        <- ушёл req_pq
read                                <- пришёл resPQ, 100 байт
frame_queued                        <- ушёл req_DH_params
wss_disconnect reason=2 phase=post_handshake_no_appdata
```

Шесть попыток подряд с нарастающей паузой — и ни одного ответа на второй шаг. Ключ не создаётся, а все запросы к этому датацентру остаются в очереди навсегда.

Что при этом **проверено и отброшено**: размер второго кадра ни при чём. Независимый клиент, доведённый до второго шага, получил от релея ответ на кадр в 341 байт — и от `kws4-1`, и от `kws2-1`. То есть релей такие кадры передаёт, и сервер на них отвечает.

**Причина установлена:** перед вторым шагом клиент отправляет `msgs_ack`, и транспорт, склеивающий очередь в один кадр, отправлял подтверждение и `req_DH_params` вместе. Релей обработал только первое, второе выбросил — отсюда тишина. Достаточно выпускать кадр на границе пакета, и рукопожатие проходит. Медийный релей `kwsN-1` здесь ни при чём: он обслуживает создание ключей наравне с обычным.

> [!warning] Как распознать это в логах
> Три признака, которые встречаются вместе:
> - `wss_disconnect ... phase=post_handshake_no_appdata` — соединение поднялось, апгрейд прошёл, приложение молчит;
> - `handshake: begin` для одного и того же датацентра, повторяющийся с нарастающей паузой;
> - шквал строк «нет ключа для датацентра» — в разобранном случае 98 926 записей за 61 секунду.
>
> Последний признак опаснее, чем кажется: такой поток вытесняет из файла всю остальную диагностику, и причину становится физически не по чему искать. Прежде чем разбирать сетевую проблему, стоит убедиться, что лог не забит одной повторяющейся строкой.

Практическое следствие, пока дефект не исправлен: WSS работает для датацентров, ключи к которым уже есть, и не поднимает ключ для нового. Аккаунт продолжает работать, переписка идёт, а медиа из «чужих» датацентров не грузится вовсе — при том что скорость и связь выглядят нормальными. Это и есть типичная жалоба «файлы качаются, а чужие картинки нет».

---

## Релей может быть закрыт: главное практическое ограничение

Все предыдущие разделы описывают, как транспорт устроен и что требует сервер. Но самое частое препятствие лежит ниже протокола: **релей конкретного датацентра может быть просто недоступен из вашей сети**, и тогда никакие правила фрейминга не помогут.

Замеры 9 августа 2026 в одной российской сети (провайдер с DPI), порт 443:

| Куда | Что это | ICMP (`ping`) | TCP 443 |
|---|---|---|---|
| `149.154.167.220` | релей DC2 и DC4 | отвечает | **открыт** |
| `149.154.174.100` | релей DC1 и DC3 | отвечает | закрыт |
| `2001:b28:f23d:8005:7:0:109:338` | тот же релей по IPv6 | отвечает | закрыт |
| `149.154.175.50` | прямой адрес DC1, мимо релея | отвечает (178 мс) | закрыт |

Открыт ровно один путь из четырёх. Практический результат для пользователя: **фотографии грузятся, а эмодзи и реакции — нет**, потому что они лежат на другом датацентре. Переписка при этом работает, скорость нормальная, никаких ошибок в интерфейсе.

> [!warning] `ping` ничего не доказывает
> Во всех четырёх строках ICMP проходит, а TCP на 443 — только в одной. DPI режет соединения по порту, оставляя пинг живым, поэтому «сервер пингуется, значит доступен» — неверный вывод, на котором легко потерять часы.
>
> Проверять надо именно TCP. В Windows:
> ```powershell
> tnc 149.154.174.100 -Port 443
> ```
> и смотреть строку `TcpTestSucceeded`. Обратите внимание на синтаксис: адрес идёт без порта, порт задаётся отдельным параметром. Написание вида `Test-NetConnection - 1.2.3.4:443` заставляет PowerShell искать хост с таким именем и висеть до таймаута — легко принять это за блокировку.

**Почему запасного пути нет.** У каждого датацентра ровно один ingress-адрес IPv4, и подставить чужой нельзя: адрес DC2 с именем `kws1` отвечает редиректом `302` (см. [[#Мифа про «только DC2 и DC4» не существует]]). Так что если этот единственный адрес закрыт, WSS для датацентра мёртв.

**IPv6 помогает не всем.** AAAA-записи есть у всех релеев, причём у DC1 и DC5 их по два против одного IPv4 — то есть шестой протокол даёт и обход, и резервирование. Но в измеренной сети IPv6 был заблокирован целиком, включая релей DC2, который по IPv4 прекрасно работал. Отсюда важное правило реализации: **нельзя предпочитать IPv6 вслепую** — наличие у устройства глобального IPv6-адреса не означает, что маршрут до релея существует, и слепое предпочтение заменяет рабочий путь заведомо мёртвым. IPv6 должен быть дополнительным кандидатом, а не заменой.

**Что делать в клиенте.** Не оставлять датацентр без содержимого навсегда. Считайте неудачи, не дошедшие даже до установленного TCP, отдельно по датацентру: несколько подряд означают, что релей недоступен, и для этого датацентра нужно временно вернуться к обычному соединению, оставив WSS там, где он работает. Иначе транспорт работает по принципу «всё или ничего», и половина медиа не грузится при внешне исправном клиенте.

**Когда WSS вообще не тот инструмент.** Он придуман против блокировки по IP: адреса датацентров режут, а `web.telegram.org` трогать дороже. Если же в конкретной сети закрыты и релеи, и прямые адреса, задача другая — нужен туннель через доступный сервер (MTProxy или VPN), который проксирует все датацентры одинаково. Проверять это стоит до того, как искать баги в транспорте.

---

## Как написать WSS-транспорт правильно

Сводка того, что следует из измерений, — в виде готового чек-листа. Оба форка ZaStoGram приведены к этому виду 8 августа 2026 (Android — версия 1.1.13, Desktop — коммит `7f3ceffb`), и расхождений по протокольной части между ними больше нет.

**1. Первый кадр — не короче 64 байт.** Это единственное требование к нарезке, и нарушается оно незаметно. Не полагайтесь на то, что «так получается само»: заведите в транспорте буфер, который придерживает начальные байты, пока их меньше заголовка. В Android-форке это `WssSocket::write`, в Desktop — склейка `prefix` с первым пакетом в `write(prefix, buffer)`.

**2. Один кадр — не больше одного MTProto-пакета.** Держать байтовый поток внутри можно и удобно, но выпускать кадр обязательно на границе пакета: всё, что попадёт в кадр вторым, сервер не увидит. Резать один пакет на несколько кадров при этом разрешено.

**3. Адрес выбирайте по датацентру, а не один на всех.** Ingress-адреса не взаимозаменяемы. Минимальная верная таблица: DC1 и DC3 → `149.154.174.100`, DC2 и DC4 → `149.154.167.220`, DC5 → `149.154.170.100`. Ещё надёжнее — резолвить `kwsN.web.telegram.org` и не хардкодить ничего; хардкод имеет смысл только как обход подмены DNS, и тогда обязательно нужен откат на имя.

**4. `Host` и TLS SNI — всегда имя релея**, даже когда подключаетесь по IP. Имя определяет и датацентр, и класс трафика; медиа — это суффикс `-1` в имени, а не отдельный адрес.

**5. Не пишите номер датацентра в init-пакет.** В байтах 60–61 он нужен MTProxy-секретам; при WSS имя хоста уже всё сказало, а маркер приводит к отказу. Обе реализации на этом обожглись.

**6. Держите откат на имя хоста и запоминайте, что сработало.** Заблокированный или устаревший адрес не должен убивать транспорт: попробуйте имя, а результат запомните на десятки минут, иначе каждое новое соединение будет заново упираться в мёртвый адрес.

**7. Различайте отказ сети и отказ протокола.** Одиночный таймаут TCP к диапазонам Telegram ничего не доказывает — нужны повторы. А молчание релея после успешного апгрейда почти наверняка означает нарушение пункта 1, а не проблемы со связью.

**8. Проверяйте создание ключа отдельно от передачи данных.** Транспорт, прекрасно работающий на датацентре с готовым ключом, может не проходить рукопожатие на новом — см. [[#Рукопожатие: место, где всё ломается тихо]]. Проверка «переписка работает» этот случай не ловит, потому что переписка идёт через датацентр аккаунта, а медиа живёт в других.

**9. Не оставляйте датацентр без содержимого, если его релей закрыт.** Считайте отдельно по датацентру неудачи, не дошедшие даже до установленного TCP: несколько подряд означают недоступный релей, а не сломанный протокол. Для такого датацентра временно возвращайтесь к обычному соединению, оставив WSS там, где он работает. Без этого транспорт получается «всё или ничего», и половина медиа не грузится при внешне исправном клиенте.

**10. Не предпочитайте IPv6 вслепую.** AAAA-записи у релеев есть, и адресов там больше, чем у IPv4, — но глобальный IPv6-адрес у устройства не гарантирует маршрут до релея. В измеренной сети IPv6 был закрыт целиком, и предпочтение шестого протокола заменило единственный рабочий путь мёртвым. IPv6 — дополнительный кандидат, а не замена.

**11. Не давайте одному состоянию заливать лог.** Очередь запросов перебирается на каждом проходе цикла событий, и любая строка внутри этого перебора превращается в тысячи записей в секунду. Пишите такие сообщения не чаще раза в секунду и указывайте число свёрнутых повторов — иначе диагностика утонет ровно в тот момент, когда она нужнее всего.

По состоянию на 9 августа 2026 оба форка ZaStoGram приведены к этому списку полностью, и расхождений по протоколу между ними нет:

| Пункт | Android 1.1.19 | Desktop `f6bd0740` |
|---|---|---|
| 64 байта в первом кадре | придерживает байты, пока не наберётся заголовок | склейка заголовка с первым пакетом |
| Один пакет на кадр | кадр режется по записанной границе пакета | один вызов записи на пакет |
| Таблица ingress DC1–DC5 | есть | есть |
| Откат при закрытом релее | есть | есть |
| Порядок IPv4/IPv6 | по возможностям устройства | наследуется от Qt |

Что остаётся разным по объективным причинам: Android живёт на собственном коде поверх OpenSSL и epoll и потому сам считает буферы и строго валидирует кадры; Desktop опирается на Qt, получая отлаженную работу с сокетом, но теряя контроль над очередью отправки. Это разница инструментов, а не разночтение протокола.

---

## Откуда берётся номер датацентра

Датацентр не передаётся в URL и не задаётся параметром — он читается из уже упомянутых байт 60–61 расшифрованного init-пакета. Дальше номер превращается в имя релея по простому правилу:

| Что нужно | Домен | Пример |
|---|---|---|
| Обычное соединение с DC N | `kwsN.web.telegram.org` | `kws2.web.telegram.org` |
| Медиа-соединение (загрузка файлов) | `kwsN-1.web.telegram.org` | `kws4-1.web.telegram.org` |
| Тестовый бэкенд | путь `/apiws_test` | поддержан только в `tg-ws-proxy` |

Медийные подключения Telegram помечает отрицательным номером датацентра — именно этот знак реализации превращают в суффикс `-1`. Ошибка здесь стоит дорого: в обоих клиентских форках был баг, когда в WSS-режиме в init-пакет всё ещё писался маркер датацентра, как для MTProxy-секрета, и релей отвечал отказом `-444`, потому что имя хоста уже само определяет и датацентр, и класс трафика.

---

## Мифа про «только DC2 и DC4» не существует

Почти во всех источниках — включая раннюю версию этой заметки, каталог маршрутов ZapretGUI и комментарий прямо в коде Desktop-форка — сказано, что веб-релеи есть только у DC2 и DC4, а `kws1`, `kws3` и `kws5` отвечают редиректом `302` либо молчат. **Проверка живыми соединениями 8 августа 2026 этого не подтверждает.**

Каждый из трёх адресов принимает апгрейд и реально проксирует MTProto — в ответ приходит настоящий `res_pq` (конструктор `0x05162463`, `auth_key_id = 0`, nonce совпадает с отправленным):

| Адрес ingress | Обслуживает | Результат проверки |
|---|---|---|
| `149.154.174.100` | `kws1`, `kws3` и их медийные `-1` | апгрейд и MTProto проходят |
| `149.154.167.220` | `kws2`, `kws4` и их медийные `-1` | апгрейд и MTProto проходят |
| `149.154.170.100` | `kws5` и `kws5-1` | апгрейд и MTProto проходят |

Откуда же взялся редирект `302`? Из ошибки метода. Ingress-адреса **не универсальны**: каждый обслуживает только свои датацентры. Если постучаться на `149.154.167.220` (это DC2/DC4) с заголовком `Host: kws5.web.telegram.org`, придёт `302 Found` с `Location: https://core.telegram.org` — и заодно с заголовком `X-Redirect-Host: kws5.web.telegram.org`, которым сервер прямым текстом сообщает, куда следовало обратиться. Обратный случай ещё коварнее: `149.154.174.100` с именем `kws2` не редиректит, а принимает соединение и молчит до таймаута.

Именно так и рождается ложный вывод: инструмент, который держит один зашитый адрес на все датацентры (а так устроены и мосты, и Desktop-форк), при попытке достучаться до DC1/DC3/DC5 получает либо `302`, либо тишину — и делает вывод, что релеев для них не существует.

> [!warning] Как проверять правильно
> Релей проверяется парой «адрес + имя», а не адресом отдельно. Для датацентра N берите ingress, обслуживающий именно этот датацентр, и ставьте `Host` и TLS SNI равными `kwsN.web.telegram.org`. Проще всего не хардкодить адреса вовсе, а резолвить имя: DNS отдаёт правильный ingress сам.
>
> Второй источник ложных выводов — сеть. Одиночный таймаут TCP на 443 к диапазонам Telegram — обычное дело там, где стоит DPI; без 4–5 повторов легко объявить живой релей мёртвым. В ходе этой проверки такое случалось дважды и оба раза сначала приводило к неверному диагнозу.

Отдельно про DNS: имена `kwsN` и медийные `kwsN-1` резолвятся в **один и тот же** адрес — суффикс `-1` различает класс трафика на стороне релея, а не машину. У всех имён есть и AAAA-записи, то есть IPv6-путь существует, хотя проверить его не удалось.

Практический смысл поправки: покрытие WSS не ограничено двумя датацентрами. Ограничение реализаций, которые держат один адрес на всё, — их собственное, а не свойство инфраструктуры Telegram.

---

## Две архитектуры: мост снаружи или транспорт внутри

Все известные реализации делятся на два лагеря, и разница между ними определяет почти всё остальное — от установки до того, что видно в логах.

| | **Мост рядом с клиентом** | **Нативный транспорт в клиенте** |
|---|---|---|
| Примеры | `tg-ws-proxy`, модуль `telegram_proxy` в ZapretGUI | ZaStoGram (Android), ZaStoGram Desktop |
| Как клиент его видит | обычный SOCKS5 или MTProxy на `127.0.0.1` | никак — это внутренний способ подключения |
| Что нужно от пользователя | поставить программу, добавить прокси в Telegram | поставить сам форк, включить тумблер |
| Клиент можно любой | да, включая официальный | нет, только этот форк |
| Криптография | поток расшифровывается мостом и шифруется заново | сквозная, без промежуточных ключей |
| Гибкость маршрутов | высокая: запасные пути, внешние SOCKS5, Cloudflare | низкая: только зашитые релеи |

Ключевое отличие — в третьей строке снизу. Мост не может просто переслать байты: клиент зашифровал их своим ключом, выведенным из секрета прокси, а Telegram такого секрета не знает. Поэтому мост расшифровывает транспортную обёртку и зашифровывает поток заново — уже как обычный клиент без прокси. Ещё раз: **речь только о транспортном слое**; MTProto-шифрование переписки на `auth_key` мост не трогает и расшифровать не может.

Нативный транспорт этой ступени не имеет вовсе: клиент сам открывает WebSocket и сам кладёт туда свой обфусцированный поток.

### Мост: tg-ws-proxy (Flowseal)

[Flowseal/tg-ws-proxy](https://github.com/Flowseal/tg-ws-proxy) — Python на asyncio, с собственной минимальной реализацией WebSocket-клиента (без сторонних библиотек) и приложением в системном трее для Windows, macOS и Linux. По умолчанию слушает `127.0.0.1:1443` как MTProto-прокси, то есть в Telegram его добавляют обычной ссылкой `tg://proxy?server=127.0.0.1&port=1443&secret=dd…`. В Docker-образе хост по умолчанию `0.0.0.0`, а `.deb`-пакет ставит systemd-юнит — тот же код разворачивают и на VPS как сетевой сервис.

Отличает проект развитая система запасных путей. Если прямой WebSocket не открылся, соединение уходит по цепочке: **Cloudflare Worker → домен за Cloudflare → прямой TCP на 443**. Первый вариант — бесплатный JS-воркер, который через `cloudflare:sockets` открывает TCP к нужному IP и мостит его в WebSocket; второй — собственный домен пользователя с A-записями `kws1`…`kws203`, направленными на адреса датацентров, в режиме Cloudflare `Flexible`. Список доменов по умолчанию хранится в исходниках в закодированном виде и обновляется раз в час; адрес `raw.githubusercontent.com` при этом закреплён за конкретным IP, чтобы обновление проходило и при испорченном DNS.

Ещё две особенности: пул из четырёх заранее открытых WebSocket-соединений на каждый датацентр (Telegram открывает подключения пачками, и прогретый пул экономит время на рукопожатиях) и запасной вариант с подменой SNI на `sprinthost.ru` — если прямое соединение с настоящим именем не проходит, попытка повторяется с чужим именем в TLS, тогда как заголовок `Host` остаётся честным.

Поддержан и вход по FakeTLS (`ee`-секрет) — с проверкой HMAC и допуском по времени ±120 секунд, а при провале проверки соединение прозрачно перебрасывается на настоящий сайт-прикрытие. Это защита от активного зондирования, и работает она только в консольном режиме: в трей-версии FakeTLS не выведен.

### Мост, встроенный в GUI: ZapretGUI

В [ZapretGUI](https://git.zapret.moe/zapretdiscordyoutube/zapret) есть отдельный раздел «Telegram Proxy» — модуль `src/telegram_proxy/` (по состоянию на коммит `a3056476`, ветка релизов 21.1.5.x). Подход к WSS взят у `tg-ws-proxy` — на это прямо указано в комментарии к коду, — но реализация своя: Python-модуль, работающий в том же процессе, что и GUI, без отдельного бинарника. Слушает `127.0.0.1:1353`, умеет два режима входа — SOCKS5 и MTProxy — и подставляет пользователю готовую ссылку `tg://socks?…` или `tg://proxy?…`, которую Telegram открывает по нажатию кнопки.

Ключевое архитектурное отличие от предыдущего проекта — **внешний SOCKS5 как штатный запасной маршрут**. Когда у датацентра нет своего рабочего релея (а это все, кроме DC2 и DC4), трафик уходит на внешние SOCKS5-серверы проекта, выбираемые пресетом по стране. Отсюда же взялась основная работа последних месяцев: логика переключения между серверами несколько раз переписывалась, потому что Telegram открывает соединения залпом, и короткая серия отказов ошибочно читалась как «сервер умер».

Модуль соседствует с основной функцией программы, но решает другую задачу: движок `winws2` работает с DPI на уровне пакетов, а `telegram_proxy` — с блокировкой по IP. Встроенная диагностика это прямо учитывает: она проверяет доступность релея, TCP+TLS до адресов всех датацентров, апгрейд до WebSocket для `kws1`…`kws5`, блокировку по SNI против блокировки по IP, живость локального порта — и заодно смотрит, запущен ли параллельно сам `winws2`.

### Нативный транспорт: ZaStoGram для Android

Форк официального Android-клиента ([zastogram/ZaStoGram](https://git.zapret.moe/zastogram/ZaStoGram), база — Telegram 12.9.2, версия приложения 1.1.2, HEAD `ab53c8d0` от 6 августа 2026) несёт WSS прямо в нативном сетевом слое `tgnet`. Появились файлы `jni/tgnet/wss/WssSocket.cpp` (779 строк) и общий интерфейс транспорта `jni/tgnet/transport/TransportSocket.h`, которых в апстриме DrKLO нет вовсе. Реализация самодостаточная: собственная машина состояний `TcpConnecting → TlsHandshake → HttpWrite → HttpRead → Ready` поверх OpenSSL и неблокирующих сокетов, без Qt и без сторонних WebSocket-библиотек.

**Покрытие датацентров шире, чем у всех остальных реализаций, и оно подтверждено.** Функция `OfficialRoute` строит маршрут для DC1–DC5, держа три зашитых адреса релеев — и проверка 8 августа 2026 показала, что все три действительно проксируют MTProto, включая медийные `kwsN-1`:

| Датацентр | Зашитый адрес релея | Домен (обычный / медиа) |
|---|---|---|
| DC1, DC3 | `149.154.174.100` | `kws1` / `kws1-1`, `kws3` / `kws3-1` |
| DC2, DC4 | `149.154.167.220` | `kws2` / `kws2-1`, `kws4` / `kws4-1` |
| DC5 | `149.154.170.100` | `kws5` / `kws5-1` |

Важная деталь истории: первая версия этого кода поддерживала только DC2 и DC4 — ровно как Desktop-форк сегодня. Расширение до DC1–DC5 внесли в тот же день, 5 августа 2026, коммитом «Fix media downloads over WSS», причём по аналогии, без проверки живым соединением: тест `Tools/check_wss_official_default.py` статически сверяет текст кода, а не факт успешного апгрейда. Догадка оказалась верной — измерения 8 августа подтвердили работоспособность всех трёх адресов (см. [[#Мифа про «только DC2 и DC4» не существует]]).

Ещё одна поправка к раннему описанию: с версии **1.1.12 (8 августа 2026)** исходящий трафик по WSS перестал быть привязан к границам MTProto-пакетов. Отдельная очередь сообщений `outgoingWssMessages` удалена, байты идут через общий `outgoingByteStream` — так же, как у любого TCP-транспорта, — и режутся на кадры по 64 КБ. Гарантию «первый кадр не короче 64 байт» держит сам транспорт: `WssSocket::write` копит начальные байты и не выпускает кадр, пока их меньше заголовка. Обратная сторона: описанный ниже второй бюджет в 4 МБ на MTProto-пакеты вместе с очередью тоже исчез, вместо него — порог 256 КБ на уже сериализованный вывод сокета.

Тестовый бэкенд и CDN-датацентры отсекаются явной проверкой (`dcId < 1 || dcId > 5 || testBackend`). Тумблер «Use WSS transport» (в интерфейсе — Data and Storage → Proxy Settings, раздел «Telegram WSS транспорт») **включён по умолчанию с версии 1.1.20**; до этого требовалось включать вручную. Осознанный выбор пользователя сохраняется: если тумблер уже трогали, берётся его значение.

Совмещение с прокси запрещено полностью и в обе стороны: включение WSS гасит и обычный прокси, и «прокси для звонков», а выбор любой строки прокси гасит WSS. Проверка продублирована в семи местах кода, включая принудительное восстановление инварианта при каждой перерисовке списка прокси, — то есть это сознательная защита, а не единственная ветка условия.

Разбор входящих кадров у этой реализации строгий: отклоняются ненулевые RSV-биты, маскированные кадры от сервера (RFC 6455 запрещает серверу маскировать), контрольные кадры длиннее 125 байт и фрагментированные, continuation-кадр без начатого сообщения, любой неизвестный опкод — вплоть до текстовых кадров, потому что подпротокол объявлен как `binary`. Каждый отказ получает собственный строковый код: их 29 штук, от `wss_tls_verify_failed` до `wss_accept_mismatch`, и они попадают в отладочный лог.

Есть и то, чего нет у Desktop-версии, — **защита от переполнения очереди на отправку**. Лимитов два: 4 МБ на готовые WebSocket-кадры внутри сокета и ещё 4 МБ на MTProto-пакеты, ждущие своей очереди уровнем выше. При превышении соединение обрывается с кодом `ENOBUFS` — управляемый отказ вместо неограниченного роста памяти при аплоаде в медленную сеть.

Обратная сторона ручной реализации — цена отладки. Вечером 5 августа и в ночь на 6-е потребовалось шесть последовательных исправлений: границы WebSocket-сообщений (заголовок, тело и паддинг писались в общий поток, и нарезка зависела от того, когда сработает системный вызов), зависание TLS-рукопожатия из-за edge-triggered epoll (пришлось перевести именно WSS-сокет на level-triggered), маркер датацентра в init-пакете, маршрут аплоада на медийный релей вместо обычного (сервер отвечал `-404`), возврат таблицы адресов релеев и, наконец, нарушение контракта OpenSSL — буфер для повторного `SSL_write` дописывался новыми кадрами и переезжал в памяти, что ломало отправку под нагрузкой.

> [!warning] README форка расходится с кодом
> В README ZaStoGram сказано, что «route, DNS, TLS SNI и hostname verification используют один официальный hostname; ручной таблицы relay IP нет». На момент разбора (6 августа 2026) это неверно: таблицу зашитых адресов убрали одним коммитом и вернули следующим тем же вечером, а документацию не обновили. Проверять такие утверждения стоит по коду `WssSocket.cpp`, а не по описанию.

История фичи поучительна сама по себе. Сначала в форк добавили локальный WSS-шлюз с произвольными host/port/path, режимом «SOCKS внутри WebSocket» и мостом на `127.0.0.1` для мини-приложений. Через неделю вырезали SOCKS-внутри-WSS, а ещё через пять недель заменили всю конструкцию тонким транспортом к официальным релеям. В README это зафиксировано как намеренное решение: ни собственного шлюза, ни SOCKS-апстрима, ни локального релея в новой версии нет. Пользователи, у которых была настроена кастомная конфигурация, теряют её при обновлении — миграция сохраняет только сам факт «WSS был включён».

### Нативный транспорт: ZaStoGram Desktop

Форк Telegram Desktop ([zastogram/ZaStoGram_desktop](https://git.zapret.moe/zastogram/ZaStoGram_desktop), версия 7.0.8 от 3 августа 2026, HEAD `729dfd39`) решает ту же задачу, но опирается на Qt. WSS живёт в `mtproto/proxy/wss/socket.cpp` поверх `QSslSocket`, а выбор сокета вынесен в фабрику: в зависимости от настроек и секрета создаётся `WssSocket`, `TlsSocket` (FakeTLS для MTProxy) или обычный `TcpSocket`. Протокольный слой при этом не знает, какой транспорт под ним, — поэтому обфускация для всех трёх одинаковая.

Первое отличие от Android-версии — **транспорт по умолчанию Wss**: без единой настройки соединения идут через веб-релеи, а к непокрытым датацентрам — обычным TCP.

Долгое время покрытие было ограничено DC2/DC4, с комментарием в коде «web sockets exist only for DC2 / DC4». Посылка оказалась неверной, а причина — технической: форк держал **один** зашитый адрес `149.154.167.220`, обслуживающий только эти два датацентра, и попытка достучаться через него до `kws1`/`kws5` давала редирект `302` (см. [[#Мифа про «только DC2 и DC4» не существует]]). Коммитом `7f3ceffb` от 8 августа 2026 адрес заменён таблицей «датацентр → ingress», как в Android-форке, и покрытие расширено до DC1–DC5. Подсказка «WSS unavailable for this DC» опирается на ту же функцию маршрута, поэтому для новых датацентров она пропала автоматически.

Фрейминг у Desktop-версии, наоборот, случайно оказался правильным: `write(prefix, buffer)` склеивает 64-байтный init с первым пакетом в **один** кадр, поэтому требование «первый кадр не короче 64 байт» выполняется само собой.

Второе — **экспертный режим с произвольным релеем**: можно указать свои host, port, path и SNI-домен, и такой маршрут работает для любого датацентра, обходя ограничение DC2/DC4. Это единственный способ во всей четвёрке реализаций подставить собственный сервер вместо инфраструктуры Telegram. Рядом с полями висит предупреждение «No certificate check is done — use only a relay you trust», и оно устарело: проверку сертификата вернули 4 июля 2026, текст в интерфейсе поправить забыли. Ошибка безобидная — пугает сильнее, чем есть риска, — но полагаться на неё как на описание поведения нельзя.

Третье — **живая индикация**. В настройках строка «Connection type» показывает реальный транспорт и задержку: `Default (WSS used, ping: 87 ms)`. Android такого не умеет вовсе: там подпись «Telegram WSS transport: Official» отражает лишь состояние тумблера, а фактическое состояние сокета живёт в C++ и наружу не отдаётся. Кроме того, Desktop показывает осмысленное предупреждение, когда датацентр вне покрытия: «WSS unavailable for this DC. Add a proxy.»

Совмещение с прокси разрешено выборочно: поверх удалённого SOCKS5 — можно, поверх MTProxy — нет (транспорт откатывается на TCP, потому что MTProxy сам является TCP-транспортом), поверх локального SOCKS5 на `127.0.0.1` или на порту `1353` — тоже нет. Последнее исключение сделано ровно под мосты из предыдущих разделов: заворачивать WebSocket в локальный инструмент, который сам уже строит WebSocket, бессмысленно.

Цена опоры на Qt видна в двух местах. Хорошая сторона: целый класс низкоуровневых ошибок исключён по построению — буферами записи и ожиданием готовности сокета занимается давно отлаженный код Qt, поэтому ни бага с переездом буфера, ни потери события записи здесь возникнуть не могло. Плохая: приложение не управляет очередью на отправку и потому не может её ограничить — предохранителя на переполнение в WSS-транспорте Desktop нет вообще. Разбор входящих кадров тоже мягче: RSV-биты, маска от сервера, состояние фрагментации и длина контрольных кадров не проверяются, а пять текстовых сообщений об ошибках сводятся к одному машинному коду.

---

## Android или Desktop: чем отличаются

По протоколу форки больше не расходятся: правило первого кадра, один пакет на кадр, таблица ingress DC1–DC5 и откат при закрытом релее выполняются в обоих. Различия остались в том, что построено вокруг транспорта.

| Ось сравнения | Android (ZaStoGram) | Desktop (ZaStoGram Desktop) |
|---|---|---|
| Соблюдение правил фрейминга | да | да |
| Покрытие датацентров | DC1–DC5 | DC1–DC5, плюс любой через свой релей |
| Откат при закрытом релее | по датацентру, с 1.1.18 | по датацентру, с `f6bd0740` |
| Дефолт | включён, с 1.1.20 | включён |
| Свой релей | нет — только инфраструктура Telegram | есть: host, port, path, SNI |
| Ограничение очереди на отправку | 4 МБ на кадры плюс 256 КБ на сериализованный вывод | отсутствует |
| Строгость разбора кадров | RSV, маска сервера, фрагментация, контрольные кадры | максимальный размер и close |
| Диагностика отказов | 29 отдельных кодов | 5 сообщений в одном коде |
| Минимальная версия TLS | задана явно: 1.2 | наследуется от Qt |
| Индикация для пользователя | статичная метка | транспорт и пинг в реальном времени |
| CDN-загрузки | отключены флагом `cdn_supported=false` | разрешены, идут мимо туннеля прямым TCP |
| Сочетание с прокси | запрещено полностью | разрешено поверх удалённого SOCKS5 |

**Транспортный слой аккуратнее у Android.** Он считает свои буферы и отказывает управляемо, строго валидирует RFC 6455 и даёт содержательную диагностику — 29 отдельных кодов вместо пяти сообщений в одном. Плата за это — ручная работа с epoll и OpenSSL: почти все найденные за август баги транспорта жили именно здесь, и каждый чинил реальный воспроизводимый отказ.

**Предсказуемость медиа тоже выше у Android.** Флаг `cdn_supported=false` убирает CDN из уравнения: файлы, стикеры и реакции идут через датацентры, а не через CDN-адреса. В Desktop CDN-редиректы разрешены, и такое соединение выходит из-под туннеля обычным TCP — ровно в той сети, где прямые адреса Telegram и режут.

**Гибкость и обратная связь — за Desktop.** Свой релей остаётся выходом, когда все зашитые адреса заблокированы: у Android в такой ситуации выбор беднее. Живой индикатор транспорта с пингом отвечает на главный вопрос пользователя — работает ли обход прямо сейчас, — на который Android не отвечает никак.

**Что одинаково слабо у обоих.** Ни один не мимикрирует под TLS-почерк браузера: рукопожатие уходит с отпечатком системной криптобиблиотеки, а не Chrome, хотя инфраструктура для подмены в обоих проектах есть — она подключена только к их же ветке MTProxy FakeTLS (см. [[mtproxy/ja4-sni-client-side|разбор JA4 и SNI]]). Заголовок `User-Agent` в обоих зашит с Chrome 131 — версией конца 2024 года. И ни один не показывает пользователю, что часть датацентров ушла на прямое соединение: обход работает молча.

> [!tip] Практический вывод
> Транспорт включён по умолчанию в обоих клиентах, и специально его настраивать не нужно. Если медиа частично не грузится — проверьте по TCP доступность релеев своей сети ([[#Релей может быть закрыт: главное практическое ограничение]]). Открыт не весь список — значит для этой сети правильнее туннель: MTProxy или VPN проводят все датацентры через один сервер, тогда как WSS работает только там, где открыт релей.

---

## Чем это отличается от MTProxy и VPN

Легко решить, что WSS — это «MTProxy, но лучше», и ошибиться. Разница в том, где стоит точка выхода.

Классический [[Zapret/mtproto/00-overview|MTProxy]] — это **ваш сервер** (или чужой публичный), к которому клиент подключается по произвольному порту и который дальше сам идёт к Telegram. Он позволяет ротировать адреса, ставить FakeTLS, делить порт 443 с настоящим сайтом — но требует VPS, а его адрес рано или поздно попадает в списки блокировок.

WSS-транспорт не требует ничего своего: клиент идёт напрямую на инфраструктуру Telegram, просто через её веб-подъезд. Отсюда и плюс, и минус. Плюс — не надо ничего поднимать и платить, а домен назначения принадлежит Telegram. Минус — вы не управляете точкой выхода: если конкретный релей начнёт отвечать редиректом или замолчит, сделать с этим нечего, кроме как переключиться на запасной маршрут.

От VPN обе схемы отличаются одинаково: они уводят только трафик Telegram и не трогают остальную систему, не держат туннель и не сажают батарею. Звонки не переносит ни одна из них — голос и видео ходят по UDP мимо любого TCP-прокси, — но это вопрос не «работают или нет», а «чем их закрывать»: трафик звонка идёт напрямую, и его задача решается пакетным обходом ([[Zapret2/Zapret2|zapret]] перехватывает в том числе UDP и STUN) или VPN.

| | MTProxy на своём VPS | WSS-транспорт | VPN |
|---|---|---|---|
| Нужен свой сервер | да | нет | обычно да |
| Куда идёт трафик | ваш IP | инфраструктура Telegram | ваш IP |
| Управление точкой выхода | полное | никакого | полное |
| Работает вне Telegram | нет | нет | да |
| Переносит звонки | нет, идут напрямую | нет, идут напрямую | да |
| Что блокируют в первую очередь | IP сервера | сами релеи и подходы к ним | IP сервера, протокол |

Отсюда практическая связка: WSS или MTProxy отвечают за переписку и медиа, пакетный обход — за звонки и за те датацентры, до которых веб-релеи не дотягиваются. Они не конкуренты и работают параллельно.

---

## Что дальше

Схема выглядит изящно, но у неё большой список оговорок: работают не все датацентры, стикеры и реакции ходят через CDN мимо релеев, звонки не проксируются вовсе, а бесплатные запасные пути через Cloudflare упираются в лимиты. Всё это разобрано отдельно — с симптомами, причинами и тем, что показывает диагностика: [[mtproxy/telegram-wss-limits|Ограничения WSS: что не работает и почему]].

---

## 📚 См. также

- [[mtproxy/telegram-wss-limits|Ограничения WSS: что не работает и почему]] — парная заметка: датацентры, медиа, звонки, Cloudflare, типичные диагнозы
- [[Zapret/mtproto/00-overview|MTProto Proxy — полный гайд]] — обзорный раздел про классический MTProxy: протокол, реализации, цензура
- [[Zapret/mtproto/01-protocol|Протокол MTProxy: 3 режима и FakeTLS]] — что такое abridged/intermediate/padded intermediate и откуда берутся теги `0xef`/`0xdd`/`0xee`
- [[mtproxy/ja4-sni-client-side|Кто может менять JA4 и SNI]] — почему обход по «почерку» соединения делается на клиенте, а не на релее
- [[mtproxy/faketls-relay-diagnosis|Релей или клиент: диагностика MTProxy]] — как понять, кто виноват, когда рабочий ключ не подключается
- [[mtproxy/tsrman-tg-android-faketls|tsrman/tg — Telegram для Android со сменой JA4]] — другой подход к тому же: не смена транспорта, а смена TLS-почерка
- 🔗 [Flowseal/tg-ws-proxy](https://github.com/Flowseal/tg-ws-proxy) — исходники моста на Python, документация по Cloudflare Worker и FakeTLS
- [[virus/github-removes-clean-zapret-keeps-malware-august-2026|🎭 GitHub снёс чистые сборки Zapret, а вирусные оставил]] — почему исходники ZaStoGram с 11 августа 2026 доступны только в Forgejo: GitHub-организация `youtubediscord` удалена
- 🔗 [zastogram/ZaStoGram](https://git.zapret.moe/zastogram/ZaStoGram) — исходники Android-форка: WSS-транспорт в `TMessagesProj/jni/tgnet/wss/`, готовые APK — в [релизах](https://git.zapret.moe/zastogram/ZaStoGram/releases)
- 🔗 [zastogram/ZaStoGram_desktop](https://git.zapret.moe/zastogram/ZaStoGram_desktop) — исходники Desktop-форка: WSS-транспорт в `Telegram/SourceFiles/mtproto/proxy/wss/`
- 🔗 [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455) — спецификация WebSocket: рукопожатие, маскирование, формат кадров

---

> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование и скачивание всего репозитория одним zip-архивом доступны в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/mtproxy/telegram-wss-transport.md) · [весь репозиторий](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main).
