Развернул Telegram‑бота на РФ‑хостинге — бот молчит. ping api.telegram.org
проходит, а TCP:443 — timeout: фильтруются именно подсети Telegram на исходящем.
Webhook не спасает — проблема двусторонняя. Решение: маленький релей за рубежом
(Caddy проксирует api.telegram.org) + long‑polling. ПДн при этом остаются в РФ.


Симптом

Бот не отвечает, HTTP‑сервер жив, в логах — таймауты на запросах к api.telegram.org.Локально (с зарубежного дева) всё работало.

Диагностика

bash

ping api.telegram.org # отвечает
curl -v https://api.telegram.org # зависает на TCP-handshake → timeout
curl -v https://github.com # ок
curl -v https://acme-v02.api.letsencrypt.org # ок (Let's Encrypt живой)

Не DNS и не общий фаервол: GitHub и LE доступны, а диапазоны Telegram — нет. Фильтрация на исходящем по подсетям Telegram.

Почему webhook не вариант

Webhook — Telegram сам POST'ит апдейты тебе. Но: (1) до РФ-сервера он на входящий не достучится, (2) да и setWebhook/ответы мы не отправим — исходящий к api.telegram.org закрыт. Тупик с обеих сторон.

Решение: релей за рубежом + long-polling

Маленькая VPS за пределами РФ, Caddy проксирует Telegram API:

caddy


relay.example.com{
reverse_proxy https://api.telegram.org {
header_up Host api.telegram.org}}

Бот ходит через релей. В grammY это client.apiRoot:

ts


const bot = new Bot(token, {
client: { apiRoot: "https://relay.example.com" },
});

Работаем в long‑polling: бот сам забирает апдейты исходящим getUpdates через релей — входящий трафик к РФ‑серверу не нужен вообще.

Грабля 1: не оставляй релей открытым

reverse_proxy на api.telegram.org = любой, кто знает URL, прогонит через тебя любой токен. Закрой по IP — только свой бэкенд:

caddy


@allowed remote_ip 203.0.113.10 # IP РФ-сервера
handle @allowed { reverse_proxy https://api.telegram.org { header_up Host api.telegram.org } }
respond 403

Грабля 2: где остаются ПДн (152-ФЗ)

Через релей идёт только управляющий канал (апдейты/ответы). База, тексты, юзеры — на РФ-сервере; на релее данные не оседают. Для оператора ПДн: хранение остаётся в РФ.

Грабля 3: long-polling не должен вешать старт

Соблазн при буте — await bot.start() / await bot.api.setMyCommands(...) на верхнем уровне. Если релей недоступен, await висит и не поднимается HTTP-сервер (а с ним REST API мини-аппа, которому Telegram не нужен). Вынеси Telegram-инициализацию в фон с ретраем: listener встаёт сразу, бот подключается, как появится связь.

Бонус: как тот же бот держит LLM дёшево

Раз речь о боте — он категоризирует траты из текста («такси 380» → Транспорт). Чтобы не гнать каждое сообщение в нейросеть: сначала локальный матчер по алиасам юзера, LLM (YandexGPT) только на промахе, и бот дозаписывает алиасы из ручных правок. Cache‑hit ~60% → ~6 центов на юзера в месяц. Зайдёт — распишу отдельным постом.

Стек

Bun, grammY, Caddy, Postgres, Selectel (РФ) + релей‑VPS за рубежом.


Это из соло‑проекта — Telegram‑бота для учёта трат. Статья про релей, но если хочется потыкать артефакт: сам бот. Предложения, вопросы — в комменты.