firewall guard: барьер покоя fail-closed и явный контракт транзиентного таймера

Барьер, обязанный ДОКАЗАТЬ отсутствие асинхронного исполнителя, в трёх местах
принимал за доказательство отсутствие наблюдения.

- отказ `systemctl` больше не выдаётся за отсутствие guard: вместо `return []`
  введён единый наблюдатель inspectRollbackGuard с исходами quiescent/pending/
  unknown и отдельным типом отказа GuardStateUnknownError;
- покой перечисляется белым списком (inactive, failed): maintenance,
  refreshing и любое незнакомое состояние systemd блокируют операцию;
- у транзиентного таймера явно заданы AccuracySec=1s (умолчание 1min
  превращало обещанные 45 секунд в 45-105) и RemainAfterElapse=no; барьер
  дополнительно опознаёт SubState=elapsed у *.timer как покой;
- команда взведения строится чистой buildArmGuardArgv и выполняется новым
  runMutatingArgv без shell, поэтому её контракт проверяется значением, а не
  грепом по исходнику;
- status перестал листить guard-юниты своей копией кода: без --plain, с
  `|| true` и с трактовкой failed как «вооружён» отчёт вечно противоречил
  барьеру. Добавлены rollback_guard_state и firewall_state=guard_unknown;
- purge-v0.sh пропускал failed-юниты из-за маркера в первой колонке.

Барьер покрыт поведенческими тестами через подставляемый SystemdUnitProbe:
прежние проверки грепом по тексту функции пережили инверсию смысла - строка
`return [];` была на месте, а решение стало неверным.

Документация (README, docs/07, 11, 12, 13, 14, CHANGELOG) приведена к реальному
окну 45-46 секунд и к новому тексту отказа. Отдельно исправлен комментарий
PNPM_AUDIT_LEVEL в versions.env: гейт давно проверяет весь lock-граф.
This commit is contained in:
2026-08-31 16:44:20 +05:00
parent 76d78ac71f
commit 2259f7c847
14 changed files with 1115 additions and 118 deletions
+16 -3
View File
@@ -81,10 +81,11 @@ sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env
| `rollback finished with N failed stage(s); manual recovery may be required` | итог: перечисленные стадии требуют ручной проверки |
| `rollback completed: N stage(s) succeeded` | восстановление отработало полностью |
| `manual recovery data preserved at /run/hy2xs/rollback/<op>` | firewall восстановлен не полностью; прежние `nftables.conf` и `hy2xs.nft` лежат по этому пути |
| `firewall rollback guard armed: … fires in 45s` | guard взведён; с этого момента операция обязана снять его до фиксации успеха |
| `firewall rollback guard armed: … fires in 45s (timer accuracy 1s)` | guard взведён; с этого момента операция обязана снять его до фиксации успеха |
| `firewall rollback guard disarmed and proven inactive` | guard снят, и это подтверждено состоянием юнитов и отсутствием маркера срабатывания |
| `automatic firewall rollback has already fired` | guard успел сработать; сервер работает на **прежнем** firewall, операция обязана завершиться отказом |
| `firewall rollback guard <unit> is still in state "…"` | остановить guard не удалось; фиксация успеха запрещена, разбирайтесь с systemd |
| `unable to verify firewall rollback guard state; systemd query failed` | состояние guard'а недоказуемо; операция не начата, чинить нужно systemd, а не ждать |
Отдельно про сработавший guard. Окно 45 секунд намеренно короче худшего случая
smoke и не обязано его покрывать: доказательством служит не время, а маркер
@@ -169,14 +170,26 @@ operation lock … is held by install (pid 1234), which is no longer running; re
```text
previous HY2XS operation is no longer running, but its firewall rollback guard
is still armed: hy2xs-fw-rollback-<op-id>.timer (active)
is still armed: hy2xs-fw-rollback-<op-id>.timer (active/waiting)
```
Условие старта — не «PID предыдущей мёртв», а «у предыдущей не осталось
исполнителей, способных изменить систему». Ждать нужно не больше 45 секунд с
исполнителей, способных изменить систему». Ждать нужно не больше 4546 секунд с
момента применения firewall; `failed` у guard покою не мешает и означает, что
пора смотреть `journalctl -u 'hy2xs-fw-rollback-*'` и запускать `repair`.
Второй отказ того же барьера выглядит иначе и требует другого действия:
```text
unable to verify firewall rollback guard state; systemd query failed,
refusing to start a lifecycle operation
```
Здесь ждать бессмысленно. Барьер обязан **доказать** покой, а не предположить
его: отсутствие ответа systemd — это отсутствие наблюдения, а не наблюдение
отсутствия guard'а. Смотрите `systemctl status` и повторяйте операцию после того,
как systemd отвечает.
`/run/lock` — это tmpfs, поэтому перезагрузка снимает замок в любом случае.
Удалять файл руками нужно только если в нём оказалось непонятное содержимое:
такой замок сознательно не переиспользуется автоматически — непонятый файл не