Симптом: 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
13 KiB
Telegram-уведомления о безопасно остановившемся выпуске Xray
Задача
Автоматический выпуск уже безопасно останавливается до появления proven,
если патчи не накладываются, сборка или тесты падают, candidate слишком долго
не проходит пилот либо canary не подтверждает активный отзыв доступа. Сейчас
это состояние видно в Forgejo Actions, systemd и root-only JSON промоутера, но
оператор должен сам регулярно проверять все три места.
Нужен отдельный наблюдатель, который быстро сообщает в Telegram только о состояниях, требующих реакции:
- workflow автоматического candidate-выпуска завершился ошибкой или был отменён;
- опубликованный candidate не получил соответствующий
provenдольше допустимого времени; - production-canary завершился ошибкой, включая отдельную повышенную критичность, если не удался и возврат на предыдущий proven.
Сам наблюдатель не должен изменять релизы, canary, конфигурацию Xray или парк узлов. Его единственная запись во внешнюю систему — операторское сообщение в Telegram.
Источники истины
- Структурированное состояние Forgejo Actions владеет результатом
candidate.yml. Сначала наблюдатель читает публичный Actions API. Если Forgejo показывает запуски в web-интерфейсе, но API возвращает пустойworkflow_runs, используется ограниченный read-only fallback: страница именноcandidate.ymlвыбирает последний запускmain, а detail-страница отдаёт встроенный JSONstate.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 или ошибка чтения не переводятся в «всё исправилось». Процесс завершается ошибкой, а предыдущее активное состояние остаётся сохранённым.
Проверка и выпуск
- Unit-тестами проверить классификацию Actions, пустой публичный API при существующем структурированном web-run, зависший runner, задержку candidate, canary-фазы, незакрытие инцидента во время retry, дедупликацию, напоминание и восстановление.
- Прогнать полную существующую проверку репозитория патчей и сборку поверх официального Xray.
- Опубликовать один commit в Forgejo и дождаться успешных
ci.ymlиcandidate.yml. - Установить service/timer на production, создать только конфигурационный env без секретов и отправить явно помеченное тестовое сообщение оператору.
- Запустить обычную проверку в текущем здоровом состоянии: новых аварийных сообщений быть не должно, unit должен завершиться успешно, состояние должно зафиксировать три здоровых условия.
- Обновить проектный навык сопровождения Xray: зафиксировать новые unit, runtime-пути, значения состояний и команды проверки.