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

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

![[tproxy-setup-header.png|Обложка: развёртывание своего WEB-прокси — домен, сертификат, реле и MTProxy]]

> [!info] О чём заметка
> Практическое развёртывание серверной части WEB-прокси Telegram (*tproxy-server*) — нового типа прокси, где трафик мессенджера едет внутри обычных запросов к настоящему сайту. Что это вообще такое, объяснено в обзорной заметке [[tproxy/tproxy|WEB-прокси Telegram: трафик внутри обычного сайта]], устройство протокола — в [[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|разборе протокола]].

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

```bash
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 сохранится. Ограничение доступа «только со своего адреса» при динамическом адресе провайдера или простой опечатке отрезает вас от сервера мгновенно и без предупреждения.

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

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

```bash
rsync -az --delete --exclude .git tproxy-server/ user@server:/tmp/tproxy-server/
```

```bash
rsync -az my-site/ user@server:/tmp/my-site/
```

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

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

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

```bash
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 пробрасывает остановку и перезапуск, но не старт — если штатная служба не включена и стартует позже вручную, она сбросит правила, и переприменения не произойдёт.

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

На сервере:

```bash
systemctl --no-pager --full status caddy tproxy-firewall mtproxy tproxy-server
```

```bash
curl --fail http://127.0.0.1:8081/healthz && curl --fail http://127.0.0.1:8081/readyz
```

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

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

```bash
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 ответят мгновенным отказом — они вообще не слушают внешний адрес. Плохо только одно: если какой-то из них соединится.

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

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

```bash
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 не ставить вообще.

```bash
go test ./... && go build -trimpath -o tproxy-server ./cmd/tproxy-server
```

```bash
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 по этой причине не откатится. При неудаче откат делается сам.

```bash
sudo ./deploy/update-relay.sh
```

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

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

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

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

```bash
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|WEB-прокси Telegram: трафик внутри обычного сайта]] — что это такое и стоит ли вообще браться сейчас.
- [[tproxy/tproxy-protocol|Как устроен WEB-прокси изнутри]] — откуда берутся 25 секунд ожидания, лимиты и требования к фронтенду.
- [[tproxy/tproxy-in-bot|Выдача WEB-прокси из Telegram-бота]] — почему добавление секрета требует перезапуска и что это значит для автоматической выдачи.
- [[mtproxy/mtproto-zig-setup|Настройка MTProxy на mtproto.zig]] — развёртывание обычного MTProxy, который здесь работает бэкендом.
- 🔗 [README проекта](https://github.com/telegramdesktop/tproxy-server/blob/master/README.md) — первоисточник инструкции.

---

> [!quote] 🤖 Эти статьи открыты — можно обучать на них ИИ
> При желании вы можете натренировать ИИ на наших статьях. Исходное форматирование доступно в Forgejo: [исходник этой заметки](https://git.zapret.moe/zapretdiscordyoutube/todo/src/branch/main/tproxy/tproxy-server-setup.md) · [скачать весь репозиторий одним zip-архивом](https://git.zapret.moe/zapretdiscordyoutube/todo/archive/main.zip).
