todo/tproxy/tproxy-server-setup.md
loop-uh e37bda6197
Some checks failed
Published content check / validate (push) Failing after 6s
tproxy: подписи к изображениям, единый термин, починенная ссылка
Мелкие хвосты после перепроверки раздела:

- у всех пяти изображений раздела появился альтернативный текст — он нужен
  и для доступности, и для поиска;
- режим доставки (carrier_mode) везде называется одинаково: варианты «способ
  доставки» и «режим транспорта» из заметки про бота убраны;
- ссылка [[ZaStoGram]] в обзоре раздела MTProxy вела на заметку, которой в
  хранилище нет, и отдавала 404 на сайте: заменена на внешнюю ссылку на
  исходники клиента в Forgejo.
2026-08-22 00:44:40 +03:00

35 KiB
Raw Permalink Blame History

date tags aliases link
2026-08-22
tproxy
telegram
установка
mtproxy
nginx
caddy
техническое
Установка tproxy-server инструкция
Как поднять WEB-прокси Telegram
tproxy-server за nginx
install.sh tproxy что делает
WEB-прокси на своём домене
tproxy-server порты и службы
https://github.com/telegramdesktop/tproxy-server/blob/master/README.md

🛠 Установка tproxy-server: свой WEB-прокси на своём домене

!tproxy-setup-header.png

[!info] О чём заметка Практическое развёртывание серверной части WEB-прокси Telegram (tproxy-server) — нового типа прокси, где трафик мессенджера едет внутри обычных запросов к настоящему сайту. Что это вообще такое, объяснено в обзорной заметке tproxy/tproxy, устройство протокола — в tproxy/tproxy-protocol. Здесь только эксплуатация: что нужно до начала, что делает автоматический установщик, чем он опасен на занятом сервере и как поставить реле за уже работающий nginx.

[!danger] Главное предупреждение, которое стоит прочитать до запуска Автоматический установщик рассчитан только на чистый сервер. На хосте, где уже работает веб-сервер, он дойдёт почти до конца, упрётся в занятые порты 80 и 443 и оборвётся — оставив после себя установленные и включённые службы, свой бинарник веб-сервера Caddy в /usr/local/bin и заменённые конфигурацию и юнит этого веб-сервера. Прежние версии он при этом сохраняет рядом, дописав к имени .before-tproxy и метку времени. Если на сервере что-то есть, сразу переходите к разделу про ручную интеграцию.

TL;DR

  1. Нужны: чистый сервер x86_64 с Ubuntu 22.04+ или Debian 12+, свой домен с записью A на его адрес, открытые снаружи порты 80 и 443 и содержимое настоящего сайта.
  2. Секрет генерируется командой openssl rand -hex 16 на своей машине и вводится в установщик по запросу без отображения на экране.
  3. Один запуск deploy/install.sh ставит всё: веб-сервер, сборку официального MTProxy с зафиксированного коммита, само реле, правила файрвола, четыре службы с суточным таймером и сертификат.
  4. Снаружи открыты только 80 и 443. Порты 2398 и 8888 занимает MTProxy на всех интерфейсах, и закрывает их только правило nftables — проверять обязательно.
  5. Файл сайта читается в память один раз при старте: после изменения страниц реле нужно перезапускать.
  6. Перезапуск реле рвёт все активные сессии — клиенты переподключаются сами, но обрыв заметен.

Что нужно приготовить заранее

Сервер. Обязательно архитектура x86_64 — этого требует сборка официального MTProxy, и установщик прекращает работу до внесения изменений, если архитектура другая. Нужен systemd, доступ root или беспарольный sudo, публичный адрес IPv4.

Домен. Запись A должна указывать прямо на сервер. Запись AAAA добавляйте только если IPv6 действительно работает: наполовину настроенный IPv6 — самая частая причина, по которой не выпускается сертификат. В документации проекта прямо сказано: на первом развёртывании не стоит ставить перед сервером сеть доставки контента. Причина не только в поведении такой сети, но и в журналах: она записывала бы адреса запросов, а в адресе едет пропуск.

Имя домена обязано быть строчным и содержать точку. Для интернационализированных доменов используйте ASCII-форму xn--…: иначе один и тот же домен на разных системах может дать разные пропуски и подключение молча не состоится — подробности в tproxy/tproxy-protocol.

Секрет. Генерируется на своей машине:

openssl rand -hex 16

Получатся 32 шестнадцатеричных символа в нижнем регистре. Это тот же формат, что у обычного MTProxy. Важная деталь про маскировку FakeTLS: её секрет — это ведущий байт ee, шестнадцать байт ключа и дописанное имя домена для подмены — здесь не годится. Его отвергают и установщик, и сервер, и сам клиент помечает такие секреты для типа WEB как неподдерживаемые: трафик и так едет внутри защищённого соединения, второй слой маскировки не нужен. При этом обычный 32-символьный секрет, который случайно начинается с символов ee, совершенно законен — примерно один из 256 сгенерированных будет таким, и выбрасывать его не нужно. Годятся обычные шестнадцатибайтовые секреты и вариант с ведущим байтом dd; при ручном ведении профилей сервер принимает и запись в base64url.

Сайт. Это не формальность. Реле отдаёт страницы этого сайта всем, кто пришёл без правильного пропуска, и именно поэтому сервер выглядит обычным. Готового шаблона в проекте намеренно нет: одинаковый стартовый сайт у множества операторов сам стал бы признаком для поиска таких серверов.

Минимально нужен один файл index.html. Обычно к нему добавляют about.html, privacy.html, 404.html, файл стилей и значок сайта. Ограничения: встроенные в HTML скрипты и стили запрещены политикой безопасности, внешние ресурсы с других доменов тоже — подключайте отдельный файл стилей вместо блока прямо в разметке. Символические ссылки не обслуживаются.

Для полноценного сайта с базой данных, формами и авторизацией предусмотрен второй режим: своё веб-приложение слушает локальный адрес вроде 127.0.0.1:3000, а реле передаёт ему все обычные запросы, сохраняя исходный заголовок хоста. В этом случае приложение не должно определять у себя четыре зарезервированных адреса — /api/v1/session, /api/v1/up, /api/v1/down и /api/v1/ws.

Файрвол хостинга. Открыть 22 (по возможности только со своего адреса), 80 и 443. Порты 2398, 8080, 8081 и 8888 наружу открывать нельзя.

[!danger] Прежде чем менять правила удалённо Сначала проверьте текущее состояние файрвола и убедитесь, что правило для SSH сохранится. Ограничение доступа «только со своего адреса» при динамическом адресе провайдера или простой опечатке отрезает вас от сервера мгновенно и без предупреждения.

Установка на чистом сервере

Скопировать каталог репозитория и каталог сайта на сервер со своей машины:

rsync -az --delete --exclude .git tproxy-server/ user@server:/tmp/tproxy-server/
rsync -az my-site/ user@server:/tmp/my-site/

Затем подключиться к серверу и запустить установщик из каталога репозитория — относительные пути он считает от текущего каталога:

cd /tmp/tproxy-server && sudo ./deploy/install.sh --hostname proxy.example.com --email you@example.com --site-dir /tmp/my-site

Для режима с собственным веб-приложением вместо каталога указывается локальный адрес:

sudo ./deploy/install.sh --hostname proxy.example.com --email you@example.com --site-upstream http://127.0.0.1:3000

Секрет установщик спросит отдельно и не отобразит его при вводе — так он не попадёт ни в историю команд, ни в список процессов. Есть флаг --secret, но он кладёт значение в список процессов, и применять его стоит только в автоматизации с контролируемым окружением.

Дополнительно принимаются --mtproxy-workers (по умолчанию 1) и --mtproxy-max-connections (по умолчанию 4096). Для первого развёртывания трогать их не нужно: лишние рабочие процессы MTProxy не ускоряют ни веб-сервер, ни реле, а конкурируют с ними за процессор.

Что происходит внутри

По шагам:

  1. Ставятся пакеты, скачивается официальный бинарник Caddy версии 2.11.4 с проверкой контрольной суммы, создаются системные пользователи caddy, mtproxy и tproxy.
  2. Скачивается архив официального MTProxy с зафиксированного коммита f36d8af7, проверяется контрольная сумма, сборка идёт от непривилегированного пользователя, и только установка результата — от root.
  3. Скачиваются служебные файлы Telegram — общий секрет и таблица маршрутизации по дата-центрам.
  4. Ищется Go версии 1.20 или новее; если его нет, установщик скачивает собственную копию в /opt. Затем прогоняются все тесты проекта и собирается бинарник реле.
  5. Раскладывается сайт, создаются конфигурация и файл профилей с секретом.
  6. Ставятся четыре службы systemd, суточный таймер и правила файрвола, конфигурация проверяется вхолостую, службы запускаются в строгом порядке, и установщик ждёт готовности до двадцати секунд.

Отдельно стоит отметить обращение с секретом. Файл профилей получает права 0400 и читается не самим реле, а systemd, который подкладывает копию во временную файловую систему — процесс реле работает от пользователя без прав и оригинал прочитать не может. А вот MTProxy получает секрет аргументом командной строки, поэтому значение видно в списке процессов. Юниты ограничивают видимость чужих процессов, так что учётные записи самих служб этого не увидят, но администратор хоста и любой неограниченный вход — увидят. Это ограничение исходного MTProxy, а не проекта: не заводите на таком сервере недоверенных пользователей.

Что окажется на сервере

Порт Кто слушает Снаружи
80, 443 (TCP) Caddy открыты
443 (UDP) никто HTTP/3 отключён намеренно
2019 никто админ-интерфейс Caddy выключен
127.0.0.1:8080 реле, основной вход только локально
127.0.0.1:8081 реле, состояние и метрики только локально
2398 MTProxy, клиентский порт слушает на всех адресах, закрыт правилом nftables
8888 MTProxy, статистика то же

Службы: caddy, tproxy-server, mtproxy, tproxy-firewall и таймер refresh-mtproxy-config, который раз в сутки обновляет таблицу маршрутизации Telegram и перезапускает MTProxy только если данные действительно изменились.

Конфигурация лежит в /etc/tproxy-server/ (файл настроек и файл профилей с секретами), сайт — в /srv/tproxy-site, данные MTProxy — в /etc/mtproxy/, сертификаты — в /var/lib/caddy.

Отдельного упоминания заслуживает правило файрвола. Оно создаёт собственную таблицу tproxy_backend и отбрасывает входящий трафик на порты 2398 и 8888 со всех интерфейсов, кроме локального. Чужие правила при этом не трогаются — ни ufw, ни firewalld, ни docker. Служба привязана к штатной службе nftables, потому что её конфигурация в Debian начинается со сброса всех правил и иначе молча оставила бы MTProxy открытым наружу.

Здесь же два дефекта, которые стоит держать в голове. Первый: удаление старой таблицы и применение новой выполняются двумя отдельными командами, между которыми есть короткое окно, когда порты открыты. Второй: привязка к службе nftables пробрасывает остановку и перезапуск, но не старт — если штатная служба не включена и стартует позже вручную, она сбросит правила, и переприменения не произойдёт.

Проверка после установки

На сервере:

systemctl --no-pager --full status caddy tproxy-firewall mtproxy tproxy-server
curl --fail http://127.0.0.1:8081/healthz && curl --fail http://127.0.0.1:8081/readyz

Разница между двумя проверками существенная: первая говорит только о том, что процесс жив, вторая реально подключается к MTProxy по TCP и возвращает 503, если бэкенд лежит. Публичный сайт при этом продолжает работать — отказ бэкенда виден только на служебном локальном порту.

Со своей машины обязательно проверить, что локальные порты действительно недоступны:

nc -vz -w 3 SERVER_IP 2398 ; nc -vz -w 3 SERVER_IP 8888 ; nc -vz -w 3 SERVER_IP 8080 ; nc -vz -w 3 SERVER_IP 8081

Ответы будут разными, и это нормально: 2398 и 8888 отвалятся по таймауту, потому что их пакеты молча отбрасывает правило файрвола, а 8080 и 8081 ответят мгновенным отказом — они вообще не слушают внешний адрес. Плохо только одно: если какой-то из них соединится.

И проверить, что маскировка работает: ответ на неверный пропуск должен совпадать с обычной главной страницей — в том числе когда к параметру дописано что-то ещё.

diff <(curl -s 'https://proxy.example.com/?bridge=wrong') <(curl -s 'https://proxy.example.com/') && echo совпадает
diff <(curl -s 'https://proxy.example.com/?bridge=wrong&x=1') <(curl -s 'https://proxy.example.com/') && echo совпадает

Настоящий адрес с рабочим пропуском в тестовые команды и журналы вставлять нельзя — он равносилен паролю.

Ручная интеграция за существующий nginx

Это основной сценарий для сервера, на котором уже что-то живёт. Автоматический установщик здесь не годится: он заменяет конфигурацию Caddy и его юнит, а новый юнит указывает на собственный бинарник установщика. Если у вас была сборка Caddy с плагинами, служба переключится на сборку без них, и конфигурация с этими плагинами перестанет проходить проверку. Резервные копии прежних файлов остаются рядом с суффиксом .before-tproxy.

Порядок такой: собрать бинарник вручную, положить конфигурацию и профили, поставить службы реле и файрвола, при необходимости поднять свой MTProxy, а конфигурацию Caddy не ставить вообще.

go test ./... && go build -trimpath -o tproxy-server ./cmd/tproxy-server
sudo install -m 0755 tproxy-server /usr/local/bin/tproxy-server

Требования к фронтенду, без которых ничего не заработает

Четыре настройки nginx по умолчанию ломают схему молча, без внятной ошибки в журнале. Диагностировать такое вслепую мучительно, поэтому стоит проверить все четыре сразу.

Заголовок хоста должен доходить оригинальным. Реле сверяет его со своим настроенным доменом и на любое несовпадение отдаёт 404 — то есть вообще на всё. По умолчанию nginx подставляет туда адрес апстрима. Лечится proxy_set_header Host $host;.

Отдельно от этого: сайт должен быть доступен именно на 443. Причина не в проверке заголовка, а в клиенте — тип прокси WEB жёстко фиксирует защищённое соединение и стандартный порт, никуда больше он не пойдёт. Завернуть реле на нестандартный порт вроде 8443 не выйдет при любых настройках фронтенда.

Заголовок пересылаемого адреса должен содержать ровно один адрес. Стандартный приём $proxy_add_x_forwarded_for дописывает новый адрес к тому, что прислал клиент, и легко порождает список. Реле такой список считает ошибкой, и результат коварный: страница моста просто никогда не выдаётся, а API отвечает обычным 404. Лечится proxy_set_header X-Forwarded-For $remote_addr; и запретом пробрасывать клиентское значение.

Версия протокола к реле. По умолчанию nginx говорит с бэкендом по HTTP/1.0, что ломает и постоянные соединения, и переход на веб-сокет. Нужно proxy_http_version 1.1;.

Фронтенд обязан быть на той же машине. Реле принимает запросы только с локального адреса и на другом адресе просто не станет слушать. Схема «nginx на одном сервере, реле на другом» не заработает никогда, и отказ будет таким же молчаливым.

Ограничение размера тела запроса. По умолчанию в nginx это 1 мегабайт, а реле принимает тела до 2 МиБ. Запросы крупнее nginx отсечёт сам, и до реле они не дойдут. Нужно client_max_body_size 4m;с запасом, чтобы служебные заголовки и кодирование не упирались в границу; всё, что действительно велико, отсечёт уже само реле.

Дополнительно понадобятся: включённый HTTP/2 для клиентов (полосные режимы без него практически не работают, потому что браузер ограничен несколькими соединениями к одному адресу), проброс заголовков перехода на веб-сокет и таймауты с запасом над долгим ожиданием. В поставляемой конфигурации Caddy это 60 секунд на чтение тела и 40 секунд на ожидание заголовков ответа от реле при ожидании в 25 секунд; в nginx тем же целям служат proxy_read_timeout и proxy_send_timeout, и меньше 60 секунд их ставить нельзя.

И три запрета, которые важнее настроек:

Все пути идут в реле, без исключений. Никаких try_files, alias, root, отдельных правил для статики или значка сайта на этом виртуальном хосте. Смысл в том, что любой путь, который обслуживается иначе, отличается по заголовкам, кодированию, обработке методов и таймингу — и становится зацепкой для того, кто целенаправленно ищет такие серверы.

Не накладывать свои заголовки на ответ корня. Страница моста отдаёт собственную политику безопасности, разрешающую встраивание с локального адреса, и глобальные заголовки запрета встраивания её сломают. В nginx есть тонкость: директивы add_header из общего блока наследуются в виртуальный хост, только если в нём нет ни одной своей. Проверяйте вывод nginx -T.

Выключить журнал доступа на этом хосте. Пропуск едет в строке запроса, токен сессии в режимах веб-сокета — в заголовке подпротокола. Обычный журнал с адресами запросов здесь равносилен записи паролей открытым текстом.

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

Эксплуатация

Обновление реле делается отдельным скриптом, и порядок в нём безопасный: прогоняет тесты, собирает кандидата, проверяет его против боевой конфигурации, сохраняет старый бинарник, подменяет атомарно, перезапускает только реле и ждёт, пока оно отзовётся. Готовность бэкенда он требует лишь в том случае, если до обновления она была: обновление на лежащем MTProxy по этой причине не откатится. При неудаче откат делается сам.

sudo ./deploy/update-relay.sh

Он намеренно не трогает конфигурацию, службы, веб-сервер, MTProxy, правила файрвола и файлы сайта. А вот повторный запуск полного установщика сохранит сайт, но перезапишет конфигурацию и файл профилей, оставив там единственный профиль. Для сервера с несколькими секретами это разрушительно.

Изменение сайта требует перезапуска реле: содержимое читается в память один раз при старте, и это сделано намеренно — чтобы у статики и у транспортных ответов был ровно один путь обработки.

Перезапуск реле рвёт все активные сессии. Существующие соединения намеренно не восстанавливаются, клиенты пересоздают транспорт сами. Планируйте это как заметное для пользователей событие.

Журналы содержат только классы событий и счётчики. В самом реле логирования запросов нет вообще: даже текст ошибки при падении слушателя намеренно не печатается, только её класс.

journalctl -u tproxy-server -u mtproxy -u caddy --since '30 minutes ago'

Профилирование выключено по умолчанию и включается отдельным флагом в конфигурации — только на время диагностики и с последующим выключением.

Несколько секретов и несколько серверов

Дополнительные секреты добавляются как новые записи в файле профилей: у каждой своё имя, свой секрет, свой локальный адрес бэкенда и при желании свои квоты. По умолчанию профилей может быть до 32.

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

Масштабирование на несколько машин устроено просто, потому что иначе не выйдет: один процесс реле обслуживает ровно одно имя хоста. Каждому серверу — свой домен, своя запись DNS, свой сертификат и свои секреты. Переиспользовать один базовый секрет технически можно, поскольку пропуск включает имя домена, но тогда все развёртывания оказываются связаны одними учётными данными.

Квоты стоит согласовать с ёмкостью бэкенда: общий потолок потоков реле имеет смысл держать не выше суммарной ёмкости MTProxy. Есть и предохранитель на старте — если резерв под служебные кадры, помноженный на максимальное число сессий, не оставляет места данным, процесс просто откажется запускаться.

Диагностика частых проблем

Симптом Причина Что проверять
Сертификат не выпускается DNS не указывает на этот хост, порты 80 и 443 не доходят или наполовину настроен IPv6 записи A и AAAA; сломанную AAAA удалить, а не оставлять
Проверка готовности отдаёт 503 MTProxy недоступен состояние службы, локальное подключение к порту 2398, наличие файлов в /etc/mtproxy
В клиенте открывается сайт вместо подключения домен и секрет не совпадают с профилем — пропуск выводится из этой пары точное совпадение обоих значений
Клиент висит в состоянии подключения встроенный браузер не может загрузить домен доступность домена; на Windows нужен установленный компонент Edge WebView2, на Linux — библиотека WebKitGTK
Установка прошла, но сайт не открывается снаружи сетевые правила хостинга, DNS или невыпущенный сертификат правила группы безопасности у провайдера, записи DNS, журнал Caddy
Служба MTProxy падает по кругу порт 8888 или 2398 уже заняты другим MTProxy до установки проверить занятость этих портов через ss -lntp

📚 См. также

  • tproxy/tproxy — что это такое и стоит ли вообще браться сейчас.
  • tproxy/tproxy-protocol — откуда берутся 25 секунд ожидания, лимиты и требования к фронтенду.
  • tproxy/tproxy-in-bot — почему добавление секрета требует перезапуска и что это значит для автоматической выдачи.
  • mtproxy/mtproto-zig-setup — развёртывание обычного MTProxy, который здесь работает бэкендом.
  • 🔗 README проекта — первоисточник инструкции.

[!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: исходник этой заметки · скачать весь репозиторий одним zip-архивом.