vpnbot-xray-patches/docs/2026-08-07-release-telegram-alert-plan.md
loop-uh 28b36ad535
All checks were successful
Follow official Xray dev releases / candidate (push) Successful in 2m40s
Test and build VPnBot Xray patches / test-and-build (push) Successful in 12m31s
Наблюдатель выпусков Xray: Telegram по сетевому контракту хоста
Симптом: vpnbot-xray-release-alert.service падал на каждой попытке
доставки (раз в RETRY_SECONDS=900): 20 с таймаута на заблокированном
149.154.166.110:443, затем ENETUNREACH на IPv6. Ни авария, ни сообщение
о восстановлении не доходили.

Первопричина: монитор слал sendMessage прямым urllib.urlopen на
api.telegram.org, в обход сетевого контракта, которым пользуются все
процессы бота: VPNBOT_TELEGRAM_IP_FAMILY=ipv4, резервные
VPNBOT_TELEGRAM_API_FALLBACK_IPV4S и relay-туннели node manager из
/run/vpnbot-node-manager/telegram-egress.json.

Исправление: контракт читается из того же root-only runtime-env бота,
откуда уже берётся токен (без копии в /etc и без импорта кода бота).
Маршруты как у бота: свежая проекция со здоровыми портами api.telegram.org
- только loopback-relay; иначе резервные IPv4, затем DNS выбранного
семейства. Меняется лишь TCP-адрес, TLS держит SNI и проверку сертификата
api.telegram.org.

Инвариант: следующий маршрут пробуется только если соединение не
установилось и запрос не ушёл; ушедший запрос без ответа -
telegram_delivery_ambiguous без повтора в этом запуске. Каждый отказ
несёт typed-код (telegram_egress_config_invalid, telegram_no_route,
telegram_connect_failed, telegram_delivery_ambiguous,
telegram_http_status, telegram_response_invalid, telegram_rejected).

Проверка: unittest discover - 44 теста, включая TLS-сервер с SNI
api.telegram.org за loopback-портом; живой запрос getMe с заведомо
неверным токеном через relay 18443/18444 и резервный 149.154.167.220
получил 401 от Telegram.

Claude-Session: local_e733a8c5-58d7-482a-bce7-150abdb901db
2026-09-24 20:04:13 +03:00

157 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Telegram-уведомления о безопасно остановившемся выпуске Xray
## Задача
Автоматический выпуск уже безопасно останавливается до появления `proven`,
если патчи не накладываются, сборка или тесты падают, candidate слишком долго
не проходит пилот либо canary не подтверждает активный отзыв доступа. Сейчас
это состояние видно в Forgejo Actions, systemd и root-only JSON промоутера, но
оператор должен сам регулярно проверять все три места.
Нужен отдельный наблюдатель, который быстро сообщает в Telegram только о
состояниях, требующих реакции:
1. workflow автоматического candidate-выпуска завершился ошибкой или был
отменён;
2. опубликованный candidate не получил соответствующий `proven` дольше
допустимого времени;
3. production-canary завершился ошибкой, включая отдельную повышенную
критичность, если не удался и возврат на предыдущий proven.
Сам наблюдатель не должен изменять релизы, canary, конфигурацию Xray или парк
узлов. Его единственная запись во внешнюю систему — операторское сообщение в
Telegram.
## Источники истины
- Структурированное состояние Forgejo Actions владеет результатом
`candidate.yml`. Сначала наблюдатель читает публичный Actions API. Если
Forgejo показывает запуски в web-интерфейсе, но API возвращает пустой
`workflow_runs`, используется ограниченный read-only fallback: страница
именно `candidate.yml` выбирает последний запуск `main`, а detail-страница
отдаёт встроенный JSON `state.run`. Иконки и локализованный HTML-текст не
считаются доказательством результата. Успешный запуск закрывает ранее
зарегистрированный сбой; незавершённый запуск считается `pending` только до
общего stall-порога, после чего становится проблемой runner.
- Forgejo Releases API и манифест candidate владеют связью
`candidate -> proven`. Задержка считается от `published_at`, а не от времени
локального опроса. Уже опубликованный proven немедленно снимает задержку.
- `/var/lib/vpnbot-xray-release-promoter/*.json` владеет результатом живого
пилота. Фазы `failed_rolled_back` и `failed_rollback_failed` являются
canary-инцидентами. Фазы повторной установки, проверки и продвижения —
переходные: во время них прежнее уведомление ещё нельзя объявлять
восстановленным.
- `/var/lib/vpnbot-xray-release-alert/state.json` владеет только жизненным
циклом операторских уведомлений: что уже отправлялось, когда была последняя
попытка и когда состояние действительно восстановилось. Он не становится
источником истины для самого выпуска.
## Архитектура
Один root-only systemd timer раз в пять минут запускает один `oneshot`-процесс.
Процесс берёт неблокирующую файловую блокировку, получает общий снимок Forgejo,
читает локальные состояния промоутера и вычисляет три независимых условия:
- `patch_pipeline`;
- `candidate_stalled`;
- `canary_failed`.
Для каждого условия возможны три результата:
- `problem` — подтверждён новый или продолжающийся инцидент;
- `healthy` — источник истины подтверждает восстановление;
- `pending` — идёт новый workflow или повторный пилот, поэтому прежний
инцидент пока нельзя закрывать.
`waiting`, `queued` или другой переходный статус не может оставаться
безмолвным бесконечно. После общего stall-порога наблюдатель публикует
операторскую проблему с идентификатором запуска и предлагает проверить
регистрацию runner, его состояние online и обязательную метку `linux`.
У каждого инцидента есть детерминированная сигнатура: идентификатор Actions-
запуска либо набор candidate-тегов и стадий. Новая сигнатура создаёт обновление
аварии, та же сигнатура не порождает повторное сообщение при каждом опросе.
## Доставка и дедупликация
Перед сетевой попыткой наблюдатель атомарно сохраняет её время. После
подтверждённого ответа Telegram Bot API он сохраняет `message_id` и время
успешной доставки. Это даёт следующие правила:
- первое сообщение отправляется один раз при переходе в проблему;
- после неоднозначной сетевой ошибки немедленного шторма повторов нет: новая
попытка разрешается только после отдельного интервала повторной доставки;
- для долгого неустранённого инцидента допускается редкое напоминание;
- сообщение о восстановлении отправляется только после подтверждённого
`healthy` и само повторяется безопасно, пока Telegram не подтвердит доставку;
- смена одной неуспешной попытки выпуска другой не создаёт ложную пару
«восстановлено -> снова сломано», а обновляет действующий класс инцидента.
Telegram Bot API не предоставляет ключ идемпотентности для `sendMessage`,
поэтому абсолютно исключить дубль при редком сценарии «Telegram принял
сообщение, но ответ потерялся» невозможно. Долговечное время попытки и большой
интервал повтора ограничивают этот случай одним редким повтором, а обычные
systemd-запуски полностью дедуплицируются.
## Сетевой путь до Telegram
На production-хосте нет IPv6-маршрута, а часть IPv4-адресов Telegram
заблокирована: прямой `urlopen` тратил весь таймаут на мёртвый IPv4 и падал
`ENETUNREACH` на IPv6, поэтому ни авария, ни восстановление не доставлялись.
Наблюдатель не заводит собственной сетевой политики и не импортирует код бота:
он читает тот же контракт, что и все процессы, владеющие ботом
(`_BOT_IDENTITY` в `deployment/service_env_profiles.py` VPnBot), из того же
root-only runtime-env, откуда берёт токен:
- `VPNBOT_TELEGRAM_IP_FAMILY` — семейство адресов (пусто/`ipv4` по умолчанию,
`auto`, `ipv6`); неизвестное значение — отказ
`telegram_egress_config_invalid`;
- `VPNBOT_TELEGRAM_API_FALLBACK_IPV4S` — резервные IPv4 Bot API;
- `VPNBOT_TELEGRAM_EGRESS_HEALTH_PATH` — путь проекции relay (по умолчанию
`/run/vpnbot-node-manager/telegram-egress.json`, владелец — node manager).
Порядок маршрутов повторяет бота (`telegram_egress.py`, `telegram_client.py`):
свежая (не старше 45 с по `updated_at_monotonic`) проекция со здоровыми портами
`api.telegram.org` — только эти loopback-relay; иначе резервные IPv4, затем DNS
выбранного семейства. Меняется только TCP-адрес: TLS несёт SNI
`api.telegram.org` и проверяет его сертификат.
Следующий маршрут пробуется только если соединение не установилось (TCP или
TLS) — запрос ещё не ушёл. Если запрос ушёл, а полного ответа нет, это
`telegram_delivery_ambiguous`, и в этом запуске он не повторяется: у
`sendMessage` нет ключа идемпотентности. Коды отказа: `telegram_no_route`,
`telegram_connect_failed`, `telegram_delivery_ambiguous`,
`telegram_http_status`, `telegram_response_invalid`, `telegram_rejected`.
## Безопасность
- Токен бота не копируется в новый файл. Наблюдатель читает только
`VPNBOT_BOT_TOKEN` из существующего root-only runtime-env VPnBot.
- Chat ID, URL API, интервалы и пути лежат в отдельном root-only
`/etc/vpnbot-xray-release-alert.env`.
- Состояние и lock имеют режим `0600`, каталог состояния — `0700`.
- Forgejo-релизы и Actions публичного patch-репозитория читаются без
write-токена промоутера.
- systemd запрещает повышение привилегий, запись в систему и запись в домашние
каталоги; разрешён только каталог состояния наблюдателя.
- Некорректный JSON, небезопасный путь, неизвестная структура API или ошибка
чтения не переводятся в «всё исправилось». Процесс завершается ошибкой, а
предыдущее активное состояние остаётся сохранённым.
## Проверка и выпуск
1. Unit-тестами проверить классификацию Actions, пустой публичный API при
существующем структурированном web-run, зависший runner, задержку candidate,
canary-фазы, незакрытие инцидента во время retry, дедупликацию, напоминание
и восстановление.
2. Прогнать полную существующую проверку репозитория патчей и сборку поверх
официального Xray.
3. Опубликовать один commit в Forgejo и дождаться успешных `ci.yml` и
`candidate.yml`.
4. Установить service/timer на production, создать только конфигурационный
env без секретов и отправить явно помеченное тестовое сообщение оператору.
5. Запустить обычную проверку в текущем здоровом состоянии: новых аварийных
сообщений быть не должно, unit должен завершиться успешно, состояние должно
зафиксировать три здоровых условия.
6. Обновить проектный навык сопровождения Xray: зафиксировать новые unit,
runtime-пути, значения состояний и команды проверки.