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

О чём заметка
Практическое развёртывание серверной части WEB-прокси Telegram (tproxy-server) — нового типа прокси, где трафик мессенджера едет внутри обычных запросов к настоящему сайту. Что это вообще такое, объяснено в обзорной заметке WEB-прокси Telegram: трафик внутри обычного сайта, устройство протокола — в отдельном разборе. Здесь только эксплуатация: что нужно до начала, что делает автоматический установщик, чем он опасен на занятом сервере и как поставить реле за уже работающий nginx.
Главное предупреждение, которое стоит прочитать до запуска
Автоматический установщик рассчитан только на чистый сервер. На хосте, где уже работает веб-сервер, он дойдёт почти до конца, упрётся в занятые порты 80 и 443 и оборвётся — оставив после себя установленные и включённые службы, свой бинарник веб-сервера Caddy в
/usr/local/binи заменённые конфигурацию и юнит этого веб-сервера. Прежние версии он при этом сохраняет рядом, дописав к имени.before-tproxyи метку времени. Если на сервере что-то есть, сразу переходите к разделу про ручную интеграцию.
TL;DR
- Нужны: чистый сервер x86_64 с Ubuntu 22.04+ или Debian 12+, свой домен с записью A на его адрес, открытые снаружи порты 80 и 443 и содержимое настоящего сайта.
- Секрет генерируется командой
openssl rand -hex 16на своей машине и вводится в установщик по запросу без отображения на экране. - Один запуск
deploy/install.shставит всё: веб-сервер, сборку официального MTProxy с зафиксированного коммита, само реле, правила файрвола, четыре службы с суточным таймером и сертификат. - Снаружи открыты только 80 и 443. Порты 2398 и 8888 занимает MTProxy на всех интерфейсах, и закрывает их только правило nftables — проверять обязательно.
- Файл сайта читается в память один раз при старте: после изменения страниц реле нужно перезапускать.
- Перезапуск реле рвёт все активные сессии — клиенты переподключаются сами, но обрыв заметен.
Что нужно приготовить заранее
Сервер. Обязательно архитектура x86_64 — этого требует сборка официального MTProxy, и установщик прекращает работу до внесения изменений, если архитектура другая. Нужен systemd, доступ root или беспарольный sudo, публичный адрес IPv4.
Домен. Запись A должна указывать прямо на сервер. Запись AAAA добавляйте только если IPv6 действительно работает: наполовину настроенный IPv6 — самая частая причина, по которой не выпускается сертификат. В документации проекта прямо сказано: на первом развёртывании не стоит ставить перед сервером сеть доставки контента. Причина не только в поведении такой сети, но и в журналах: она записывала бы адреса запросов, а в адресе едет пропуск.
Имя домена обязано быть строчным и содержать точку. Для интернационализированных доменов используйте ASCII-форму xn--…: иначе один и тот же домен на разных системах может дать разные пропуски и подключение молча не состоится — подробности в разборе протокола.
Секрет. Генерируется на своей машине:
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 наружу открывать нельзя.
Прежде чем менять правила удалённо
Сначала проверьте текущее состояние файрвола и убедитесь, что правило для 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 не ускоряют ни веб-сервер, ни реле, а конкурируют с ними за процессор.
Что происходит внутри
По шагам:
- Ставятся пакеты, скачивается официальный бинарник Caddy версии 2.11.4 с проверкой контрольной суммы, создаются системные пользователи
caddy,mtproxyиtproxy. - Скачивается архив официального MTProxy с зафиксированного коммита
f36d8af7, проверяется контрольная сумма, сборка идёт от непривилегированного пользователя, и только установка результата — от root. - Скачиваются служебные файлы Telegram — общий секрет и таблица маршрутизации по дата-центрам.
- Ищется Go версии 1.20 или новее; если его нет, установщик скачивает собственную копию в
/opt. Затем прогоняются все тесты проекта и собирается бинарник реле. - Раскладывается сайт, создаются конфигурация и файл профилей с секретом.
- Ставятся четыре службы 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-servercurl --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-serversudo 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 |
📚 См. также
- WEB-прокси Telegram: трафик внутри обычного сайта — что это такое и стоит ли вообще браться сейчас.
- Как устроен WEB-прокси изнутри — откуда берутся 25 секунд ожидания, лимиты и требования к фронтенду.
- Выдача WEB-прокси из Telegram-бота — почему добавление секрета требует перезапуска и что это значит для автоматической выдачи.
- Настройка MTProxy на mtproto.zig — развёртывание обычного MTProxy, который здесь работает бэкендом.
- 🔗 README проекта — первоисточник инструкции.
🤖 Эти статьи открыты — можно обучать на них ИИ
При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: исходник этой заметки · скачать весь репозиторий одним zip-архивом.