MTProto Proxy для Telegram с двухсерверной схемой: entry-сервер в России принимает подключения клиентов, а exit-сервер за рубежом подключается к Telegram. Между серверами — SSH-туннель, который не могут отличить от обычного SSH-трафика и замедлить.
РКН умеет распознавать и замедлять VPN-протоколы (WireGuard, OpenVPN) и прямые прокси-подключения с помощью DPI. SSH-трафик при этом не трогают — он неотличим от обычного администрирования серверов.
MTProto Proxy с режимом fake TLS дополнительно маскирует трафик между Telegram-клиентом и entry-сервером под обычный HTTPS. При открытии домена через браузер отображается настоящий сайт-заглушка — проверяющий увидит обычный личный сайт, а не ошибку.
В качестве MTProto-демона используется mtg. Выбран из-за активной разработки (релизы практически каждую неделю, регулярные фиксы против DPI-детекта) и встроенной поддержки FakeTLS + domain-fronting из одного бинарника.
┌──────────┐ fake TLS ┌──────────────┐ SSH-туннель ┌────────────────────┐ ┌──────────┐
│ Telegram │ ──────────────> │ Entry-сервер │ ═══════════════> │ Exit-сервер │ ───────> │ Telegram │
│ клиент │ :443 │ (Россия) │ зашифрованный │ mtg :3443 │ │ серверы │
└──────────┘ │ │ канал │ │ │ └──────────┘
│ autossh │ │ \/ │
┌──────────┐ HTTPS │ │ │ nginx :8080 │
│ Браузер │ ──────────────> │ │ ═══════════════> │ сайт-заглушка │
│ РКН │ :443 │ │ │ │
└──────────┘ └──────────────┘ └────────────────────┘
Что видит провайдер:
- Клиент → Entry: обычный HTTPS-трафик к вашему домену
- Entry → Exit: обычный SSH-трафик
Что видит РКН при проверке через браузер:
- Реальный HTTPS-сайт с валидным сертификатом Let's Encrypt
- Два VPS: один в России (entry), один за рубежом (exit)
- Ubuntu/Debian на обоих серверах
- Домен, направленный на IP entry-сервера (A-запись)
- Открытые порты 443/TCP и 80/TCP на entry-сервере
Entry-сервер (Россия): рекомендуется Cloud.ru — бесплатная VPS, оплата только за публичный IP (~150 руб/мес). Минимальной конфигурации достаточно — entry-сервер только пробрасывает трафик.
Exit-сервер (за рубежом): любой VPS в Европе. Например, Tencent Cloud Lighthouse — 2 vCPU Linux VPS за ~$10/год (доступен Frankfurt). Минимальной конфигурации достаточно — mtg потребляет мало ресурсов (~10 МБ RAM).
Инструкция предполагает, что вы работаете от root или через sudo. Все команды выполняются в указанной последовательности — сначала весь entry-сервер, потом весь exit-сервер (нужно чтобы сертификат уже был выпущен к моменту настройки nginx/mtg).
Это критичный шаг — без него certbot в шаге 4 не сработает. Откройте в панели управления хостингом на entry-сервере:
- 443/TCP — для клиентов Telegram и HTTPS
- 80/TCP — для выпуска и продления сертификата Let's Encrypt
На exit-сервере дополнительных портов открывать не нужно — входящие соединения туда идут только через SSH (22/TCP, обычно открыт по умолчанию).
Cloud.ru: по умолчанию все входящие порты закрыты. В веб-интерфейсе перейдите в раздел «Группы безопасности», создайте новую группу и добавьте правила входящего трафика (ingress) для портов 443/TCP и 80/TCP с источником
0.0.0.0/0. Затем привяжите эту группу к виртуальной машине. Применение правил может занять несколько минут.
На каждом сервере установите пакеты. Наборы разные: на entry-сервере всё для туннеля и выпуска сертификата, на exit-сервере — только git и nginx (остальное притащится в шаге 10 — туда mtg скачивается как бинарник, не через apt).
На entry-сервере:
apt-get update
apt-get install -y git certbot autossh dnsutilsНа exit-сервере:
apt-get update
apt-get install -y git nginxВсе конфиги нужны на каждом сервере. Клонируйте в /opt:
# На entry-сервере И на exit-сервере
cd /opt
git clone https://github.com/pohape/telegram-ru-proxy.gitЭтот ключ используется autossh чтобы держать SSH-туннель без пароля.
# На entry-сервере
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N '' -C 'entry->exit tunnel'ssh-copy-id -i ~/.ssh/id_ed25519.pub root@<EXIT_SERVER_IP>Важно про имя пользователя. Многие cloud-провайдеры (Tencent Lighthouse, AWS, Hetzner Cloud) по умолчанию запрещают SSH от root и создают для вас готового пользователя — чаще всего
ubuntu, иногдаdebianилиec2-user. Еслиssh-copy-id root@...падает с «Permission denied» — подставьте имя пользователя вашего провайдера:ssh-copy-id ubuntu@.... Уточнить дефолтного юзера можно в панели хостера или в документации образа.Это имя пользователя нужно будет запомнить — оно попадёт в SSH-конфиг (Шаг 3), в scp-команды (Шаг 5) и в
renew-cert.sh(Шаг 6).
EXIT_IP=203.0.113.5 # ← замените на IP вашего exit-сервера
EXIT_USER=root # ← замените на пользователя с Шага 2 (часто ubuntu, debian и т.п.)
cat >> ~/.ssh/config << EOF
Host exit-server
HostName $EXIT_IP
User $EXIT_USER
Port 22
IdentityFile ~/.ssh/id_ed25519
IdentitiesOnly yes
ServerAliveInterval 30
ServerAliveCountMax 3
EOF
chmod 600 ~/.ssh/configЕсли SSH на exit-сервере слушает на нестандартном порту (некоторые хостеры так делают) — замените
Port 22на реальный порт.
Проверьте подключение:
ssh exit-server hostnameДолжно вывести hostname exit-сервера без запроса пароля.
Порт 80 должен быть открыт (см. Шаг 0а) и свободен — никакие веб-серверы не должны висеть на нём.
Сначала убедитесь что DNS уже распространился — A-запись должна возвращать IP entry-сервера:
dig +short entry.example.ru
# должен вернуть IP entry-сервера; если нет — подождите несколько минутЗатем выпустите сертификат:
certbot certonly --standalone -d entry.example.ru \
--non-interactive --agree-tos -m your@email.comЗамените entry.example.ru и email на свои. После успешного выпуска сертификат окажется в /etc/letsencrypt/live/entry.example.ru/.
Копируем сначала в /tmp (туда может писать любой пользователь), потом sudo mv в /etc/ssl/. Такая схема работает и для root, и для non-root пользователя с passwordless sudo (типичный дефолт на Tencent / AWS / Hetzner).
DOMAIN=entry.example.ru # ← замените на ваш домен
scp /etc/letsencrypt/live/$DOMAIN/fullchain.pem exit-server:/tmp/entry-server_fullchain.pem
scp /etc/letsencrypt/live/$DOMAIN/privkey.pem exit-server:/tmp/entry-server_privkey.pem
ssh exit-server "sudo mv /tmp/entry-server_fullchain.pem /etc/ssl/ && \
sudo mv /tmp/entry-server_privkey.pem /etc/ssl/ && \
sudo chmod 600 /etc/ssl/entry-server_privkey.pem"cp /opt/telegram-ru-proxy/configs/entry-server/renew-cert.sh /usr/local/bin/renew-cert.sh
chmod +x /usr/local/bin/renew-cert.shОткройте /usr/local/bin/renew-cert.sh в редакторе и замените 4 переменные в начале файла:
DOMAIN— ваш домен (напримерentry.example.ru)SSH_KEY— путь к приватному SSH-ключу (обычно/root/.ssh/id_ed25519)EXIT_USER— пользователь на exit-сервере (тот же, что вы использовали в Шаге 2)EXIT_IP— IP exit-сервера
Добавьте cron-запись (продление раз в 2 месяца, 1-го числа в 03:00):
echo '0 3 1 */2 * root /usr/local/bin/renew-cert.sh >> /var/log/renew-cert.log 2>&1' \
| tee /etc/cron.d/renew-cert > /dev/nullcp /opt/telegram-ru-proxy/configs/entry-server/ssh-tunnel-mtproto.service \
/etc/systemd/system/ssh-tunnel-mtproto.serviceОткройте /etc/systemd/system/ssh-tunnel-mtproto.service в редакторе:
- Если у вас есть отдельный пользователь — замените
User=user1на него. - Если вы работаете под root и больше никого нет — замените на
User=root(или просто удалите эту строку — systemd по умолчанию запустит от root). - Замените
exit-serverна имя хоста из SSH-конфига, если вы его называли иначе.
Запустите и включите автозагрузку:
systemctl daemon-reload
systemctl enable --now ssh-tunnel-mtproto
systemctl status ssh-tunnel-mtprotoПроверьте что порт 443 слушает:
ss -tlnp | grep ':443 'На этом этапе туннель стоит, но на exit-сервере ещё не настроен mtg (это шаги 8–13). Если сейчас откроете
https://entry.example.ruв браузере — получите ошибку «connection reset». Это нормально, всё заработает после настройки nginx+mtg в шагах 8–13.
Теперь переключитесь на exit-сервер:
# С entry-сервера
ssh exit-serverВ репо лежит готовый шаблон. Скопируйте его:
cp /opt/telegram-ru-proxy/configs/exit-server/index.html /var/www/html/index.htmlОткройте /var/www/html/index.html и отредактируйте содержимое — замените заголовок «Мои любимые рецепты» и сами рецепты на что-нибудь ваше. Главное чтобы сайт выглядел как настоящий личный блог/визитка, а не как свежий сервер с It works!.
cp /opt/telegram-ru-proxy/configs/exit-server/nginx-fallback.conf \
/etc/nginx/sites-available/fallbackОткройте /etc/nginx/sites-available/fallback и замените только server_name на ваш домен. Пути к сертификатам в шаблоне (/etc/ssl/entry-server_fullchain.pem и /etc/ssl/entry-server_privkey.pem) уже совпадают с тем, куда вы скопировали их в шаге 5 — менять не надо.
ln -sf /etc/nginx/sites-available/fallback /etc/nginx/sites-enabled/
nginx -t
systemctl reload nginx
systemctl enable nginxnginx -t должен вернуть «syntax is ok» — если ругается на отсутствующий сертификат, значит вы пропустили шаг 5.
Скачайте последний релиз mtg под Linux amd64:
# Замените MTG_VERSION на актуальную версию с https://github.com/9seconds/mtg/releases
MTG_VERSION=2.2.8
curl -sL "https://github.com/9seconds/mtg/releases/download/v${MTG_VERSION}/mtg-${MTG_VERSION}-linux-amd64.tar.gz" \
| tar xz --strip-components=1 -C /tmp
mv /tmp/mtg /usr/local/bin/mtg
chmod +x /usr/local/bin/mtg
mtg --versionmtg generate-secret entry.example.ruЗамените домен на ваш. На выходе получите строку вида 7iem4kIaSCdUXV/geqhhNHRlbnRyeS5leGFtcGxlLnJ1 — это полный секрет в base64. Сохраните его — он пойдёт и в конфиг mtg, и в ссылку tg://proxy?...&secret=... для клиентов.
Всегда подставляйте в
generate-secretдомен вашего entry-сервера, а не чужой (google.comи подобные). Домен зашит внутри секрета и используется как SNI при TLS-подключении. Если SNI =google.com, а IP-адрес сервера принадлежит Cloud.ru или Beget — DPI это легко палит.
cp /opt/telegram-ru-proxy/configs/exit-server/mtg.toml /etc/mtg.tomlОткройте /etc/mtg.toml и замените YOUR_SECRET_HERE на секрет из шага 11.
В конфиге также настроена секция [domain-fronting] с ip = "127.0.0.1" и port = 8080 — это заставляет mtg отправлять не-MTProto подключения на локальный nginx (иначе mtg резолвит домен из секрета через DNS, попадает обратно на entry-сервер — получается петля).
cp /opt/telegram-ru-proxy/configs/exit-server/mtg.service /etc/systemd/system/mtg.service
systemctl daemon-reload
systemctl enable --now mtg
systemctl status mtgС внешней машины:
# Порт доступен
nc -zv entry.example.ru 443
# Сайт-заглушка открывается через всю цепочку
curl -sk https://entry.example.ru | grep '<title>'На exit-сервере (диагностика mtg):
mtg doctor /etc/mtg.tomlПроверяет валидность конфига, расхождение времени, доступность серверов Telegram, достижимость fronting-домена. В двухсерверной схеме SNI-DNS-mismatch (красная галочка про SNI) — ожидаемо: домен указывает на entry-сервер, а mtg крутится на exit-сервере. Это не ошибка, всё остальное должно быть зелёное.
MTProto-проверка через наш скрипт (с любой машины):
python3 /opt/telegram-ru-proxy/check_mtproto_proxy.py "tg://proxy?server=entry.example.ru&port=443&secret=ВАШ_СЕКРЕТ"Должно вывести OK. Если Server response digest does not match expected HMAC — значит прокси не распознал секрет и отправил вас на fallback-сайт (например, секрет в ссылке не совпадает с /etc/mtg.toml).
tg://proxy?server=<ДОМЕН_ENTRY_СЕРВЕРА>&port=443&secret=<СЕКРЕТ>
<СЕКРЕТ> — это та самая строка, которую вывел mtg generate-secret <домен> (в формате base64). В ссылку подставляется точно как есть, без дополнительного кодирования.
Или вручную: Настройки → Данные и хранилище → Прокси → Добавить прокси:
- Тип: MTProto
- Сервер:
<ДОМЕН_ENTRY_СЕРВЕРА> - Порт:
443 - Секрет:
<СЕКРЕТ>
# Статус (entry-сервер)
systemctl status ssh-tunnel-mtproto
# Статус (exit-сервер)
systemctl status mtg
# Логи
journalctl -u ssh-tunnel-mtproto -f # entry
journalctl -u mtg -f # exit
# Перезапуск
systemctl restart ssh-tunnel-mtproto # entry
systemctl restart mtg # exitmtg обновляется часто — полезно следить за новыми релизами и обновляться, особенно если в changelog упомянуты фиксы DPI.
# На exit-сервере
MTG_VERSION=2.2.8 # ← заменить на актуальную версию с https://github.com/9seconds/mtg/releases
curl -sL "https://github.com/9seconds/mtg/releases/download/v${MTG_VERSION}/mtg-${MTG_VERSION}-linux-amd64.tar.gz" | tar xz --strip-components=1 -C /tmp
mv /tmp/mtg /usr/local/bin/mtg
chmod +x /usr/local/bin/mtg
systemctl restart mtg
mtg --version# На exit-сервере
mtg generate-secret entry.example.ru
# Вписать новый секрет в /etc/mtg.toml
systemctl restart mtg
# Обновить ссылку tg://proxy у клиентовВсе сервисы добавлены в автозагрузку (systemctl enable).
| Сценарий | Поведение |
|---|---|
| Перезагрузка entry-сервера | autossh стартует автоматически, поднимает туннель |
| Перезагрузка exit-сервера | mtg и nginx стартуют автоматически. autossh на entry переподключится в течение 30 сек |
| Оба сервера одновременно | Каждый поднимет свои сервисы. autossh будет пытаться подключиться пока exit-сервер не станет доступен |
В комплекте идёт скрипт check_mtproto_proxy.py — глубокая проверка MTProto-прокси, которая подтверждает, что сервер действительно обслуживает FakeTLS MTProto-клиентов, а не просто принимает TCP-соединения. Именно такую проверку не получается сделать простым curl-ом: страница-заглушка может открываться, TCP-порт отвечать, но Telegram при этом не работать (например, когда прокси по какой-то причине отправляет все подключения на fallback-сайт вместо MTProto).
Скрипт выполняет настоящий FakeTLS handshake, как это делает Telegram-клиент:
- Формирует TLS 1.3 ClientHello со структурой, которую ждёт mtg (длина ≥ 517 байт, TLS record version
0x0301, SNI с доменом из секрета). - Вычисляет 32-байтный
HMAC-SHA256(секрет, ClientHello с зануленным random-полем), XOR-ит последние 4 байта с текущим unix-временем и подставляет результат в поле random. - Отправляет ClientHello на сервер и читает ответ.
- Извлекает из ответа 32-байтный серверный digest (поле
randomв ServerHello). - Вычисляет ожидаемое значение:
HMAC-SHA256(секрет, клиентский_digest + ServerHello_с_зануленным_digest). - Сравнивает. Совпадение = сервер знает секрет и корректно обслужил MTProto-клиента. Несовпадение = запрос ушёл в domain fronting (fallback), то есть прокси не узнал клиента.
За счёт HMAC-проверки скрипт отличает работающий MTProto-прокси от «мёртвого» прокси, который просто показывает сайт-заглушку любому входящему.
Только стандартная библиотека Python 3 (hmac, hashlib, socket, struct, secrets, base64, urllib, argparse).
python3 check_mtproto_proxy.py "tg://proxy?server=...&port=...&secret=..."Скрипт принимает полную tg://proxy ссылку — ту же, что раздаётся пользователям. Секрет распознаётся в любом из трёх форматов:
- hex:
ee<32 hex>...(напр.ee418622effe42a41b1ff4a28341079a6e68656c6c6f64656e69732e7275) - base64: стандартный с
+и/ - base64 url-safe: с
-и_
| Результат | stdout | exit code |
|---|---|---|
| Прокси работает (HMAC совпал) | OK |
0 |
| Прокси не узнал секрет (ушёл в fallback) | Server response digest does not match expected HMAC — proxy did NOT recognize the secret (routed to domain fronting fallback) |
1 |
| TCP/SSL не прошли | TCP connect failed to server:port: ... / TCP connect timeout |
1 |
| Сервер закрыл соединение | Server closed connection after ClientHello |
1 |
| Сломанный секрет | Invalid secret: ... |
1 |
Скрипт спроектирован для использования с self-hosted-tg-alerts-uptime-monitor — self-hosted системой мониторинга с уведомлениями в Telegram. Она умеет мониторить сайты, выполнять shell-команды, проверять SSL-сертификаты, отправлять алерты при падении и уведомления при восстановлении, а также генерировать периодические отчёты.
Пример конфигурации в config.yaml:
commands:
mtproto_proxy_1:
command: "python3 /opt/telegram-ru-proxy/check_mtproto_proxy.py 'tg://proxy?server=your-server.example.ru&port=443&secret=YOUR_SECRET'"
search_string: "OK"
timeout: 15
schedule: '*/5 * * * *'
tg_chats_to_notify:
- 123456789
notify_after_attempt: 2При падении прокси вы получите уведомление в Telegram, а при восстановлении — уведомление о восстановлении с указанием длительности даунтайма.
🇷🇺 Если мониторинг крутится в России и провайдер блокирует
api.telegram.org— настройте в монитор-тулзе опциюtelegram_proxy(SOCKS5 через SSH-туннель). См. его README, секция «Telegram Proxy».
telegram-ru-proxy/
├── README.md
├── check_mtproto_proxy.py # Скрипт мониторинга (FakeTLS handshake)
└── configs/
├── entry-server/
│ ├── ssh-tunnel-mtproto.service # systemd-юнит SSH-туннеля
│ └── renew-cert.sh # скрипт продления сертификата
└── exit-server/
├── mtg.toml # конфиг mtg
├── mtg.service # systemd-юнит mtg
├── nginx-fallback.conf # nginx vhost для сайта-заглушки
└── index.html # шаблон сайта-заглушки (рецепты)