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

Удачи!