From 05e82989a8f96efeb1e24a21c1a7ff0d622cd439 Mon Sep 17 00:00:00 2001 From: bot-backend Date: Sun, 23 Aug 2026 23:32:35 +0300 Subject: [PATCH] =?UTF-8?q?fix(ops):=20=D0=B7=D0=B0=D0=BA=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D0=B8=D1=82=D1=8C=20=D1=80=D0=B0=D0=B1=D0=BE=D1=87=D0=B8?= =?UTF-8?q?=D0=B9=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=20api.telegram.org=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=BE=D0=BC=20=D1=85=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Замер 2026-08-23 с Poincare: у api.telegram.org семь публикуемых адресов, отвечает РОВНО ОДИН — 149.154.167.220 (302 за 0.11 с). Резолвер при этом отдаёт 149.154.166.110, который мёртв. Скан всего 149.154.167.0/24 подтвердил: живой он один во всём блоке. Это не блокировка Selectel и не наш файрвол: с Beget отвечают три адреса из тех же семи, включая тот, что отдаёт DNS там. Telegram частично фильтруется у ОБОИХ провайдеров, просто Beget попадает на живой адрес. Через прокси ASocks Telegram не проходит ни с одного из четырёх узлов (все 000) — узлы российские, путь закрыт. Без закрепления tradein-tgbot на long-polling резолвит мёртвый адрес, виснет и умирает МОЛЧА: restart поднимает его заново, он снова виснет, ни краша, ни строки в логе. Закреплены все три потребителя Telegram, а не только бот: - tgbot и backend — extra_hosts в новом docker-compose.selectel.yml. backend тоже ходит в Telegram: пересылка алертов GlitchTip (api/v1/glitchtip.py) и support-чат (api/v1/support.py). - хостовые скрипты ops/lib-backup.sh и ops/uptime-healthcheck.sh — шаг 11 в selectel-bootstrap.sh кладёт запись в /etc/hosts. Им compose не помогает, они идут из cron, не из контейнера. Отдельный override-файл, а не правка docker-compose.prod.yml: на Beget закрепление не нужно, и менять поведение действующего прода ради будущего хоста нельзя. Файл просто не передаётся в -f. Адрес вынесен в TELEGRAM_API_IP с текущим дефолтом — если он умрёт, правка в одну переменную окружения без релиза. Шаг bootstrap проверяет не факт записи, а что Bot API отвечает 401 на фиктивный токен, и при другом коде печатает команду поиска нового живого адреса. Проверено: docker compose config даёт ровно две вставки extra_hosts (tradein-backend, tradein-tgbot) и ничего больше; TELEGRAM_API_IP подставляется в обе. Refs #3059, #3057 --- ops/selectel-bootstrap.sh | 35 ++++++++ tradein-mvp/docker-compose.selectel.yml | 106 ++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 tradein-mvp/docker-compose.selectel.yml diff --git a/ops/selectel-bootstrap.sh b/ops/selectel-bootstrap.sh index 951a274b..5579eecb 100644 --- a/ops/selectel-bootstrap.sh +++ b/ops/selectel-bootstrap.sh @@ -206,6 +206,41 @@ eff_root="$(sshd -T 2>/dev/null | awk '/^permitrootlogin /{print $2}')" echo "итог: passwordauthentication=$eff_pass permitrootlogin=$eff_root" [ "$eff_pass" = "no" ] || die "PasswordAuthentication остался '$eff_pass' — какой-то файл в sshd_config.d перебивает наш; НЕ перезапускаю" +log "11. Закрепление рабочего адреса api.telegram.org" +# Замер 2026-08-23 с этого хоста: у api.telegram.org семь публикуемых адресов, +# отвечает РОВНО ОДИН — 149.154.167.220. Резолвер при этом отдаёт 149.154.166.110, +# который мёртв. Скан всего 149.154.167.0/24 подтвердил: живой он один во всём блоке. +# Это не блокировка Selectel — с Beget отвечают три адреса из тех же семи, просто +# там штатный резолвер попадает на живой. Через прокси ASocks Telegram тоже не +# проходит (все четыре узла — 000), так что обход только через закрепление IP. +# +# Контейнерам это даёт tradein-mvp/docker-compose.selectel.yml (extra_hosts для +# tgbot и backend). Здесь — для ХОСТОВЫХ скриптов, которым compose не помогает: +# ops/lib-backup.sh (уведомления о бэкапах) и ops/uptime-healthcheck.sh. +# Без этого они молча перестают слать алерты — а это ровно тот канал, которым +# мы узнали бы о любой другой поломке. +TELEGRAM_API_IP="${TELEGRAM_API_IP:-149.154.167.220}" +if grep -qE '^[0-9.]+[[:space:]]+api\.telegram\.org$' /etc/hosts; then + sed -i -E "s|^[0-9.]+([[:space:]]+api\.telegram\.org)$|${TELEGRAM_API_IP}\1|" /etc/hosts + echo "запись обновлена -> ${TELEGRAM_API_IP}" +else + echo "${TELEGRAM_API_IP} api.telegram.org" >> /etc/hosts + echo "запись добавлена -> ${TELEGRAM_API_IP}" +fi +# Проверяем не сам факт записи, а что Bot API реально отвечает: 401 на заведомо +# фиктивный токен означает, что до API достучались и он нас понял. +tg_code="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \ + https://api.telegram.org/bot000000:FakeProbeToken/getMe 2>/dev/null || echo 000)" +if [ "$tg_code" = "401" ]; then + echo "проверка: Bot API отвечает (401 на фиктивный токен) — закрепление работает" +else + echo "ВНИМАНИЕ: Bot API вернул '$tg_code' вместо 401 — адрес ${TELEGRAM_API_IP} мог умереть." + echo " Найди живой: for ip in \$(seq 1 254); do curl -s -o /dev/null --max-time 3 \\" + echo " --resolve api.telegram.org:443:149.154.167.\$ip -w \"149.154.167.\$ip %{http_code}\\n\" \\" + echo " https://api.telegram.org/; done | grep 302" + echo " и пропиши его в TELEGRAM_API_IP (здесь и в .env.runtime для контейнеров)." +fi + cat < 302 за 0.11 с (единственный живой) +# 149.154.166.110 -> 000 <- именно его отдаёт резолвер хоста! +# 149.154.167.99 -> 000 +# 149.154.175.100 -> 000 +# 149.154.171.5 -> 000 +# 91.108.4.5 -> 000 +# 91.108.56.130 -> 000 +# +# Сканом всей подсети 149.154.167.0/24 с этого же хоста подтверждено, что +# 149.154.167.220 — единственный отвечающий адрес Telegram во всём блоке, не +# только среди этих семи. Это НЕ блокировка Selectel и не наш файрвол: с +# Beget отвечают три адреса из тех же семи (включая тот, что отдаёт DNS там), +# т.е. Telegram частично фильтруется у ОБОИХ провайдеров по-разному — Beget +# просто попадает на живой адрес через штатный резолвер, а Selectel нет. +# +# Через прокси (ASocks, все четыре узла) Telegram тоже не проходит — все +# запросы 000, узлы российские. Этот путь закрыт, обход только через IP. +# +# Проверено закрепление (прежде чем закреплять в compose): +# - на хосте через /etc/hosts: curl https://api.telegram.org/ -> 302, 0.11 с +# - настоящий Bot API (/bot/getMe) -> {"ok":false,"error_code":401} за +# 0.11 с (для сравнения — с Beget тот же ответ приходит за 0.25 с, т.е. +# закреплённый адрес не просто живой, а ещё и быстрее дефолтного пути) +# - В КОНТЕЙНЕРЕ через docker --add-host -> 302 за 0.125 с (подтверждает, +# что extra_hosts ниже — не отличается от прямой проверки на хосте) +# - стабильность: 5 запросов подряд -> 302 302 302 302 302, без единого сбоя +# +# ── Что ломается без этого обхода ─────────────────────────────────────────── +# tradein-tgbot — long-polling воркер (см. docker-compose.prod.yml, сервис +# tgbot): он не слушает входящих соединений, а сам постоянно ходит наружу к +# api.telegram.org. Если резолвер отдаёт мёртвый адрес, httpx виснет на +# таймауте long-poll'а и процесс тихо умирает — restart: unless-stopped его +# поднимает заново, он снова резолвит тот же мёртвый адрес и снова виснет. +# Никакого явного краша или строки в логе, по которой это легко поймать — +# отсюда и требование закрепить адрес ДО переезда, а не разбираться постфактum. +# +# Вместе с ботом молча ложатся: +# - ops/lib-backup.sh — уведомления об успехе/провале бэкапов в Telegram +# - ops/uptime-healthcheck.sh — uptime-нотификации +# (оба — ХОСТ-скрипты, не контейнеры; extra_hosts на них не действует, этот +# файл их не чинит — см. TODO в конце файла). +# +# ── TELEGRAM_API_IP: почему переменная, а не жёсткий IP ───────────────────── +# Google/Cloudflare-класса анycast у Telegram нет: их адреса — обычные +# датацентровые IP, которые Telegram время от времени меняет. Если +# 149.154.167.220 однажды тоже станет мёртвым (или Selectel поменяет +# маршрутизацию и он перестанет быть единственным живым), правка — это +# одна строка в .env.runtime (TELEGRAM_API_IP=<новый адрес>) и +# `docker compose ... up -d --force-recreate --no-deps tgbot`, БЕЗ релиза и +# без правки этого файла. Дефолт ниже (:-149.154.167.220) — текущий +# подтверждённый живой адрес, применяется если переменная не задана. +# ── Почему ДВА сервиса, а не только бот ───────────────────────────────────── +# В api.telegram.org ходит не только tgbot. Внутри контейнера backend это +# делают ещё два пути: +# - app/api/v1/glitchtip.py — пересылка алертов GlitchTip в Telegram-тему +# - app/api/v1/support.py — support-чат +# Оба бьют httpx напрямую в api.telegram.org из того же контейнера, что и +# API. Ломаются они ЗАМЕТНЕЕ бота (не long-poll, а per-request: конкретный +# запрос отваливается по таймауту), но ломаются — и, что важнее, ломается +# пересылка алертов, то есть ещё один канал, которым мы узнали бы о беде. +# Закреплять адрес только боту значило бы починить треть и выглядеть готовым. +services: + tgbot: + extra_hosts: + - "api.telegram.org:${TELEGRAM_API_IP:-149.154.167.220}" + backend: + extra_hosts: + - "api.telegram.org:${TELEGRAM_API_IP:-149.154.167.220}" + +# ── Хостовая половина — закрыта в ops/selectel-bootstrap.sh ───────────────── +# ops/lib-backup.sh и ops/uptime-healthcheck.sh исполняются НА ХОСТЕ (cron, +# не в docker), поэтому extra_hosts им не помогает. Их закрывает шаг 11 +# bootstrap-скрипта: он кладёт ту же запись в системный /etc/hosts, идемпотентно +# и с той же переменной TELEGRAM_API_IP. Итого закреплены все три потребителя: +# tgbot, backend и хостовые скрипты. +# +# ⚠️ ОДНА ТОЧКА ОТКАЗА, ЗАФИКСИРОВАНА ОСОЗНАННО. 149.154.167.220 — единственный +# живой адрес во всём 149.154.167.0/24 с этого хоста. Если он умрёт, бот снова +# умрёт молча, а канал оповещения об этом сам идёт через Telegram — поломка +# скрывает сама себя. Поэтому закрепление НЕОБХОДИМО, НО НЕ ДОСТАТОЧНО: нужен +# независимый канал алертов. Проверено, что с нового хоста доступен +# smtp.beget.com:465 (порт 587 закрыт), плюс GlitchTip остаётся на Beget и +# доступен по имени. Отдельная задача, см. #3059.