
В августе появилась информация о новом варианте проксирования в 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. Выяснилось, что у меня этот порт уже был занят: порты 8 080 и 8 081 используются для работы прокси, и на моём хосте они тоже были заняты. В итоге я сделал глобальную замену портов для директории 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>. При этом необходимо указать действующий апстрим вашего сайта, куда по умолчанию будет направляться трафик.
В случае успешного развёртывания вы увидите примерно следующий результат:
nginx still owns 80/443; only the prefix below is routed to the relay. === http context (e.g. /etc/nginx/conf.d/tproxy-relay.conf) === If your http{} already defines $connection_upgrade, drop the map and reuse the existing name. map $http_upgrade $connection_upgrade { default upgrade; '' ""; } upstream tproxy_relay { server 127.0.0.1:8083; keepalive 32; } === 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 или есть чем поделиться по этой теме, приглашаю в комментарии!
Спасибо, что дочитали до конца!
0ka
Проверьте плодит ли клиент соединения к серверу как раньше или нет. Т.е. есть ли http2 или как в мтпрото: множество соединений к серверу (большинство сайтов себя так не ведут), ркн заметили это и поставили ограничение тлс сессий (сибирскую блокировку) на многие хостинг провайдеры чтобы заблочить мтпрото
https://habr.com/ru/articles/1044396/
gmixo Автор
http2 поддерживается, его можно включить в nginx, с точки зрения настроек это обычный веб сервер
0ka
поддерживается нгинксом? я про клиент, и не просто про alpn h2, а настоящее количество открытых соединений
gmixo Автор
да, я писал про конфиг nginx,
количество соединений с клиента я не замерял, используется совместно https и websocket, соответственно под каждый ws будет отдельное соединение, сколько их реально используется не отслеживал
Nemoumbra
Ну так проверьте!
0ka
проверил у себя:
carrier https открывает 2 соединения.
carrier https-lanes открывает 6 соединений, дальше тг просто зависает и бесконечно пытается переподключиться.
carrier websocket открывает 1 соединение.
carrier websocket-lanes открывает по соединению для каждой картинки\видео, за 30 сек листаний фото\видео\файлов на канале открылось 16 соединений.
как по мне единственный нормальный carrier это websocket.
у меня на компе для подключений к прокси используется edge webview из винды, но при фейле подключения предлагает подключиться через браузер
кстати в статье в конфиге nginx есть строчка
proxy_set_header Connection $connection_upgrade;у меня на неё нгинкс ругается, заменил на
proxy_set_header Connection "upgrade";gmixo Автор
Спасибо за замечание, дополнил в статье вывод скрипта, там было предложено использовать маппинг
Dreams_and_magic
У меня всё 4 конфигурации отлично работают на виндовом клиенте и на андроиде, а на линуксе https-lanes падает.