От HAProxy до VLESS+Reality: все грабли одного MTProto-прокси
Если вы когда-нибудь поднимали свой MTProto-прокси для Telegram и он у вас «работает, но как-то не очень» — эта статья для вас. Я прошёл путь от «просто добавить relay» до «переписать всю цепочку на VLESS+Reality с нуля», и по дороге собрал приличную коллекцию граблей. Расскажу всё по порядку, чтобы вам не пришлось наступать на те же.
Исходная ситуация
Была задача простая на первый взгляд: поднять MTProto проксю для Telegram на сервере в РФ. Причина стандартная — хочется, чтобы телега работала
Взял mtg (ghcr.io/9seconds/mtg:2 — отличная реализация MTProto-прокси с поддержкой fake-TLS), поднял на сервере — и оно не работает. Точнее, работает, но еле-еле: то подключается, то нет, на мобильном интернете почти всегда фейл.
Первая мысль, которая приходит в голову почти всем в такой ситуации: «провайдер блокирует адреса Telegram». И это отчасти правда — но, как выяснилось, правда не вся.
Первая попытка: спрятать Telegram за релеем
Логика была такая: раз провайдер режет соединения именно к IP Telegram, то уберу эти IP из виду. Арендую второй сервер за границей, туда ставлю настоящий mtg, а на RU-сервере — просто TCP-релей (HAProxy или голый iptables DNAT), который прозрачно перекидывает байты дальше.
Клиент → RU-сервер (HAProxy, чистый TCP passthrough) → Зарубежный сервер (mtg) → Telegram
Казалось бы, логично: сервер в РФ теперь физически ничего не знает про Telegram, он просто гоняет байты во внешний IP. Провайдер не должен видеть ничего подозрительного.
Не сработало. И вот тут первая важная вещь, которую стоит понять сразу, чтобы не терять на неё время, как я: HAProxy в режиме TCP (mode tcp) — это чистый passthrough на уровне байтов. Он не меняет ни один бит в трафике. А значит, если DPI-система провайдера умеет распознавать сигнатуру самого MTProto/fake-TLS протокола (а современные системы это умеют, обфускация MTProto далеко не идеальна против активного анализа), то ей совершенно не важно, куда в итоге идёт этот трафик — хоть в Telegram напрямую, хоть транзитом через десять серверов. Она видит характерный паттерн прямо на границе сети провайдера и давит/тормозит его там же.
Проверил гипотезу с портом — может, дело в нестандартном порту 9443? Переставил на 8080, на 443 — без разницы. Заменил HAProxy на голый DNAT через iptables (чтобы вообще убрать userspace-прокси из цепочки) — тот же результат. Значит, дело не в конкретной реализации relay и не в порту — дело в самом факте, что через сеть этого провайдера идёт трафик, похожий на MTProto.
Вторая попытка: SOCKS5 вместо голого relay
Ладно, чистый TCP passthrough не работает — попробуем завернуть трафик во что-то менее прозрачное. mtg умеет из коробки ходить через upstream SOCKS5-прокси (секция [network] proxies в конфиге) — это открывает дорогу к архитектуре «mtg работает локально на RU-сервере, а наружу к Telegram ходит через SOCKS5-туннель на зарубежный сервер»:

Поднял простенький SOCKS5-сервер на зарубежном хосте, всё настроил — и снова нестабильно, местами даже хуже, чем relay. Проверил разные реализации SOCKS5-сервера (сначала лёгкий Go-шный прокси, потом Xray в режиме socks-inbound) — разницы никакой. Значит, дело не в конкретной реализации SOCKS5.
И тут стало ясно: SOCKS5 — это ТОЖЕ обычный незашифрованный TCP-туннель, без какой-либо маскировки. Если провайдер детектит характерный паттерн MTProto-трафика на границе своей сети, ему всё равно, обёрнут этот трафик в HAProxy-relay или в SOCKS5-CONNECT — сигнатура protocol’а внутри никуда не делась.
Третья попытка: настоящий VLESS+Reality
Вот тут наконец нашёлся правильный инструмент. VLESS+Reality — это протокол, специально спроектированный для того, чтобы противостоять именно активному DPI-анализу. В отличие от fake-TLS в MTProto (который просто ИМИТИРУЕТ структуру TLS-хендшейка), Reality делает настоящий TLS 1.3 хендшейк с настоящим сайтом — сервер буквально ходит на реальный домен (например, www.samsung.com или любой другой популярный сайт с хорошей TLS 1.3 + X25519 поддержкой) и использует часть его сертификата в своём ответе. Для DPI-системы, которая пассивно смотрит на трафик, это неотличимо от обычного похода пользователя на этот сайт.
Новая схема:
Клиент → mtg (RU-сервер, локально)
→ SOCKS5 (localhost)
→ Xray-клиент (VLESS+Reality исходящий)
→ Xray-сервер (зарубежный, VLESS+Reality входящий)
→ freedom outbound → Telegram
Собрал всё на голом Xray (без готовых панелей вроде 3x-ui — если хотите держать инфраструктуру минимальной и понятной, голого xray-core в docker-контейнере более чем достаточно). Подключился — и… соединение мгновенно рвётся. Ноль байт в ответ, обрыв через доли секунды.
Грабля №1: xtls-rprx-vision
Первая попытка конфига VLESS-outbound включала модный flow: "xtls-rprx-vision" — это оптимизация, которая распознаёт внутреннюю структуру TLS-записей проксируемого трафика и переключается на «прямую склейку» сокетов для меньших накладных расходов (оф док). Звучит отлично, если вы проксируете чужой TLS-трафик (например, обычный веб-сёрфинг через VLESS).
Но мы туннелируем не TLS, а сырые байты протокола MTProto — они просто ЗАКОДИРОВАНЫ под fake-TLS на этапе клиент↔наш сервер, а вот на этапе сервер↔сервер это уже произвольный бинарный поток без какой-либо TLS-структуры внутри. Vision пытается распарсить эти байты как TLS-записи, не находит ожидаемой структуры — и, судя по всему, обрывает соединение как «что-то пошло не так».
Решение: не используйте flow: "xtls-rprx-vision", если туннелируете не-TLS payload. Просто уберите поле flow из конфига клиента и сервера — обычный VLESS+Reality без Vision прекрасно передаёт произвольные байты.
Грабля №2: «протухший» IP — а может, и нет
Убрал Vision — соединение стало устанавливаться, но проходило только процентов 40-60 попыток. Естественная мысль: зарубежный ip уже светился и Telegram придерживает соединения именно с этого IP из-за накопленной «плохой репутации»?
Проверил напрямую: взял свежий сервер у другого провайдера, без единого байта истории. Задеплоил тот же Reality-сервер. Результат — те же самые 40-60% успешных подключений. Гипотеза не подтвердилась: дело было не в конкретном IP.
Грабля №3: цена TLS-хендшейка на каждое подключение
А дело оказалось вот в чём. Telegram-клиент открывает много коротких параллельных TCP-соединений — это нормальное поведение MTProto (разные соединения под разные типы запросов: обновления, медиа, служебные пакеты). У каждого такого соединения довольно короткое «терпение» — если оно не получает первый байт ответа за секунду-другую, клиент его обрывает и пробует заново.
А теперь смотрим, что происходит на нашей стороне: каждое новое MTProto-подключение клиента = новое VLESS-соединение = новый полный Reality TLS-хендшейк (это минимум пара round-trip’ов до зарубежного сервера и обратно, плюс сама Reality-механика). Секунда терпения клиента улетает на установку тоннеля, а полезные данные ещё даже не начали идти.
Собрал точную временную линию по логам и tcpdump: клиент стартует стрим, наш Xray-клиент принимает SOCKS5-запрос и начинает VLESS-хендшейк — и где-то через 700-900 мс клиент уже обрывается, не дождавшись ни байта.
Решение — мультиплексирование (mux). Xray умеет прогонять много логических потоков через один уже установленный VLESS-туннель, не переустанавливая TLS-хендшейк каждый раз:

Добавляется в конфиг outbound на клиентской стороне (там, где VLESS исходящий). После этого изменения тоннель наконец начал держать нагрузку: логи чистые, ни одной ошибки установки соединения.
Финальная неожиданность: дело было не только в архитектуре
После всех этих фиксов картина улучшилась, но всё ещё была нестабильной именно на исходном RU-сервере. На этом моменте я взял тестовый сервер у другого облачного провайдера в России и развернул на нём точно ту же связку (mtg + mux + VLESS/Reality) — и она заработала идеально сразу, без единой ошибки в логах.
Вывод оказался неожиданно простым: проблема с самого начала была в сетевой политике конкретного хостинг-провайдера первого RU-сервера, а не в архитектуре как таковой. Он, судя по всему, применяет DPI на границе своей сети и режет/тормозит распознанные MTProto-паттерны — причём делает это независимо от того, во что вы этот трафик заворачиваете, если только это не выглядит как настоящий TLS с реального сайта.
## Итоговая архитектура

Два сервера, три контейнера. RU-сервер держит mtg и Xray-клиент в одном docker-compose стеке (они общаются между собой по внутренней docker-сети, наружу торчит только порт mtg). Зарубежный сервер держит только Xray с VLESS+Reality входом.
Как повторить у себя
Шаг 1. Зарубежный сервер — точка выхода в Telegram
Нужен сервер с адекватной (не заблокированной вашим локальным DPI) связью до Telegram. Подойдёт практически любой недорогой VPS за пределами вашей юрисдикции.
Генерируем ключи Reality и UUID клиента:
docker run --rm teddysun/xray xray x25519
# выведет PrivateKey и Password (это PublicKey)
openssl rand -hex 8 # short ID
cat /proc/sys/kernel/random/uuid # UUID клиента
Конфиг /opt/xray-reality/config.json:
{ "log": {"loglevel": "warning"}, "inbounds": [{ "listen": "0.0.0.0", "port": 8443, "protocol": "vless", "settings": { "clients": [{"id": "ВАШ_UUID"}], "decryption": "none" }, "streamSettings": { "network": "tcp", "security": "reality", "realitySettings": { "show": false, "dest": "ВАШ_ДЕСТ_ДОМЕН:443", "xver": 0, "serverNames": ["ВАШ_ДЕСТ_ДОМЕН"], "privateKey": "ВАШ_PRIVATE_KEY", "shortIds": ["ВАШ_SHORT_ID"] } } }], "outbounds": [{"protocol": "freedom", "tag": "direct"}]}
Про выбор dest-домена: нужен реальный, живой сайт с поддержкой TLS 1.3 и X25519, желательно не самый заезженный пример из туториалов (Xray сам предупредит в логах, если выбор слишком очевидный — тогда лучше взять что-то менее популярное в качестве примера Reality-камуфляжа, но всё ещё крупное и легитимное).
docker-compose.yml:
services: xray-reality: image: teddysun/xray:latest container_name: xray-reality volumes: - "./config.json:/etc/xray/config.json:ro" ports: - "8443:8443" restart: unless-stopped
cd /opt/xray-reality && docker compose up -d
Шаг 2. RU-сервер (или любой другой сервер, который будет точкой входа для клиентов)
Генерируем mtg secret с fake-TLS маскировкой под нужный домен:
docker run --rm ghcr.io/9seconds/mtg:2 generate-secret -x ВАШ_ДОМЕН_МАСКИРОВКИ
Конфиг /opt/mtg-vless/xray-client.json:
{ "log": {"loglevel": "warning"}, "inbounds": [{ "listen": "0.0.0.0", "port": 1080, "protocol": "socks", "settings": {"auth": "noauth", "udp": false} }], "outbounds": [{ "protocol": "vless", "settings": { "vnext": [{ "address": "IP_ЗАРУБЕЖНОГО_СЕРВЕРА", "port": 8443, "users": [{"id": "ВАШ_UUID", "encryption": "none"}] }] }, "streamSettings": { "network": "tcp", "security": "reality", "realitySettings": { "show": false, "fingerprint": "chrome", "serverName": "ВАШ_ДЕСТ_ДОМЕН", "publicKey": "ВАШ_PUBLIC_KEY", "shortId": "ВАШ_SHORT_ID" } }, "mux": { "enabled": true, "concurrency": 32 }, "tag": "vless-out" }]}
Конфиг /opt/mtg-vless/mtg.toml:
secret = "ВАШ_MTG_SECRET"bind-to = "0.0.0.0:443"public-ipv4 = "IP_ЭТОГО_СЕРВЕРА"prefer-ip = "only-ipv4"
[network]proxies = [ "socks5://xray-client:1080"]
docker-compose.yml — обратите внимание, оба сервиса в одном файле, чтобы mtg мог обращаться к xray-client по имени через встроенный docker DNS:
services: xray-client: image: teddysun/xray:latest container_name: xray-client volumes: - "./xray-client.json:/etc/xray/config.json:ro" restart: unless-stopped mtg: image: ghcr.io/9seconds/mtg:2 container_name: mtg-vless command: ["run", "/mtg.toml"] volumes: - "./mtg.toml:/mtg.toml:ro" ports: - "443:443" depends_on: - xray-client restart: unless-stopped
cd /opt/mtg-vless && docker compose up -d
Шаг 3. Проверка перед реальным тестом
mtg умеет сам себя тестировать — прогоняет пробные подключения ко всем дата-центрам Telegram через ваш прокси-конфиг:
docker run --rm --network mtg-vless_default -v /opt/mtg-vless/mtg.toml:/mtg.toml:ro \
ghcr.io/9seconds/mtg:2 doctor /mtg.toml
Смотрите на блок Validate network connectivity with proxy socks5://xray-client:1080 — там должны быть зелёные галочки по всем DC. Блок про «native network connectivity» (прямое соединение мимо тоннеля) закономерно будет красным — это нормально, мы его не используем.
Шаг 4. Ссылка для клиентов
tg://proxy?server=IP_ВАШЕГО_ВХОДНОГО_СЕРВЕРА&port=443&secret=ВАШ_MTG_SECRET
Чек-лист граблей, чтобы не наступать повторно
— Не используйте flow: "xtls-rprx-vision", если туннелируете не настоящий TLS-трафик, а что-то другое (в нашем случае — MTProto). Vision ломает произвольные бинарные потоки.
— Обязательно включайте mux на клиентском VLESS outbound. Без него каждое новое короткое подключение платит полную цену TLS-хендшейка, и быстро ретраящие клиенты (а MTProto именно такой) никогда не дождутся ответа.
— prefer-ip = "only-ipv4" в конфиге mtg — если у вашего сервера IPv6 настроен криво или не тестировался, дефолтное предпочтение IPv6 добавит случайные подвисания.
— Простой TCP-relay (HAProxy, iptables DNAT, голый SOCKS5) не прячет протокол от DPI. Если ваш провайдер умеет распознавать сигнатуру MTProto/fake-TLS, ему всё равно, во что вы это заворачиваете, пока это не выглядит как настоящий TLS до реального сайта.
— Проверяйте гипотезу «провайдер блокирует» на РАЗНЫХ провайдерах, прежде чем чинить архитектуру до бесконечности. Иногда дешевле и быстрее взять тестовый сервер у другого хостера и сравнить, чем неделями оптимизировать код вокруг проблемы, которая находится вне вашего контроля.
Заключение
MTProto-прокси в 2026 году в РФ — это уже не просто «поднял бинарник и работает». Провайдерский DPI умеет распознавать протокол, и обычная маскировка через relay или SOCKS5 больше не спасает. VLESS+Reality на сегодня — один из немногих реально рабочих способов сделать трафик неотличимым от обычного HTTPS для пассивного анализа. Но даже с правильным протоколом дьявол в деталях: flow, mux, prefer-ip — каждая мелочь может превратить «почти работает» в «не работает вообще».
Если у вас похожая ситуация — начните с самого дешёвого теста: проверьте прямую связность с зарубежным сервером до Telegram, прежде чем городить сложную архитектуру. Может оказаться, что дело не в вашем коде, а в конкретном хостере — и тогда вся экономия времени будет в том, чтобы просто сменить сервер.
Удачи!
ссылка на оригинал статьи https://habr.com/ru/articles/1075528/