docs: описать транзакционный guard и взаимное исключение операций

- docs/07: полный порядок staged apply, инвариант снятия guard, объяснение
  почему окно 45 секунд не обязано покрывать smoke и почему guard не трогает
  nftables.service, семантическая проверка эффективного firewall;
- docs/11: разделы A5e/A5f для новых unit-тестов и серверные сценарии D1e
  (guard доходит до дедлайна), D1f (конкурентные операции), D1g (успешная
  операция не оставляет следов транзакции); матрица и acceptance criteria
  дополнены;
- docs/12: разбор отказов "уже выполняется другая операция" и
  firewall_guard_fired;
- docs/13: строки журнала guard в таблице recovery, новый раздел 8a про замок
  операций;
- docs/14 и purge-v0.sh: очистка /run/hy2xs, замка операций и candidate-файлов
  firewall — /run это tmpfs, но очистка не имеет права требовать перезагрузки;
- README: защита от потери доступа при смене firewall и раздел "Одна операция
  за раз";
- CHANGELOG: шестой проход.
This commit is contained in:
2026-08-31 01:31:15 +05:00
parent 50ec4d9717
commit 39139e95f7
8 changed files with 532 additions and 11 deletions
+72 -6
View File
@@ -82,12 +82,78 @@ IPv4-only policy:
## Порядок применения
1. Подготовить candidate-файл (`/etc/nftables.d/hy2xs.nft`).
2. Проверить `nft -c -f`.
3. Создать rollback timer.
4. Применить candidate и проверить SSH/Hysteria/UI.
5. При успехе отменить rollback timer.
6. При провале — rollback.
1. Снять резервную копию `/etc/nftables.conf`, `/etc/nftables.d/hy2xs.nft` и
состояния юнита `nftables.service` в `/run/hy2xs/rollback/<op-id>/` и
**доказать**, что копия создана. Отказ здесь останавливает операцию до
первой мутации.
2. Подготовить candidate-файлы и проверить их `nft -c -f`.
3. Подставить candidate в production-пути.
4. Взвести rollback guard: транзиентный юнит `hy2xs-fw-rollback-<op-id>`
с окном 45 секунд.
5. Применить ruleset и проверить SSH/Hysteria/UI.
6. Снять guard и **доказать**, что он снят (см. ниже).
7. Долговечно зафиксировать успех.
8. Только после этого удалить данные отката и candidate-файлы.
Порядок шагов 6–8 существенен: между снятием guard и удалением данных отката
стоит фиксация успеха, поэтому отказ записи маркера (заполненный диск,
read-only ФС) оставляет откат выполнимым.
### Rollback guard
Guard — это защита от потери доступа к серверу. Он существует ради ситуации, в
которой применённые правила отрезали SSH и оператор больше не может ничего
сделать руками.
Окно guard намеренно короткое — 45 секунд — и намеренно **не** покрывает
smoke: smoke на медленном, но исправном сервере может идти заметно дольше.
Увеличение окна лечило бы гонку расширением, а не устранением.
Вместо этого guard оставляет за собой факт:
```text
/run/hy2xs/rollback/<op-id>/auto-rollback-fired
```
Маркер создаётся rollback-скриптом **первым действием**, до любой проверки и до
первой попытки восстановления. Отсюда инвариант фиксации успеха:
```text
маркер auto-rollback-fired отсутствует
И hy2xs-fw-rollback-<op-id>.timer в состоянии inactive
И hy2xs-fw-rollback-<op-id>.service в состоянии inactive
=> автоматический откат больше не может сработать
```
Пока этот инвариант не доказан, `phase: installed` не записывается. Если guard
успел сработать, операция **обязана** завершиться отказом — даже если smoke
прошёл зелёным: сервер в этот момент работает на прежнем firewall, а не на том,
который сгенерировала операция.
Проверка состояния юнитов идёт по `ActiveState`, а не по коду возврата
`systemctl stop`: для транзиентного юнита, который уже отработал и был убран
systemd, `stop` возвращает 5, и этот исход неотличим от успешного снятия
взведённого таймера.
Сам rollback-скрипт восстанавливает файлы и ruleset, накапливает код возврата и
уходит в `failed` при частичном восстановлении. Состояние `nftables.service` он
сознательно не трогает: у этого юнита `ExecStop=/usr/sbin/nft flush ruleset`, то
есть остановка сервиса стёрла бы только что восстановленные правила. Enable и
active восстанавливает обычный откат в процессе оркестратора, где порядок стадий
контролируется.
### Проверка эффективного firewall
`nft -c -f /etc/nftables.conf` разбирает текущий файл, каким бы он ни был, и
поэтому ничего не говорит о том, чей это firewall. Smoke дополнительно сверяет:
1. `/etc/nftables.d/hy2xs.nft` совпадает с фрагментом, отрендеренным для этой
конфигурации;
2. `/etc/nftables.conf` принадлежит HY2XS и подключает именно его;
3. таблица `inet hy2xs` реально загружена в ядро.
Все три — наблюдение, поэтому проверка выполняется и в `doctor`, где она
обнаруживает расхождение effective firewall с конфигурацией.
## Что не делаем