mtproto.zig

September 8, 2026 · View on GitHub

mtproto.zig

Держите близких на связи.

Крошечный Telegram-прокси, который вы запускаете на своём сервере. Он прячется в обычном HTTPS — цензуре его не найти, а вашим близким не потерять. Установка одной командой, одна ссылка, чтобы поделиться.

177 КБ · меньше 1 МБ RAM · 0 зависимостей — да, он настолько лёгкий (подробности ниже ↓)

Технически: крошечный MTProto-прокси на Zig без зависимостей, маскирует трафик Telegram под обычный TLS 1.3 HTTPS.

License: MIT Zig Platform


Почему этот? · Установка · Обновление · Команды · Маршрутизация · Конфиг · Дашборд · Сборка · Docker · Безопасность · Совместимость · FAQ


Для кого это

  • Вы там, где Telegram режут или блокируют, и хотите просто вернуть его.
  • Вы — тот, к кому семья идёт за помощью, и хотите защитить родителей и друзей ссылкой, которую они нажимают один раз и больше о ней не думают.

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

Чем это лучше VPN?

VPN заметен — цензор узнаёт протокол и блокирует его, а VPN на всё устройство медленный и сажает батарею. Этот прокси выглядит как обычный HTTPS-сайт, ведёт только Telegram, и тем, кому вы дали ссылку, не нужно ничего устанавливать: они нажимают одну ссылку, остальное Telegram делает сам. Помещается на самый дешёвый VPS, стартует мгновенно, больше ничего настраивать не нужно.

В сравнении с другими MTProto-прокси

Большинство MTProto-прокси крупные, тянут зависимости и потребляют много памяти. Этот проект устроен иначе:

ProxyЯзыкБинарникBaseline RSSСтартЗависимости
mtproto.zigZig177 КБ0.75 МБ< 10 мс0
Official MTProxyC524 КБ8.0 МБ< 10 мсopenssl, zlib
TelemtRust15 МБ12.1 МБ~ 5-6 с423 crates
mtgGo13 МБ11.6 МБ~ 30 мс78 modules
MTProtoProxyPythonN/A~ 30 МБ~ 300 мсpython3, cryptography
JSMTProxyNode.jsN/A~ 45 МБ~ 400 мсnodejs, openssl

Почему Zig?

Zig даёт производительность и минимальный footprint уровня C, но без привычной боли C-проектов:

  • Без произвольных аллокаций: слоты соединений и буферы заранее выделяются на старте. Нет GC, который может уронить кадры под нагрузкой.
  • Герметичная кросс-компиляция: можно запустить zig build на macOS и получить статически слинкованный Linux-бинарник. Без Docker и несовпадений glibc.
  • Comptime: маппинг протокола, endian-конверсии и двуязычные строки mtbuddy вычисляются при компиляции.

Прокси также включает набор техник обхода DPI:

ТехникаЧто делает
Fake TLS 1.3Соединения выглядят как обычный HTTPS
DRSИмитирует размеры TLS-record у Chrome/Firefox
Маскировка от активных пробЕсли цензор проверяет ваш сервер, он получает настоящий TLS-хендшейк от локального веб-бэкенда (реальный серт, если домен ваш, иначе self-signed), а не молчащий прокси. Опционально: фронтить реальный tls_domain:443 для доменов с одноходовым x25519
TCPMSS=88Дробит ClientHello на маленькие TCP-пакеты
nfqws TCP desyncFake packets + TTL-limited splits против stateful DPI
Split-TLS1-байтовые Application records против пассивных сигнатур
VPN tunnel poolМаршрутизация через WireGuard/AmneziaWG с SO_MARK и failover
IPv6 hoppingАвто-ротация IPv6 из /64 при банах через Cloudflare API
Anti-replayОтбрасывает replay handshakes и активные пробы ТСПУ Revisor
Multi-userОтдельные secret для разных пользователей
MiddleProxyME transport с автообновлением Telegram metadata

MiddleProxy нужен для промо-тега и медиа на аккаунтах без Premium. Без него фото, видео, истории и другое медиа на non-Premium аккаунтах нужно считать недоступными, а не «иногда лагающими». Звонки Telegram не поддерживаются: Telegram ведёт звонки через SOCKS-пути, а такой трафик нельзя нормально замаскировать в mtproto.zig под обычный HTTPS.


Установка

Установка, обновление и управление делаются через mtbuddy — нативный Zig CLI, который поставляется вместе с прокси.

Одна команда

curl -fsSL https://raw.githubusercontent.com/sleep3r/mtproto.zig/main/deploy/bootstrap.sh | sudo bash

# Явно разрешить unsigned bootstrap mode (не рекомендуется)
curl -fsSL https://raw.githubusercontent.com/sleep3r/mtproto.zig/main/deploy/bootstrap.sh | sudo bash -s -- --insecure
# или: MTPROTO_INSECURE=1

Скрипт скачивает последний mtbuddy, проверяет minisign-подпись и SHA-256 checksum из GitHub Release, затем запускает mtbuddy --help. После этого установите прокси:

# Минимально: secret генерируется автоматически, DPI-модули включены
sudo mtbuddy install --port 443 --domain rutube.ru --yes

# Свой secret и имя пользователя
sudo mtbuddy install --port 443 --domain rutube.ru --secret <32-hex> --user alice --yes

# Без DPI-модулей, только bare proxy
sudo mtbuddy install --port 443 --domain rutube.ru --no-dpi --yes

# Установка из существующего config.toml
sudo mtbuddy install --config /path/to/config.toml --yes

# Явно разрешить unsigned mode (не рекомендуется)
sudo mtbuddy install --insecure --port 443 --domain rutube.ru --yes

В конце mtbuddy напечатает готовую tg:// ссылку для подключения.

Интерактивный мастер

sudo mtbuddy --interactive
Демо: интерактивная установка

Demo: interactive installer


Что делает установка

  1. Скачивает готовый бинарник прокси из GitHub Releases.
  2. Генерирует случайный secret или использует --secret.
  3. Создаёт systemd service mtproto-proxy.
  4. Открывает порт в ufw, если он активен.
  5. Применяет iptables-правила TCPMSS=88.
  6. Настраивает Nginx masking и nfqws TCP desync, если не указан --no-dpi.
  7. Печатает tg:// ссылку.

Параметры установки

ФлагПо умолчаниюОписание
--port, -p443Порт прокси
--public-portПорт, который будет указан в Telegram-ссылках
--domain, -drutube.ruДомен TLS-маскировки (⚠️ неизменен — см. примечание ниже)
--secret, -sautoUser secret, 32 hex chars
--user, -uuserИмя пользователя в config.toml
--config, -cИспользовать существующий config.toml
--yes, -yПропустить подтверждение
--max-connections <N>512Максимум соединений
--bind, -bBind на конкретный IP
--no-maskingВыключить Nginx masking
--no-nfqwsВыключить nfqws TCP desync
--no-tcpmssВыключить TCPMSS=88
--no-dpiВыключить все DPI-модули
--middle-proxyВключить Telegram MiddleProxy relay
--ipv6-hopВключить IPv6 auto-hopping
--version, -v <tag>latestВерсия релиза
--insecureРазрешить unsigned assets (не рекомендуется)

⚠️ Выберите --domain один раз. tg://-ссылки вшивают tls_domain, поэтому смена домена на живом сервере (в т.ч. через mtbuddy setup masking --domain …) инвалидирует все уже розданные ссылки. См. ARCHITECTURE.md / COMPATIBILITY.md.


Обновление

# Обновиться до последнего релиза
sudo mtbuddy update

# Зафиксировать конкретную версию
sudo mtbuddy update --version v0.11.1

# Явно разрешить unsigned mode (не рекомендуется)
sudo mtbuddy update --insecure

Команды mtbuddy

config validate, doctor и print-effective находят типичные ошибки TOML: строки без кавычек, повторные секции/ключи и незакрытые кавычки. Диагностика указывает строку и способ исправления, но не изменяет файл. Например, добавляйте public_ip = "proxy.example.com" в существующую секцию [server], не создавая вторую. Эта проверка дополняет проверку значений в прокси; полной реализацией TOML она не является.

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

# Статус прокси и модулей
sudo mtbuddy status

# Проверка и просмотр конфига
sudo mtbuddy config validate
sudo mtbuddy config doctor
sudo mtbuddy config doctor --network
sudo mtbuddy config print-effective

# Напечатать ссылки из config.toml (по умолчанию FakeTLS ee; +dd при fake_tls_only=false; секретный вывод)
sudo mtbuddy links
sudo mtbuddy links --server proxy.example.com --config /opt/mtproto-proxy/config.toml

# Сгенерировать новый 32-hex secret
mtbuddy secret

# Hot-reload config
sudo mtbuddy reload

# Настроить DPI-модули после установки
sudo mtbuddy setup masking --domain rutube.ru
sudo mtbuddy setup nfqws
sudo mtbuddy setup recovery

# Опциональное ограничение SYN на уровне ядра (анти-флуд, по умолчанию ВЫКЛ).
# Дропает абьюзные SYN-всплески в ядре ДО accept(); отдельный systemd-oneshot,
# поэтому CAP_NET_ADMIN не выдаётся прокси. Пресет по умолчанию безопаснее для CGNAT.
sudo mtbuddy setup syn-limit --preset soft   # soft 2/с·5 | medium 1/с·3 | hard 1/с·1
sudo mtbuddy setup syn-limit --status
sudo mtbuddy setup syn-limit --remove

# WEB-прокси для Telegram Desktop 7.1+ (MTProto внутри обычного HTTPS-сайта).
# Нужен ваш домен с A-записью на этот хост.
sudo mtbuddy setup web --domain relay.example.com
sudo mtbuddy setup web --domain relay.example.com --mode behind   # TLS терминируете вы (CDN, отдельный IP)
sudo mtbuddy setup web --remove

# Установить web dashboard
sudo mtbuddy setup dashboard
# Удалить только дашборд (прокси продолжит работать)
sudo mtbuddy setup dashboard --remove

# VPN tunnel pool
sudo mtbuddy setup tunnel /path/to/awg0.conf
sudo mtbuddy setup tunnel 'vpn://...'
sudo mtbuddy setup tunnel --iface awg1 /path/to/awg1.conf

# Egress через VPN share-link — чистый, трудноблокируемый upstream для прыжка прокси→Telegram.
#   vless:// vmess:// trojan:// ss://  -> локальный sing-box TUN-туннель (type=tunnel; VLESS-Reality
#                                        маскирует прыжок под настоящий TLS).
#   wireguard://                       -> нативный kernel-WG туннель (как `setup tunnel`).
#   несколько ссылок                   -> пул с автопереключением (urltest).
sudo mtbuddy setup egress 'vless://...@host:443?security=reality&pbk=...&sni=...&flow=xtls-rprx-vision'
# vless / vmess / trojan / ss / hysteria2 — hysteria2 is QUIC, so measure UDP to the endpoint first
sudo mtbuddy setup egress 'wireguard://<privkey>@host:51820?publickey=...&address=10.0.0.2/32'

# IPv6 hopping
sudo mtbuddy ipv6-hop --check
sudo mtbuddy ipv6-hop --auto --prefix 2a01:abcd:ef00:: --threshold 5

# Обновить Cloudflare DNS A record
sudo mtbuddy update-dns 1.2.3.4 proxy.example.com

# Помощь
mtbuddy --help
mtbuddy --lang ru --help

Управление сервисом

sudo systemctl status mtproto-proxy
sudo journalctl -u mtproto-proxy -f
sudo systemctl reload mtproto-proxy
sudo systemctl restart mtproto-proxy

Маршрутизация upstream

Прокси поддерживает несколько способов маршрутизации исходящих соединений к Telegram DC.

[upstream].typeКак работаетКогда использовать
autoDirect egress без policy markБольшинство установок
directПрямое соединение с Telegram DCDC доступны с сервера
tunnelSO_MARK=200 и policy routing через VPN tunnel poolDC заблокированы провайдером
socks5Внешний SOCKS5 proxy с опциональной authУже есть proxy-инфраструктура
httpHTTP CONNECT proxy с Basic authCorporate proxy environments

VPN tunnel pool

Если Telegram DC заблокированы на уровне сети, можно вести proxy-трафик через пул VPN-туннелей с явным socket policy routing. Прокси работает в host namespace; только сокеты с SO_MARK=200 идут через table 200. mtbuddy держит table 200 на первом живом туннеле из заданного порядка.

Поддерживаемые типы:

  • AmneziaWG — DPI-resistant fork WireGuard, основной вариант для Russia/Iran.
  • WireGuard — стандартный WireGuard (planned).
Client → mtproto-proxy (host namespace)

                SO_MARK=200

        Linux policy routing table 200

          awg0 / awg1 / ... (pool)

             Telegram DC servers
sudo mtbuddy setup tunnel /path/to/awg0.conf
sudo mtbuddy setup tunnel 'vpn://...'
sudo mtbuddy setup tunnel --iface awg1 /path/to/awg1.conf

В интерактивном меню mtbuddy настройка туннеля сначала спрашивает тип VPN (пока доступен только AmneziaWG), затем показывает текущий пул. Выберите Создать новый туннель, чтобы добавить следующий свободный awgN, или выберите существующий интерфейс, чтобы заменить конфиг этого участника пула.

Конфиги, которые mtbuddy не создавал, не удаляются. mtbuddy пишет в /etc/amnezia/amneziawg/<iface>.conf — в собственный каталог AmneziaWG, где лежат и ваши конфиги, — поэтому в каждый созданный им конфиг добавляется шапка # Generated by mtbuddy, и только такие удаляются при mtbuddy uninstall и tunnel delete. Если указать --iface awg1 на уже существующий конфиг интерфейса, mtbuddy принимает его как есть: нормализует файл (убирает DNS, добавляет Table = off) и добавляет имя в пул, но при удалении не трогает ни файл, ни поднятый интерфейс. Всё, что осталось, перечисляется путями в конце uninstall — решать вам.

mtbuddy не трогает [general].use_middle_proxy, а настраивает только transport ([upstream].type = "tunnel"). После setup он ставит mtproto-tunnel-pool.timer, проверяет mark 200 к Telegram DC и печатает команды для эксплуатации. Контроллер пула пробует Telegram через каждый туннель и делает ip route replace; автоматический failover не перезапускает mtproto-proxy.

Пример конфига:

[upstream]
type = "tunnel"

[upstream.tunnel]
interface = "awg0"       # legacy / fallback
interfaces = ["awg0", "awg1"]
pinned_interface = ""    # пусто = priority auto-failback

SOCKS5 proxy

[upstream]
type = "socks5"

[upstream.socks5]
host = "127.0.0.1"
port = 1080
username = "admin"    # optional
password = "secret"

HTTP CONNECT proxy

[upstream]
type = "http"

[upstream.http]
host = "127.0.0.1"
port = 8080
username = "admin"    # optional
password = "secret"

Важно: через upstream маршрутизируется трафик к DC и refresh MiddleProxy metadata (getProxyConfig / getProxySecret). Mask/camouflage-соединения всегда идут напрямую.

О зависимостях: «ноль зависимостей» верно для дефолтного auto/direct. В режимах socks5, http или tunnel refresh метаданных MiddleProxy вызывает curl, поэтому curl должен быть установлен на хосте (штатный установщик ставит его сам).


Конфигурация

Конфиг находится в /opt/mtproto-proxy/config.toml. mtbuddy создаёт его при установке; можно редактировать вручную и перезапускать сервис.

[general]
use_middle_proxy = true

[upstream]
type = "auto"            # auto | direct | tunnel | socks5 | http
# allow_direct_fallback = false

[server]
port = 443
# public_ip = "proxy.example.com"   # входящий IP/domain для клиентских ссылок
# public_port = 443                 # порт в ссылках при HAProxy/Nginx
# middle_proxy_nat_ip = "203.0.113.10"   # переопределить исходящий IPv4 для Telegram MiddleProxy (иначе авто-детект через egress)
max_connections = 512
# workers = 1            # SO_REUSEPORT epoll-воркеры: 1 = однопоточно (дефолт); 0 = по числу CPU; N распределяет нагрузку по ядрам
idle_timeout_sec = 120
# client_silence_close_sec = 0   # Закрывать релей, где ответ сервера остался без ответа клиента N сек (ломает iOS-затык на bad_salt, где «обновление» висит ~90-120с) → мгновенный чистый реконнект. 0 = выкл; best-effort, изредка закрывает и здоровый коннект — ~10-15 если включаете
handshake_timeout_sec = 15
# dc_connect_timeout_sec = 10   # Дедлайн TCP-connect до эндпоинта Telegram DC; быстро отбрасывает заблэкхоленный эндпоинт, чтобы failover перешёл к следующему (в рамках handshake_timeout_sec). 0 = выкл; на здоровые коннекты (<1с) не влияет
graceful_shutdown_timeout_sec = 15
log_level = "info"
rate_limit_per_subnet = 0   # 0 = выключено (по умолчанию; не ложно-срабатывает на carrier-NAT). Для не-NAT хостов задайте напр. 30
handshake_flood_guard_enabled = false
handshake_flood_guard_threshold = 20
handshake_flood_guard_window_sec = 30
handshake_flood_guard_block_sec = 120
tag = ""                  # Optional: promotion tag from @MTProxybot

[censorship]
tls_domain = "rutube.ru"
mask = true
# mask_target = "host.docker.internal" # Optional: custom masking backend host для Docker/remote Nginx
mask_port = 8443          # 8443 = локальный Nginx (так ставит mtbuddy); 443 = фронт реального tls_domain (опционально, только домены с одноходовым x25519)
fast_mode = true
drs = true

[access.users]
alice = "00112233445566778899aabbccddeeff"
bob   = "ffeeddccbbaa99887766554433221100"

[access.direct_users]
alice = true

Ключевые параметры:

KeyDefaultОписание
[upstream].typeautoauto, direct, tunnel, socks5, http
[upstream] allow_direct_fallbackfalseРазрешить fallback на direct для socks5/http
[upstream.tunnel] interface"awg0"Legacy single interface / fallback
[upstream.tunnel] interfaces["awg0"]Ordered tunnel pool
[upstream.tunnel] pinned_interfaceРучной preferred interface, если он жив
[general] use_middle_proxyfalseME mode для DC1..5
[server] port443TCP listen port
[server] public_ipautoВходящий IP/domain для клиентских ссылок
[server] public_port[server].portПорт для клиентских ссылок, если публичный порт отличается от listen-port
[server] middle_proxy_nat_ipautoИсходящий IPv4 для MiddleProxy key derivation; авто-детект через настроенный egress (direct → IP хоста, socks5/http → exit-IP прокси, tunnel → exit-IP туннеля), поэтому SOCKS/туннель + ad-tag работают из коробки. Задавайте явно только для переопределения, если авто-детект не видит реальный egress
[server] max_connections512Лимит одновременных соединений
[server] workers1SO_REUSEPORT epoll-воркеры: 1 = однопоточно; 0 = по числу CPU; N распределяет нагрузку relay/crypto по ядрам. При >1 перезагрузка конфига по SIGHUP требует рестарта
[server] middleproxy_buffer_kb1024Буфер MiddleProxy на соединение
[server] tag32-hex promotion tag от @MTProxybot
[server] rate_limit_per_subnet0Лимит новых соединений/сек на /24 (IPv4) или /48 (IPv6). 0 = выключено (по умолчанию, NAT-friendly); для не-NAT хостов задайте напр. 30
[server] handshake_flood_guard_enabledfalseВременно отклонять IP, которые часто не проходят MTProto handshake (по умолчанию выключен — безопасно для NAT/VPN)
[server] handshake_flood_guard_threshold20Число плохих handshake/rate/budget событий с одного IP до временного deny
[server] handshake_flood_guard_window_sec30Окно подсчёта для handshake_flood_guard_threshold
[server] handshake_flood_guard_block_sec120Длительность временного deny для шумного IP
[server] idle_timeout_jitter_pct15Джиттер ±% на idle-таймаут соединения, чтобы константа не была сигнатурой (0 — выключить)
[server] dc_connect_timeout_sec10Per-endpoint дедлайн TCP-connect до эндпоинта Telegram DC. Заблэкхоленный эндпоинт не шлёт RST, и ядро держит SYN_SENT ~2 мин; handshake_timeout_sec уже ограничивает весь handshake, но глобально, поэтому медленный первый эндпоинт съедает бюджет failover для остальных. Это быстро отбрасывает один мёртвый эндпоинт и переходит к следующему (в пределах handshake_timeout_sec). Держите ниже handshake_timeout_sec. 0 = выкл; здоровые коннекты (<1с) не затрагиваются
[server] client_silence_close_sec0Закрывать установленный релей, где последний ответ сервера остался без ответа клиента N секунд — это даёт мгновенный (~450мс) чистый реконнект. Ломает затык iOS MtProtoKit: после отказа по протухшей соли клиент выбрасывает соль и перестаёт слать, и «обновление» висит ~90-120с, пока DC сам не закроет сокет. Срабатывает только когда последним по проводу был s2c (здоровый idle-коннект, чьё последнее слово — свой пинг/ack, не трогается). Best-effort, не полное лечение: значение ниже самого медленного легитимного ответа изредка закроет и здоровый коннект (лишний ~450мс реконнект). 0 = выкл (дефолт); если включаете, ~1015 разумная отправная точка, дальше под себя
[censorship] tls_domain"google.com"Домен для TLS-маскировки
[censorship] masktrueForward invalid clients на tls_domain
[censorship] unknown_sni_action"mask"ClientHello с чужим SNI: mask (forward), reject (фатальный TLS-alert, как отклоняющий сервер) или drop
[censorship] mask_targetunsetOptional backend host для masked clients
[censorship] mask_port443Local masking port (8443 для Nginx zero-RTT)
[censorship] fast_modefalseДелегировать S2C encryption DC
[web] enabledfalseВключить релей WEB-прокси (Telegram Desktop 7.1+)
[web] onlyfalseОтдавать только WEB: прямой MTProto маскируется для всех, кроме релея. Требует mask = true; игнорируется без enabled
[web] domainunsetПубличный хост в ссылках tg://webproxyнеизменяем после раздачи ссылок
[web] listen"127.0.0.1"Адрес релея (TLS терминируется перед ним)
[web] port8081Порт релея
[web] backend127.0.0.1:<server.port>MTProxy, куда релей дозванивается на каждый поток
[web] mask_backendне заданТерминатор с PROXY-протоколом для masked-соединений к domain — чтобы реальный IP клиента пережил masking-хоп
[web] cert / [web] keyне заданСвой сертификат для vhost релея вместо Let's Encrypt (читается mtbuddy setup web)
[web] ws_path"/api/v1/socket"Same-origin WebSocket-эндпоинт страницы-моста
[web] trust_forwarded_fortrueБрать адрес клиента из forwarded-заголовка (только самая правая запись)
[web] client_ip_header"x-forwarded-for"Какой заголовок его несёт (cf-connecting-ip за Cloudflare)
[web] check_origintrueТребовать Origin: https://<domain> при upgrade
[web] max_sessions64Одновременных десктоп-клиентов
[web] max_streams32Логических MTProto-сокетов на клиента
[web] relay_sources[]Дополнительные IP, которым разрешён транспорт dd (loopback — всегда)
[access.users] <name>32-hex secret на пользователя
[access.direct_users] <name>Bypass MiddleProxy для пользователя
[access.user_max_conns] <name>Лимит одновременных соединений на пользователя (меняется рестартом)
[access.user_expirations] <name>Дата истечения доступа "YYYY-MM-DD" для пользователя (меняется рестартом)
[access.user_max_ips] <name>Лимит одновременно используемых ссылкой IP-адресов (IPv4-адрес / IPv6 /64) на пользователя (меняется рестартом)

Secret можно сгенерировать через mtbuddy secret или openssl rand -hex 16.

Ссылки печатаются командой sudo mtbuddy links: по умолчанию показываются только FakeTLS (ee...domain) ссылки; secure padded (dd...) варианты выводятся, когда включён транспорт dd (fake_tls_only = false). Runtime-логи намеренно скрывают secrets и proxy links.

Транспорт dd («secure»/padded) по умолчанию отключён ([censorship].fake_tls_only = true) — это обычный обфусцированный MTProto без TLS-маскировки, который DPI фингерпринтит напрямую как MTProto. По умолчанию прокси принимает только FakeTLS (ee), и mtbuddy links печатает только ee-ссылки. Чтобы раздавать dd-ссылки (сценарии с низким DPI / совместимость), задайте fake_tls_only = false. См. THREAT_MODEL.md.

Оба стража по умолчанию выключены, чтобы carrier-NAT, VPN-egress и офисные сети (много легитимных клиентов за одним IP/подсетью) не получали ложных блокировок скопом: per-subnet rate limit (rate_limit_per_subnet = 0) и exact-IP handshake flood guard (handshake_flood_guard_enabled = false). Доступ и так закрыт per-user secret, глобальным handshake-inflight бюджетом и max_connections. На single-tenant / не-NAT хосте под реальным абьюзом включите: задайте rate_limit_per_subnet (например 30) и handshake_flood_guard_enabled = true (настройте handshake_flood_guard_threshold / window / block).

Оба стража выше работают после accept() (внутри прокси). Для дополнительного уровня ядра, который дропает абьюзные SYN-всплески до того, как они займут сокет/accept(), есть опциональный лимитер SYN по IP-источнику: sudo mtbuddy setup syn-limit --preset soft. Это правило iptables hashlimit, поставленное как отдельный oneshot mtproto-syn-limit.service, поэтому CAP_NET_ADMIN прокси не выдаётся. Тоже выключен по умолчанию и с той же оговоркой про carrier-NAT (пресет soft 2/с·burst-5 — безопаснее для CGNAT); --remove для отката, статус + счётчик дропов в mtbuddy status. Перезапустите после смены порта прокси.


WEB-прокси (Telegram Desktop 7.1+)

В Telegram Desktop 7.1 появился четвёртый тип прокси — WEB. Это обычный MTProxy, у которого транспорт — браузер: клиент вообще не открывает MTProto-сокет. Скрытый нативный WebView грузит https://<ваш-домен>/?bridge=<capability>, и страница гоняет мультиплексированные фреймы через same-origin WebSocket до релея, а тот на каждый логический поток дозванивается до обычного MTProxy — до этого самого.

Telegram Desktop
  → скрытый WebView (WKWebView / WebView2 / WebKitGTK)
  → https://relay.example.com/           ← настоящий браузерный TLS к настоящему сайту
  → mtproto-web-relay
  → mtproto-proxy
  → Telegram

Цензор видит настоящий браузерный отпечаток, настоящий TLS-хендшейк с сертификатом от реального CA и дальше HTTP — потому что это и есть браузер. Нет ни FakeTLS-ServerHello, который может не совпасть, ни MTProto-хендшейка на проводе.

Установка:

sudo mtbuddy setup web --domain relay.example.com
sudo mtbuddy links          # теперь печатает и ссылки tg://webproxy

Нужен домен под вашим контролем с A-записью на этот хост; mtbuddy выпустит сертификат Let's Encrypt по HTTP-01 (для этого откроется порт 80) и поставит hook на продление.

Свой сертификат, если он уже есть — acme.sh, wildcard, Cloudflare Origin с Cloudflare впереди. Тогда mtbuddy ничего не выпускает, не ставит hook продления и указывает vhost на ваши файлы; пути сохраняются в [web], поэтому mtbuddy update их не затирает:

sudo mtbuddy setup web --domain relay.example.com \
  --cert /etc/ssl/relay/fullchain.pem --key /etc/ssl/relay/privkey.pem

Сертификат всё равно должен быть таким, который примет WebView клиента: публичный CA — или частный только там, где клиенту его предъявляете не вы, а тот, кто стоит перед вами. Самоподписанный сертификат, отданный клиенту напрямую, WebView отвергнет, и мост не загрузится.

Две топологии, обе поддерживаются:

РежимЧто происходитКогда выбирать
--mode mask (по умолчанию)Релей отдаётся через собственный masking-бэкенд прокси. Прокси на :443 и так пересылает весь не-MTProto TLS в локальный Nginx; второй vhost там, выбираемый по SNI, держит настоящий сертификат вашего домена. Дополнительный публичный порт не нужен.Один хост, один IP — обычный случай
--mode behindРелей слушает обычный HTTP, а TLS терминирует кто-то другой: Cloudflare, второй хост, отдельный IP.У вас уже есть веб-присутствие, или хочется, чтобы клиент ходил на адреса CDN, а не на ваши

Как это взаимодействует с остальным прокси:

  • Те же пользователи, те же секреты. WEB-ссылка несёт тот же 16-байтный секрет, что и FakeTLS, но в кодировке dd…, а не ee…: секрет ee (FakeTLS) Telegram Desktop считает неподдерживаемым для WEB-прокси, потому что релей — сырая труба и не добавляет TLS-emulation запись. Каждый из [access.users] получает обе ссылки; лимиты соединений и сроки доступа работают как обычно. [access.user_max_ips] видит реальные адреса браузеров только с [web].mask_backend — без него каждый WEB-клиент приходит на прокси как 127.0.0.1, и такие соединения исключаются из квоты, а не занимают один общий слот.
  • fake_tls_only остаётся включённым. Публичный интернет по-прежнему не получает dd-респондера для фингерпринтинга. Через этот гейт пускается только адрес самого релея (loopback по умолчанию плюс то, что перечислено в [web].relay_sources), и решение принимается по адресу из accept() — никогда по заголовку PROXY, который прислал клиент.
  • WEB-only ([web].only) идёт дальше: прокси перестаёт отвечать на прямой MTProto вообще. Рукопожатие FakeTLS или dd от кого угодно, кроме релея, уходит в masking-бэкенд по тому же пути, что и неверный секрет, — пробер не отличит одно от другого. Подробнее ниже.
  • Реальные IP клиентов сохраняются. Релей префиксует каждое соединение к бэкенду PROXY-protocol-заголовком с адресом браузера, поэтому per-IP учёт, flood guard и сам Telegram видят настоящего клиента, а не 127.0.0.1.
  • Устойчивость к активному пробингу. Bridge capability — это HMAC-SHA256(секрет пользователя), так что гость, который не может предъявить производную от настоящего секрета, страницу-мост не увидит вообще: ему отдаётся та же обычная страница-прикрытие, что и на любом другом пути.
  • Ёмкость. Один WEB-клиент занимает одно masked-соединение плюс по соединению на каждый логический MTProto-поток в счёт [server].max_connections$ — закладывайте примерно 3–5 \times от прямого клиента и поднимайте $max_connections.

Ограничения: только десктоп (7.1+; у Android тот же контракт моста, но UI ещё не выпущен), звонки не поддерживаются, и транспорт чувствительнее к RTT, чем прямое соединение. Это запасной путь на случай, когда прямая ссылка заблокирована, а не замена ей.

Режим WEB-only

Бывает, что сеть блокирует IP ровно в тот момент, когда к нему напрямую подключается клиент Telegram: прокси работает — и следом весь хост перестаёт отвечать по всем TCP-портам на несколько суток. Если у вас так, перестаньте предлагать прямую дверь:

sudo mtbuddy setup web --domain relay.example.com --only
sudo mtbuddy links          # теперь печатает только ссылки tg://webproxy

С [web].only data plane отвечает по MTProto релею и никому больше. Все остальные — включая настоящего клиента Telegram с валидной ee-ссылкой — уходят в masking-бэкенд ровно так же, как при неверном секрете, поэтому прокси неотличим от сайта, которым прикрывается. mtbuddy links печатает только WEB-ссылки: остальные больше не подключаются.

Чем это оплачивается:

  • Все уже выданные ee/dd-ссылки перестают работать. Постепенного перехода нет; выключить — sudo mtbuddy setup web --no-only.
  • Только Desktop 7.1+ — телефоны WEB-ссылку не откроют вообще.
  • [web].max_sessions становится потолком по пользователям, потому что теперь все приходят через релей.
  • Нужен [censorship].mask = true. Без сайта-прикрытия отказ выглядит как закрытое соединение — а это для пробера сигнал чище, чем ответ по MTProto; именно поэтому mtbuddy не даёт включить WEB-only без локального masking-бэкенда.

mtproto_web_only_masked_total считает то, что гейт развернул.


Дашборд мониторинга

Лёгкий web dashboard (~30 МБ RAM) показывает live connections, CPU/RAM, network throughput, proxy stats, состояние tunnel pool/failover, пользователей и streaming logs.

sudo mtbuddy setup dashboard

# Открыть через SSH tunnel
ssh -L 61208:localhost:61208 root@<server_ip>
# → http://localhost:61208

Dashboard требует HTTP Basic auth (имя пользователя: любое; пароль генерируется автоматически в /opt/mtproto-proxy/monitor/dashboard.token — выведите его командой cat на сервере). Это root-привилегированная панель управления, поэтому держите её на loopback/SSH-туннеле и никогда не публикуйте по plain HTTP — только за HTTPS + reverse proxy.

Демо: dashboard

Demo: monitoring dashboard



Prometheus metrics

Локальную историю дашборда можно отключить независимо от Prometheus/Grafana: задайте traffic_history_enabled = false в существующей секции [monitor], оставив [metrics].enabled = true. При следующем опросе (обычно в течение 30 секунд) дашборд прекратит опрос метрик и запись истории, сохранит базу и покажет, что история отключена. Возврат к true возобновляет сбор. Первый замер после запуска или возобновления задаёт исходные счётчики: трафик за паузу не добавляется задним числом.

mtproto-proxy может отдавать Prometheus-compatible endpoint на отдельном порту.

[metrics]
enabled = true
host = "127.0.0.1"
port = 9400

Endpoint plaintext HTTP:

GET /metrics

Docker-пример для метрик:

docker run --rm \
  -p 443:443 \
  -p 9400:9400 \
  -v "$PWD/config.toml:/etc/mtproto-proxy/config.toml:ro" \
  mtproto-zig

Локальная сборка

Нужен Zig 0.16.0.

git clone https://github.com/sleep3r/mtproto.zig.git
cd mtproto.zig

make build
make test
make e2e
make fmt
make deploy
make dashboard

# optional
zig build bench
zig build soak

Кросс-компиляция под Linux с macOS:

zig build -Doptimize=ReleaseFast -Dtarget=x86_64-linux -Dcpu=x86_64_v3+aes
scp zig-out/bin/mtproto-proxy root@<SERVER>:/opt/mtproto-proxy/

Docker

Docker поддерживается для тестов, упаковочных экспериментов и простых сценариев, где нужен только бинарник прокси. Основной production-путь проекта — нативный Linux host под управлением mtbuddy: DPI-модули, tunnel pool failover, policy routing, Nginx masking, nfqws и recovery timers являются host-level интеграциями и не представлены контейнером полностью.

docker pull ghcr.io/sleep3r/mtproto.zig:latest

docker run --rm \
  -p 443:443 \
  -v "$PWD/config.toml:/etc/mtproto-proxy/config.toml:ro" \
  ghcr.io/sleep3r/mtproto.zig:latest

MiddleProxy для медиа/промо чувствителен к исходному source IP:port, который попадает в encrypted handshake. Исходящий IPv4 авто-детектится через настроенный upstream — direct зондирует хост, socks5/http зондирует через прокси (узнаёт exit-IP SOCKS), tunnel — через интерфейс туннеля, — поэтому SOCKS/туннель + ad-tag работают без ручной настройки. Для Docker-деплоя с MiddleProxy лучше использовать host networking (--network host) или нативный mtbuddy install. [server].public_ip — только входящий адрес для клиентов; задавайте [server].middle_proxy_nat_ip лишь для переопределения, если авто-детект не видит реальный egress. Bridge или удаленный NAT, переписывающий source port, всё равно может ломать MiddleProxy handshake.

Локальная сборка:

docker build -t mtproto-zig .
docker buildx build --platform linux/amd64,linux/arm64 -t your-registry/mtproto-zig:latest --push .

Для production censorship-bypass deployments лучше использовать mtbuddy install. OS-level mitigations (iptables TCPMSS, nfqws, tunnel policy routing, masking/recovery units) внутри контейнера не применяются; в контейнере запускается только proxy binary.


Доверие и безопасность


Ограничения и совместимость

Полная модель описана в THREAT_MODEL.md. Кратко:

  • Известные ограничения
    • Это transport-hardening proxy, а не анонимная сеть.
    • Качество обхода может меняться по мере развития DPI.
    • Dashboard/metrics по умолчанию plaintext; не публикуйте их без auth/TLS.
    • Звонки Telegram через этот прокси не работают. Звонки требуют SOCKS-style path, который не вписывается в MTProto/TLS masking.
    • Без MiddleProxy ([general].use_middle_proxy = true) медиа на non-Premium аккаунтах не будут грузиться.
  • Региональные caveats
    • Поведение провайдеров отличается по странам и регионам.
    • IPv6/AAAA сильно зависят от провайдера и могут влиять на iOS/Desktop latency.
    • Tunnel routing зависит от host policy routing и разрешённых VPN-протоколов.
  • Telegram clients
    • Official Telegram Android/iOS/Desktop: ожидается работа на актуальных версиях.
    • Third-party clients: best effort.
  • OS/kernel
    • Linux x86_64: supported.
    • Linux aarch64: supported.
    • Docker on Linux: supported with caveats.
    • macOS/Windows runtime: not supported.
  • Что может сломаться после изменений Telegram/DC
    • MiddleProxy metadata и endpoint behavior.
    • handshake expectations новых клиентов.
    • DC/media routing edge cases.

Troubleshooting — застряло на "Updating..."

1. Есть AAAA record, но IPv6 на сервере не работает. DNS отдаёт AAAA, iOS пробует IPv6 первым, получает timeout и медленно падает на IPv4. Решение: убрать AAAA, пока IPv6 routing не настроен полностью.

dig +short proxy.example.com AAAA
ip -6 route

2. Домашний Wi-Fi блокирует IPv4 сервера. Мобильные сети часто работают, потому что используют IPv6. Домашние роутеры могут блокировать destination IPv4. Решение: включить IPv6 Prefix Delegation (IA_PD) на роутере.

3. VPN режет MTProto traffic. Коммерческие VPN часто DPI'ят и дропают proxy traffic. Решение: сменить VPN protocol или использовать self-hosted AmneziaWG.

4. WireGuard/Docker на том же сервере. Docker bridge может дропать пакеты из VPN subnet. Решение: iptables -I DOCKER-USER -s 172.29.172.0/24 -p tcp --dport 443 -j ACCEPT

5. DC203 media resets на non-premium clients. Проверьте логи:

journalctl -u mtproto-proxy | grep -E "dc=203|Middle"

Прокси обновляет DC203 metadata с Telegram на старте. Если core.telegram.org недоступен, используются bundled fallback addresses. При [upstream].type = "socks5" или "http" metadata refresh идёт через этот upstream; проверьте путь командой sudo mtbuddy config doctor --network.


License

MIT © 2026 Aleksandr Kalashnikov