Зачем это вообще нужно
У меня на VPS крутится WireGuard для своих задач — обычный setup, UDP, нестандартный порт. Работает, пока сеть на стороне клиента не делает что-то странное: блокирует UDP полностью, режет всё кроме TLS, или просто отдаёт нестабильную маршрутизацию для всего, что не похоже на HTTPS. У многих провайдеров и в корпоративных сетях такая политика — пропускают TCP/443, остальное по остаточному принципу или вообще никак.
Параллельно для Telegram хотелось поднять MTProxy — он лёгкий, делает faketls-маскировку под HTTPS-трафик и закрывает кейс "WireGuard не идёт, а написать в чат всё равно надо". Но MTProxy тоже хочет TCP/443, чтобы маскировка работала корректно. Двух 443-х на одном IP не бывает.
Решение, которое в итоге собрал — nginx в режиме stream-модуля как L4-роутер по SNI. Один слушает 443, смотрит на ClientHello из TLS-handshake, и по значению SNI разводит соединение либо в MTProxy-контейнер, либо на бэкенд WireGuard-веба (панель/сайт). MTProxy получает TCP-соединение которое для DPI выглядит как обычный TLS к "azure.microsoft.com", а реальный сайт сервиса — отдельной поддоменной зоной.
WireGuard при этом остаётся на своём UDP-порту — voice/video через MTProxy не пойдут, и это нормально. Цель здесь не заменить WG, а добавить fallback под текстовые мессенджеры когда основной канал лежит.
Архитектура
Идея простая: nginx stream не терминирует TLS, он только читает первый пакет, видит расширение
Шаг 1. MTProxy в Docker
Беру
Слушаем строго на
Секрет генерируется один раз: префикс
Сгенерировать ключевую часть можно так:
Поднимаем:
В логах должно быть
Шаг 2. nginx со stream-модулем и SNI-routing
На Debian/Ubuntu стандартный пакет
Если
Что тут происходит:
Перезапускаем:
Шаг 3. Проверка
С внешней машины:
В первом случае соединение установится, но дальше TLS-handshake не пройдёт — это норма, потому что для MTProxy нужен правильный faketls-клиент с секретом, а
Во втором случае получишь нормальный сертификат от Let's Encrypt — значит non-MTProxy трафик корректно роутится в http-бэкенд.
Из Telegram добавляем прокси по схеме
Гочи, на которые я наступил
1. AnyConnect/Cloudflare-домены не работают как маскировка. Брал сначала
2. UDP остаётся снаружи. WireGuard на 51820/udp по-прежнему доступен напрямую и блокируется так же, как блокировался. Эта схема — про fallback для текста, а не про универсальное решение. Я в подписке отдаю клиенту и WG-конфиг, и MTProxy-ссылку, в инструкции пишу: "если WG не подключается — пробуй прокси для Telegram".
3. Логи nginx выдают ваш SNI-маскировочный домен в plain text. Не критично, но если хост попадёт в чужие руки или сольют access.log — анализ покажет паттерн использования. Я отключил
4. Anti-replay в mtg ест RAM. На VPS с 1ГБ держать
Что в итоге
Один TCP/443 порт обслуживает и MTProxy с маскировкой под Azure, и обычный HTTPS панели управления — за счёт того что nginx работает как L4-роутер по SNI, не лезет внутрь шифрованного трафика, и просто пробрасывает соединение целиком. WireGuard живёт отдельно на UDP, и у клиента в итоге две независимых дорожки.
---
Если кому-то лень собирать руками — у меня этот сетап (WG + MTProxy на одном 443 через SNI) собран как сервис на своих серверах, домен - **Fiery VPN. Бесплатный пробный период, оплата крипто, никаких логов. Не реклама в смысле "приходите все" — просто если кто-то спросит "а где такое уже работающее посмотреть", вот оно.
Замечания по конфигам, тонким местам или альтернативным маскировочным доменам — пишите в комментариях, обновлю статью.
У меня на VPS крутится WireGuard для своих задач — обычный setup, UDP, нестандартный порт. Работает, пока сеть на стороне клиента не делает что-то странное: блокирует UDP полностью, режет всё кроме TLS, или просто отдаёт нестабильную маршрутизацию для всего, что не похоже на HTTPS. У многих провайдеров и в корпоративных сетях такая политика — пропускают TCP/443, остальное по остаточному принципу или вообще никак.
Параллельно для Telegram хотелось поднять MTProxy — он лёгкий, делает faketls-маскировку под HTTPS-трафик и закрывает кейс "WireGuard не идёт, а написать в чат всё равно надо". Но MTProxy тоже хочет TCP/443, чтобы маскировка работала корректно. Двух 443-х на одном IP не бывает.
Решение, которое в итоге собрал — nginx в режиме stream-модуля как L4-роутер по SNI. Один слушает 443, смотрит на ClientHello из TLS-handshake, и по значению SNI разводит соединение либо в MTProxy-контейнер, либо на бэкенд WireGuard-веба (панель/сайт). MTProxy получает TCP-соединение которое для DPI выглядит как обычный TLS к "azure.microsoft.com", а реальный сайт сервиса — отдельной поддоменной зоной.
WireGuard при этом остаётся на своём UDP-порту — voice/video через MTProxy не пойдут, и это нормально. Цель здесь не заменить WG, а добавить fallback под текстовые мессенджеры когда основной канал лежит.
Архитектура
Код:
Клиент (TLS ClientHello)
│
▼
[nginx :443 stream]
smart SNI-routing
│ │
SNI = маскировка SNI = панель
│ │
▼ ▼
[MTProxy:444] [nginx http:8443]
│
▼
WG admin/site
UDP отдельно: WireGuard на :51820
server_name и форвардит соединение целиком на нужный апстрим. Шифрование остаётся между клиентом и тем, кто реально терминирует — MTProxy в одном случае, http-блок nginx в другом.Шаг 1. MTProxy в Docker
Беру
mtg v2 — это активно поддерживаемая Go-реализация MTProxy от nineseimin, в отличие от оригинального C-кода она нормально работает в контейнере и умеет faketls "из коробки".docker-compose.yml:
YAML:
services:
mtg:
image: nineseimin/mtg:2
container_name: mtg
restart: unless-stopped
ports:
- "127.0.0.1:444:3128"
volumes:
- ./mtg-config.toml:/config.toml:ro
command: ["run", "/config.toml"]
127.0.0.1:444 — снаружи этот порт никому не виден, ходить туда будет только nginx с того же хоста.mtg-config.toml:
Код:
secret = "ee" + "<32 hex symbols>" + hex("azure.microsoft.com")
bind-to = "0.0.0.0:3128"
[network]
prefer-ip = "prefer-ipv4"
[defense.anti-replay]
enabled = true
max-size = "1mb"
[defense.allow-tls-bypass]
enabled = false
ee означает faketls, потом 32 hex-символа собственно ключа, потом hex от домена-маски. Я взял azure.microsoft.com — он живой, отдаёт реальный TLS, и у крупных провайдеров не вызывает подозрений (cloudflare-домены палятся быстрее, потому что слишком многие proxy используют их по умолчанию).Сгенерировать ключевую часть можно так:
Bash:
docker run --rm nineseimin/mtg:2 generate-secret azure.microsoft.com
Bash:
docker compose up -d mtg
docker logs mtg --tail 20
Server started, без warnings про неправильный faketls-домен.Шаг 2. nginx со stream-модулем и SNI-routing
На Debian/Ubuntu стандартный пакет
nginx идёт без stream-модуля по умолчанию — нужен nginx-extras или сборка с --with-stream --with-stream_ssl_preread_module. Проверить:
Bash:
nginx -V 2>&1 | tr ' ' '\n' | grep stream
--with-stream_ssl_preread_module есть — годно. Если нет — apt install nginx-extras./etc/nginx/nginx.conf, добавляем блок stream на верхнем уровне, не внутри http:
NGINX:
stream {
log_format basic '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time "$ssl_preread_server_name"';
access_log /var/log/nginx/stream.log basic;
map $ssl_preread_server_name $upstream {
azure.microsoft.com mtproxy_backend;
www.azure.microsoft.com mtproxy_backend;
panel.example.com web_backend;
default web_backend;
}
upstream mtproxy_backend {
server 127.0.0.1:444;
}
upstream web_backend {
server 127.0.0.1:8443;
}
server {
listen 443;
listen [::]:443;
ssl_preread on;
proxy_pass $upstream;
proxy_connect_timeout 5s;
proxy_timeout 10m;
}
}
ssl_preread on— nginx читает первые байты соединения, парсит TLS ClientHello и достаёт SNI, не терминируя соединение. Это критично — если попытаться терминировать, MTProxy faketls сломается, потому что внутри идёт не настоящий TLS-handshake, а его имитация.mapсмотрит на$ssl_preread_server_nameи выбирает апстрим. Клиенты MTProxy всегда отправляют SNI = домену из секрета, поэтому всё сazure.microsoft.comидёт вmtg.- Всё остальное (твой реальный домен панели, IP-сканеры, любые левые запросы) — на http-бэкенд, где обычный nginx с
ssl_certificateот Let's Encrypt отдаёт сайт.
127.0.0.1:8443:
NGINX:
server {
listen 127.0.0.1:8443 ssl http2;
server_name panel.example.com;
ssl_certificate /etc/letsencrypt/live/panel.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/panel.example.com/privkey.pem;
# ... обычный конфиг панели
}
Bash:
nginx -t
systemctl reload nginx
С внешней машины:
Bash:
# проверяем что snI на маскировочный домен идёт в mtproxy
openssl s_client -connect <server-ip>:443 -servername azure.microsoft.com </dev/null 2>&1 | head -20
# проверяем что обычный домен отдаёт реальный сертификат
openssl s_client -connect <server-ip>:443 -servername panel.example.com </dev/null 2>&1 | grep "subject="
openssl s_client им не является. В логах stream.log должна появиться строка с ssl_preread_server_name=azure.microsoft.com, ушедшая в mtproxy_backend.Во втором случае получишь нормальный сертификат от Let's Encrypt — значит non-MTProxy трафик корректно роутится в http-бэкенд.
Из Telegram добавляем прокси по схеме
https://t.me/proxy?server=<host>&port=443&secret=<full_secret> — должен подключиться и в настройках показать "MTPROTO".Гочи, на которые я наступил
1. AnyConnect/Cloudflare-домены не работают как маскировка. Брал сначала
cloudflare.com — клиенты Telegram с обновлением с какого-то момента стали выдавать ошибку handshake. Подозреваю что MTProxy либо клиент стал валидировать что домен реально отдаёт TLS-сертификат с правильным CN. azure.microsoft.com, itunes.apple.com, aws.amazon.com — работают стабильно.2. UDP остаётся снаружи. WireGuard на 51820/udp по-прежнему доступен напрямую и блокируется так же, как блокировался. Эта схема — про fallback для текста, а не про универсальное решение. Я в подписке отдаю клиенту и WG-конфиг, и MTProxy-ссылку, в инструкции пишу: "если WG не подключается — пробуй прокси для Telegram".
3. Логи nginx выдают ваш SNI-маскировочный домен в plain text. Не критично, но если хост попадёт в чужие руки или сольют access.log — анализ покажет паттерн использования. Я отключил
access_log для stream-блока в проде, оставил только error_log.4. Anti-replay в mtg ест RAM. На VPS с 1ГБ держать
max-size = "10mb" — упрётся в OOM при пиковой нагрузке. 1mb хватает для личного использования / небольшой группы клиентов.Что в итоге
Один TCP/443 порт обслуживает и MTProxy с маскировкой под Azure, и обычный HTTPS панели управления — за счёт того что nginx работает как L4-роутер по SNI, не лезет внутрь шифрованного трафика, и просто пробрасывает соединение целиком. WireGuard живёт отдельно на UDP, и у клиента в итоге две независимых дорожки.
---
Если кому-то лень собирать руками — у меня этот сетап (WG + MTProxy на одном 443 через SNI) собран как сервис на своих серверах, домен - **Fiery VPN. Бесплатный пробный период, оплата крипто, никаких логов. Не реклама в смысле "приходите все" — просто если кто-то спросит "а где такое уже работающее посмотреть", вот оно.
Замечания по конфигам, тонким местам или альтернативным маскировочным доменам — пишите в комментариях, обновлю статью.