🤖 Выдача WEB-прокси Telegram из своего бота

Обложка: выдача WEB-прокси из Telegram-бота — две строки на пользователя

О чём заметка

WEB-прокси — новый, четвёртый тип прокси в Telegram, где трафик едет внутри обычных HTTPS-запросов к вашему сайту. Что это такое и зачем нужно — в обзорной заметке WEB-прокси Telegram: трафик внутри обычного сайта; как поднять сервер — в Установка tproxy-server. Здесь разбирается только один вопрос: как раздавать такой доступ пользователям из любого Telegram-бота, что для этого нужно хранить и почему автоматическая выдача упирается в неожиданное ограничение.

Насколько это проверено

Разбор сделан по исходному коду серверной части tproxy-server (репозиторий telegramdesktop/tproxy-server, состояние на 22 августа 2026) и по коду Telegram Desktop. Автор прямо помечает проект как доказательство работоспособности (proof-of-concept) — экспериментальную реализацию, а не готовый продукт. Поля конфигурации и формат ссылки могут поменяться без сохранения совместимости. Практических историй массовой выдачи WEB-прокси через ботов на 22 августа 2026 года нет: клиентская поддержка ещё не дошла до стабильных сборок Telegram (подробности в обзорной заметке).

TL;DR

  1. Пользователю нужно передать всего два значения — имя хоста и 32-символьный шестнадцатеричный секрет. Это на порядок проще, чем выдача VLESS или WireGuard, где генерируются ключи и целые конфигурационные файлы.
  2. Ссылка для передачи выглядит как https://t.me/webproxy?server=proxy.example.com&secret=000102…, есть и вариант tg://webproxy с теми же параметрами.
  3. Секреты живут в файле profiles.json на сервере. Их количество ограничено параметром max_profiles — по умолчанию 32.
  4. Главная проблема автоматизации: сервер не умеет перечитывать конфигурацию на лету. Добавить профиль можно только перезапуском процесса, а перезапуск рвёт активные сессии всех остальных пользователей.
  5. Отзыв доступа у одного человека возможен только сменой секрета его профиля. Поэтому «один профиль на пользователя» упирается в потолок из пункта 3, а «один общий секрет на всех» лишает возможности отключить конкретного человека.
  6. Практичный вывод на август 2026: бот может раздавать заранее нарезанный пул профилей, но не выдавать доступ по-настоящему динамически.

Что вообще нужно выдать пользователю

Сначала — чем WEB-прокси отличается от привычных схем выдачи. Когда бот выдаёт доступ к VLESS, он создаёт на сервере отдельную запись пользователя со своим идентификатором UUID — длинным уникальным номером, который сервер сверяет при каждом подключении. Когда бот выдаёт WireGuard, он генерирует пару ключей и собирает целый конфигурационный файл с адресами и маршрутами. В обоих случаях на сервере появляется физическая сущность, привязанная к конкретному человеку.

С WEB-прокси всё устроено иначе, и это устройство унаследовано от MTProxy. Пользователю передаются ровно два поля:

Hostname: proxy.example.com
Secret:   000102030405060708090a0b0c0d0e0f

Имя хоста пишется без https://, без порта, без слеша и без параметров: тип прокси WEB жёстко подразумевает HTTPS и порт 443. Секрет — те же 32 шестнадцатеричных символа (16 байт), что и у обычного MTProxy; общепринята запись в нижнем регистре, её же требует установщик сервера. Никаких пользовательских ключей, сертификатов и профилей на стороне клиента не создаётся.

Проще говоря: доступ к WEB-прокси — это не персональная учётная запись, а знание пароля. Кто знает пару «домен + секрет», тот подключается; сервер не различает, один это человек или тысяча.

Из этой пары клиент сам, локально, вычисляет так называемый bridge-capability — производный «пропуск», который потом уходит в сетевой запрос вместо самого секрета. Считается он как HMAC-SHA256, где ключом служат байты секрета, а сообщением — строка tdesktop-web-proxy-bridge-v1\n вместе с именем хоста. HMAC — это способ получить из ключа и сообщения короткий отпечаток, который невозможно подделать, не зная ключа.

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

Формат ссылки и как её отдавать в чате

Готовая ссылка, которую бот отправляет пользователю сообщением:

https://t.me/webproxy?server=proxy.example.com&secret=000102030405060708090a0b0c0d0e0f

Клиенты также принимают эквивалентную запись через внутреннюю схему приложения: tg://webproxy?server=…&secret=…. Это тот же самый набор параметров, просто ссылка открывается напрямую установленным клиентом, минуя веб-страницу.

Публичный t.me пока не знает этого маршрута

В документации проекта прямо сказано: публичный фронтенд t.me маршрут webproxy пока не регистрирует. То есть ссылка вида https://t.me/webproxy?... на 22 августа 2026 года не обязательно откроется как прокси — при тестировании её нужно скармливать клиенту напрямую. Для бота это значит, что нельзя рассчитывать на привычный сценарий «пользователь нажал на ссылку, всё настроилось само»: пока разумнее отдавать и ссылку, и два поля отдельным текстом, чтобы человек мог ввести их руками.

Отдельная тонкость касается доменов с национальными буквами. Если ваш домен интернационализированный (например, кириллический), пользователю нужно публиковать его в форме xn--… — это ASCII-запись такого имени, называемая A-label. Причина техническая: разные версии библиотеки, на которой построен клиент, по-разному приводят некоторые символы к каноническому виду, из-за чего один и тот же домен на Windows и на Linux даёт разные пропуски и подключение молча не состоится — подробности в разборе протокола. Бот, который отдаёт домен в ASCII-форме, эту проблему обходит целиком.

Где на сервере живут секреты

Все выданные секреты перечислены в одном файле; путь к нему задаётся обязательным полем настроек. В эталонной установке оператор правит /etc/tproxy-server/profiles.json, а служба читает его копию, которую systemd подкладывает во временный каталог при каждом старте. Структура файла проста:

{
  "profiles": [
    {
      "name": "alpha",
      "secret": "0123456789abcdef0123456789abcdef",
      "backend": "127.0.0.1:2398",
      "carrier_mode": "https"
    },
    {
      "name": "beta",
      "secret": "fedcba9876543210fedcba9876543210",
      "backend": "127.0.0.1:2399",
      "carrier_mode": "websocket",
      "limits": {
        "max_sessions": 32,
        "max_streams": 512,
        "max_streams_per_session": 32
      }
    }
  ]
}

Каждая запись здесь называется профилем — это именованный набор «секрет плюс правила», под которым подключается группа пользователей. От этих полей зависит вся модель выдачи.

name — идентификатор от 1 до 64 символов, уникальный внутри файла. Он не виден пользователю и нужен серверу для учёта: по имени профиля считаются его сессии, подключения к бэкенду и ограничения скорости создания новых соединений.

secret — тот самый секрет, который получает пользователь. Принимается в шестнадцатеричном виде (32 или 34 символа) либо в кодировке base64url. Раскодироваться он обязан ровно в 16 или 17 байт, причём 17-байтовый вариант должен начинаться с байта dd — это унаследованный от MTProxy признак режима случайного дополнения. Два профиля не могут иметь секреты, дающие одинаковый пропуск: такой файл сервер не примет.

backend — адрес, куда реле отправляет извлечённый из кадров трафик этого профиля, то есть адрес работающего рядом MTProxy. Обязательно локальный числовой адрес вида 127.0.0.1:2398; имена вроде localhost и внешние адреса отвергаются. Это осознанное ограничение архитектуры: реле физически не может отправить трафик наружу, куда попросит клиент, поэтому даже взломанное реле не превращается в открытый прокси для всего интернета.

carrier_mode — режим доставки, один из четырёх (https, https-lanes, websocket, websocket-lanes); по умолчанию https. Подробнее эти режимы разобраны в заметке про устройство протокола. Здесь важно другое: режим выбирается на сервере, через профиль, и клиент про него ничего не знает. Значит, бот может выдавать разным группам пользователей разные секреты и тем самым переключать им режим доставки — например, отдавать websocket тем, у кого сеть его пропускает, и консервативный https остальным. Со стороны пользователя это по-прежнему просто «другой секрет».

limits — необязательные квоты профиля: сколько одновременных сессий и потоков ему разрешено, с какой скоростью он может создавать новые, сколько данных копить. Пропущенное значение наследуется из общих настроек сервера (а два поля — предел потоков на сессию и число одновременных подключений к бэкенду — берут меньшее из общей настройки и собственного предела потоков профиля). Заданное значение может только понизить общий потолок, но не поднять; ноль означает «наследовать», поэтому обнулить лимит в профиле нельзя. Для бота это единственный встроенный механизм справедливого дележа: если один секрет раздали слишком широко, его квота не даст ему съесть весь сервер.

У файла не должно остаться ни одного бита прав для группы и для остальных — на практике это 0400, но и 0600 проверку проходит. Сервер это проверяет и отказывается стартовать, если файл доступен группе или всем остальным. Исключение сделано для механизма systemd, который подкладывает секреты сервису во временный каталог; тогда допускаются права только на чтение.

Главное ограничение: перезапуск ради каждого нового пользователя

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

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

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

Отсюда следуют три работающие стратегии, и выбирать приходится осознанно.

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

Пул заранее нарезанных профилей. Оператор один раз создаёт N профилей со случайными секретами (по умолчанию их не может быть больше 32 — это параметр max_profiles), прописывает те же секреты бэкенду MTProxy, запускает сервер и дальше просто раздаёт неиспользованные секреты из этого пула. Бот хранит соответствие «профиль → пользователь» в своей базе, а сервер о пользователях ничего не знает. Перезапуски не нужны вовсе, пока пул не исчерпан. Плата: потолок числа выданных секретов на один домен — по умолчанию 32, поднять его можно, но это правка файла настроек и, значит, тот же перезапуск, — и необходимость планировать пул заранее.

Пакетное применение. Бот копит заявки и применяет их окном — например, раз в сутки ночью, одним перезапуском. Пользователь получает доступ не мгновенно, а к утру. Для платного сервиса это обычно неприемлемо, для закрытого круга — вполне.

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

Отзыв доступа: то, чего в этой модели нет

Отзыв — обратная сторона «доступ это просто знание пароля». Раз сервер не различает пользователей внутри профиля, отобрать доступ у одного человека, не тронув остальных, невозможно. Единственный способ сделать секрет недействительным — изменить его в profiles.json и перезапустить реле. После этого доступ теряют все, кому этот секрет был выдан.

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

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

Честный вывод про биллинг

На август 2026 WEB-прокси не годится для сервиса с оплатой по подписке и автоматическим отключением после окончания периода. Механизма, который делает это дёшево, в архитектуре нет: ни персональных учётных записей, ни горячего применения изменений, ни отзыва без ущерба соседям. Он хорошо подходит для другого — раздать рабочий доступ ограниченному кругу людей там, где остальные способы не проходят по сети.

Что бот должен хранить у себя

Раз сервер не ведёт учёт пользователей, всё состояние ложится на бота. Минимальный набор полей на одну выдачу:

  • имя хоста, с которого выдан доступ (оно же часть пропуска, поэтому обязательно сохранять вместе с секретом, а не подставлять «текущий домен» при показе);
  • имя профиля на сервере — чтобы знать, какую запись править при отзыве;
  • сам секрет в открытом виде: из работающего процесса реле его уже не достать (после старта в памяти остаётся только производный пропуск), но на сервере он продолжает лежать открытым текстом в файле профилей и в переменных окружения MTProxy. Хранить его у бота нужно не потому, что он безвозвратно теряется, а чтобы не ходить за ним на сервер и не зависеть от его переустановки;
  • идентификатор пользователя Telegram и дата выдачи;
  • режим доставки, если разным группам выдаются разные (иначе потом не понять, почему у части людей другое поведение).

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

Сколько ботов и сколько доменов

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

Поэтому масштабирование выглядит так: каждому новому серверу — свой домен, своя DNS-запись, свой сертификат и свой набор профилей. Бот, раздающий доступ на несколько серверов, обязан хранить домен вместе с каждой выдачей и не пытаться «переназначить» пользователя на другой сервер простой заменой адреса — секрет там другой, и пропуск не совпадёт.

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

Пошаговый план внедрения

  • Поднять сервер по инструкции из заметки про установку и убедиться, что обычный сайт на домене открывается, а служебные порты снаружи закрыты.
  • Решить модель выдачи: пул профилей, пакетное применение или один общий секрет. От этого зависит вся остальная логика бота.
  • Если выбран пул — сгенерировать секреты заранее (openssl rand -hex 16 на каждый), вписать их в profiles.json одним разом и запустить сервер; после этого перезапуски не понадобятся.
  • Завести в базе бота таблицу выдач с полями из раздела выше, обязательно включая домен и имя профиля.
  • Научить бота отдавать пользователю и готовую ссылку, и два поля отдельным текстом — на случай, если ссылка t.me/webproxy в его клиенте не сработает.
  • Заранее написать процедуру отзыва (смена секрета плюс перезапуск) и честно посчитать, скольких пользователей она затронет.
  • Проверить, что в журналах бота и сервера не оседают ни секреты, ни ссылки с пропуском: адрес bridge-запроса содержит производный пропуск, и его попадание в логи равносильно утечке доступа.

📚 См. также


🤖 Эти статьи открыты — можно обучать на них ИИ

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