Тестируем работу WEB-proxy для Telegram


В августе появилась информация о новом варианте проксирования в Telegram. Новость заинтересовала меня, но свободного времени, чтобы разобраться, как оно устроено, тогда не было. Наконец, время нашлось, и я готов поделиться тем, что удалось выяснить.
На Хабре вышла статья, в которой подробно описана теория устройства нового прокси. Я знаком с Web и о Proxy слышал, но принципиальное отличие новой реализации оставалось неясным. Оказалось, что это всё тот же MTProxy, но «завёрнутый» в HTTPS/WebSocket‑соединение. Telegram-клиент открывает страницу в WebView, и со стороны это выглядит как обычное взаимодействие «клиент‑сервер» через HTTP+WSS. В комментариях к статье (спасибо @Mustard!) была приведена ссылка на реализацию прототипа такого WEB-proxy. Из описания понятно, что реализация строится на наборе сервисов, написанных на Go, поверх Caddy, который громко заявил о себе.
Признаюсь, лично мне пока не довелось воспользоваться этим чудо-web-сервером. В моём pet-проекте работает nginx, и он меня вполне устраивает. Тысячами хосты создавать не требуется, поэтому переход на Caddy пока не рассматривается. Инсталляция tproxy-server предлагает установить Caddy с нуля, при этом требует наличия действующей HTTPS-страницы, что выглядит немного противоречиво. На моём сервере уже достаточно сильно разросшаяся конфигурация nginx c множеством используемых портов — своего рода мини-зоопарк, только Caddy там и не хватало. Поэтому захотелось получить tproxy-server, но «с перламутровыми пуговицами». Вот тут и начинается приключение.
Решение пришло само собой. Вникать в тонкости реализации tproxy-server не захотелось, поэтому за небольшое количество токенов одного известного LLM-сервиса и 15 минут времени я получил скрипт, который не поднимает Caddy, а просто отдаёт блок с Location. Этот блок можно добавить в существующую конфигурацию nginx, сохранив полный контроль — именно то, что было нужно. Если у вас похожая ситуация, оставляю ссылку на скрипт. Команда для развёртывания выглядит так:
sudo ./deploy/install‑nginx.sh ‑hostname your-host.ru ‑base‑path base_path ‑secret dd1234567890aсdf1234567890aсdf12 ‑site‑upstream http://127.0.0.1:3001После запуска скрипт скачал зависимости и настроил сервисы, но завершился с ошибкой:
tproxy-server did not become readyНачал разбираться. Сервис действительно запустился, однако для успешного завершения должна пройти ready-проба http://127.0.0.1:8081/readyz. Выяснилось, что у меня этот порт уже был занят: порты 8080 и 8081 используются для работы прокси, и на моём хосте они тоже были заняты. В итоге я сделал глобальную замену портов для директории tproxy-server:
grep -rl '8080' . | xargs sed -i 's/8080/8083/g'
grep -rl '8081' . | xargs sed -i 's/8081/8082/g'После этого повторный запуск прошёл успешно.
Скрипт поднимает апстрим tproxy_relay, доступный извне по адресу: https://<hostname>/<base_path>. При этом необходимо указать действующий апстрим вашего сайта, куда по умолчанию будет направляться трафик.
В случае успешного развёртывания вы увидите примерно следующий результат:
=== inside the existing server {} for your-host.ru ===
TLS certificates, HSTS and security headers stay as they are; add only the
lines this block lacks.
gzip on;
# Carrier uplink batches reach 2 MiB; nginx defaults to 1 MiB and would break
# the transport with 413.
client_max_body_size 4m;
location ^~ /base_path/ {
# No URI part after the upstream name: the original path, including the
# prefix, is forwarded unchanged. Do NOT write `.../;` here.
proxy_pass http://tproxy_relay;
proxy_http_version 1.1;
proxy_set_header Host $host;
# Exactly one address. $proxy_add_x_forwarded_for appends and can produce a
# list, which the relay rejects.
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
# Parked long polls must not be cut or held back (relay long_poll is 25s).
proxy_read_timeout 90s;
proxy_send_timeout 90s;
proxy_buffering off;
proxy_request_buffering off;
access_log off; # never log carrier URIs or Authorization
}
Apply: sudo nginx -t && sudo systemctl reload nginx
If nginx serves the site from static files rather than an upstream, give the
relay a loopback mirror of the same root and keep --site-upstream pointed at
it (BASE_PATH.md section 4); in-prefix 404s must match the site's own:
server {
listen 127.0.0.1:3000;
server_name _;
root /srv/www/site;
index index.html;
error_page 404 /404.html;
}
После добавления в приложение Telegram, WEB-proxy успешно подключается. Сервер отдаёт страницу по base_path, при этом для страницы также доступен API, используемый Telegram-клиентом. Красивое решение, не правда ли?
Отмечу, что в официальной документации не рекомендуют использовать дополнительные прокси перед развёрнутым WEB-proxy (!). Хотя теоретически это возможно, и в некоторых случаях применяется.
Суть в том, как происходит процесс рукопожатия на стороне tproxy_relay. Сервис проверяет два момента:
HTTP-заголовок Host в запросе
Так называемый Capability
Вот как это описывает LLM:
Как реально сопоставляется capability
1. Capability считает клиент — не сервер. Он берёт hostname, который вы вписали в поле server, и локально выводит
HMAC-SHA256(secret, "tdesktop-web-proxy-bridge-v2\n" + host + "\n" + base) (internal/config/config.go:444-452). Готовое 43-символьное значение уходит в запрос как ?bridge=….
2. Релей пересчитывает ожидаемый HMAC из своей конфигурации — DeriveCapability(host = PublicHostname, basePath, secret) в config.go:574, а не из r.Host.
3. Сравнение идёт с значением в query: bridgeProfile достаёт bridge= и зовёт MatchCapability, который побайтово сравнивает с Profiles[i].Capability (internal/server/secrets.go:11-22, internal/session/manager.go:143-160).То есть приложение-клиент формирует хеш и передаёт его в параметре запроса, где имя хоста берётся из https://t.me/webproxy?server=your-host.ru/base_path&secret=. Если попытаться предоставить доступ к прокси через https://t.me/webproxy?server=your-proxy.ru/base_path&secret=, то проверку по хосту мы не пройдём, так как клиент будет рассчитывать хеш, исходя из имени хоста your-proxy.ru.
Чтобы такая конфигурация работала, при настройке WEB-proxy нужно указать в качестве hostname your-proxy.ru:
sudo ./deploy/install‑nginx.sh ‑hostname your-proxy.ru ‑base‑path base_path ‑secret dd1234567890aсdf1234567890aсdf12 ‑site‑upstream http://127.0.0.1:3001Затем следует запретить перезапись заголовка Host в конфигурации веб-сервера your-proxy.ru для Location base_path. Например, для nginx необходимо указать:
location ^~ /base_path/ {
...
proxy_set_header Host your-proxy.ru;
}То есть сохранить в заголовке исходный хост. При такой настройке подключение будет возможно и через прокси.

Если вам тоже было интересно узнать больше о внутреннем устройстве WEB-proxy или есть чем поделиться по этой теме, приглашаю в комментарии!
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.