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 с конфигурацией.
## Что не делаем
+179 -2
View File
@@ -245,6 +245,59 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
- успешный smoke фиксируется отдельной фазой до снятия таймера;
- уборка после точки фиксации выполняется best-effort.
## A5e. Транзакционность rollback guard (unit)
`orchestrator/test/firewall-guard.test.ts`:
- маркер `auto-rollback-fired` создаётся rollback-скриптом **первым действием**
до проверки `prepared` и до первой попытки восстановления, в том числе когда
восстанавливать нечего. Без этого «guard сработал» недоказуемо: транзиентные
юниты systemd после выполнения исчезают, и `systemctl stop` для них неотличим
от успешного снятия взведённого таймера;
- скрипт не маскирует ошибки (`|| true`, `2>/dev/null`), не использует `set -e`
и возвращает накопленный `rc`: каждый сообщённый отказ поднимает код возврата,
поэтому частичное восстановление уходит в `failed`, а не в молчаливый `0`;
- скрипт не трогает `nftables.service`: у него `ExecStop=nft flush ruleset`, и
остановка сервиса стёрла бы только что восстановленные правила;
- скрипт разбирается **настоящим** shell-парсером. Парсер принимается только
после двусторонней проверки — он обязан принять заведомо корректный скрипт и
отвергнуть заведомо сломанный, иначе тест ничего не проверяет;
- небезопасный ключ операции отвергается: он служит именем каталога, именем
systemd-юнита и подставляется в текст скрипта;
- снятие guard проверяет маркер **с обеих сторон** остановки, подтверждается
`ActiveState` обоих юнитов, и на пути фиксации успеха допускает единственное
состояние — `inactive`; на пути восстановления `failed` тоже допустим;
- сработавший guard опознаётся по **типу** ошибки: ошибка с тем же текстом, но
другого типа классифицируется по владению, как и прежде;
- эффективный firewall сверяется семантически (фрагмент, entrypoint,
загруженная таблица), и проверка выполняется read-only раннерами — та же
проверка идёт в `doctor`;
- состояние `nftables.service` снимается до первой мутации и восстанавливается
стадиями, идущими **до** применения ruleset;
- candidate-файлы убираются после успеха и best-effort при откате;
- ключ операции считается одной функцией: install писал в маркер сырой
ISO-timestamp, и путь `/run/hy2xs/rollback/<op_id>` из runbook не существовал.
## A5f. Взаимное исключение операций (unit)
`orchestrator/test/operation-lock.test.ts`:
- второй захват при живом держателе отказывает, и отказ называет держателя —
команду, PID и время начала;
- замок снимается в `finally` и после отказа операции: иначе первая же неудачная
установка заблокировала бы сервер до перезагрузки;
- замок мёртвого держателя переиспользуется, временный файл переиспользования не
остаётся на диске;
- непонятое содержимое замка **не** снимается автоматически: оно не доказывает
отсутствие операции, и сомнение трактуется в пользу отказа;
- `readLockHolder` отличает «замка нет» от «замок нечитаем»;
- захват под read-only guard отказывает, наблюдение — разрешено. Замок берётся
до включения guard, и проверка существует, чтобы перенос захвата внутрь
читающей фазы отказал громко, а не записал файл молча;
- политика CLI закреплена структурно: `install`/`reconfigure`/`repair`/`doctor`
вызываются только под замком, `status`/`diagnostics` его не берут, но сообщают
об идущей операции, а `preflight-install` отказывает до собственных проверок.
## A5b. Долговечная запись маркера (unit)
`orchestrator/test/atomic-write.test.ts`:
@@ -956,6 +1009,111 @@ dd if=/dev/zero of=/var/lib/hy2xs/filler bs=1k count=64 2>/dev/null || true
`manifest.json`, и в нём перечислены все семь путей, включая отсутствовавшие с
`"present": false`.
## D1e. Guard доходит до дедлайна — фиксация успеха запрещена
Проверяется на чистом хосте. Это сценарий гонки между автоматическим откатом
firewall и успешным smoke.
Окно guard — 45 секунд, и оно намеренно короче худшего случая smoke: на
медленном, но исправном сервере retry-бюджеты дают заметно больше. Раньше это
означало, что автоматический откат мог вернуть прежний firewall, пока smoke
продолжает идти, а единственной проверкой firewall в smoke был `nft -c` — разбор
текущего файла, каким бы он ни был. Прежний валидный ruleset проходил её
зелёным, и сервер объявлялся успешно настроенным с **предыдущим** firewall.
Сценарий:
1. установка доходит до шага `firewall`, в журнале появляется
`firewall rollback guard armed`;
2. smoke искусственно замедляется дольше 45 секунд. Проще всего задержать один
из сервисов — например, добавить в `hy2xs-admin.service` временный
`ExecStartPre=/bin/sleep 60` и выполнить `systemctl daemon-reload` до запуска
установки;
3. guard срабатывает: в journal появляется юнит
`hy2xs-fw-rollback-<op-id>.service`, а на диске —
`/run/hy2xs/rollback/<op-id>/auto-rollback-fired`;
4. установка **обязана** завершиться отказом, даже если smoke успел сойтись;
5. в маркере установки стоит `phase: firewall_guard_fired`, а не
`installed`, и не `smoke_failed`;
6. `installed: true` не записан;
7. выполняется обычный откат операции: firewall возвращается к прежнему
состоянию, развёрнутые этой операцией юниты останавливаются;
8. SSH остаётся доступным.
Отдельно проверяется вторая половина того же дефекта — семантический smoke.
Если на рабочей установке подменить `/etc/nftables.d/hy2xs.nft` на прежний
валидный ruleset и выполнить `hy2xs-orchestrator doctor`, диагностика обязана
отказать с сообщением про несовпадение эффективного firewall, а не пройти по
`nft -c`.
## D1f. Конкурентная операция отказывает до первой мутации
Проверяется на рабочей установке. Проверяемое свойство — отказ происходит
**до** снятия резервной копии и до первой мутации, а не в середине транзакции.
1. запускается длинный `reconfigure --apply` (например, с задержкой в
`ExecStartPre`, как в D1e);
2. во втором терминале, пока первый идёт, запускается второй
`reconfigure --apply`;
3. второй отказывает сразу, с текстом
`another HY2XS operation is already in progress: reconfigure (pid …)`;
4. `/etc/hy2xs/backups/` **не** пополнился каталогом второй операции;
5. `/etc/hysteria/config.yaml`, unit-файлы и `/etc/nftables.conf` изменены
ровно один раз — первой операцией;
6. `/run/hy2xs/rollback/` содержит каталог только первой операции.
Те же проверки для пар:
```text
install идёт -> doctor отказывает
install идёт -> install.sh отказывает на PHASE 0, до собственных проверок
reconfigure идёт -> repair отказывает
```
И обратная проверка — наблюдающие команды не блокируются:
```text
reconfigure идёт -> hy2xs-orchestrator status
→ выполняется
→ в отчёте operation_in_progress = "reconfigure (pid …)"
→ human_status предупреждает, что это снимок незавершённой транзакции
reconfigure идёт -> diagnostics collect
→ выполняется
→ в stderr есть note об идущей операции
```
Отдельно проверяется, что замок не переживает своего держателя:
1. `reconfigure --apply` прерывается `Ctrl+C` — замок снят, следующий
`reconfigure` проходит;
2. процесс убивается `kill -9`, после чего следующая операция сообщает
`is held by … which is no longer running; reclaiming it` и продолжает;
3. `/run/lock/hy2xs-orchestrator.lock` не остаётся после завершения операции.
## D1g. Успешная установка не оставляет следов транзакции
Проверяется на чистом хосте, обычной успешной установкой. Это обратная проверка
к D1c и D1e: она ловит противоположную ошибку — данные транзакции, пережившие
её завершение.
После `installed`:
```text
systemctl list-units --all 'hy2xs-fw-rollback-*' → пусто
ls /run/hy2xs/rollback/ → пусто
ls /run/lock/hy2xs-orchestrator.lock → отсутствует
ls /etc/nftables.conf.candidate → отсутствует
ls /etc/nftables.d/hy2xs.nft.candidate → отсутствует
```
и `/var/lib/hy2xs/install-state.json` содержит `phase: installed`,
`installed: true`, а `op_id` в нём совпадает с именем каталога, который лежал в
`/run/hy2xs/rollback/` во время установки.
`/etc/nftables.conf.candidate` — прямая регрессия: он не удалялся вообще, и
успешная установка оставляла его на сервере навсегда.
## D1a. Проход установки не спотыкается о собственный маркер
Проверяется на чистом хосте, обычной успешной установкой.
@@ -1031,8 +1189,20 @@ hy2xs-orchestrator doctor
- при `HY2XS_FIREWALL_MODE=managed` install/reconfigure блокируются;
- при `HY2XS_FIREWALL_MODE=takeover` создаются backup/rollback guard и apply проходит.
4. **Rollback guard cleanup**:
- после успешного apply/smoke не остаются `hy2xs-fw-rollback-*.timer/.service`.
4. **Rollback guard cleanup** (сценарий D1g):
- после успешного apply/smoke не остаются `hy2xs-fw-rollback-*.timer/.service`;
- `/run/hy2xs/rollback/`, `/run/lock/hy2xs-orchestrator.lock` и
`*.candidate` не переживают успешную операцию.
4a. **Guard доходит до дедлайна** (сценарий D1e):
- `auto-rollback-fired` создан, операция завершается отказом с
`phase: firewall_guard_fired`;
- `installed: true` не записан, даже если smoke успел сойтись.
4b. **Конкурентные операции** (сценарий D1f):
- вторая операция отказывает **до** снятия резервной копии и первой мутации;
- `status`/`diagnostics` не блокируются и сообщают об идущей операции;
- замок не переживает своего держателя.
5. **Partial install + repair**:
- состояние `install-state` фиксирует промежуточную фазу;
@@ -1118,3 +1288,10 @@ hy2xs-orchestrator doctor
58. копия привязана к операции: откат восстанавливает состояние непосредственно перед текущим проходом, а не сохранённое предыдущим
59. ни одна команда отката не глушит свой код возврата; отказавшие стадии перечисляются, а артефакты восстановления удаляются только после подтверждённого успеха
60. `doctor` не выполняет проб, изменяющих данные в админке: авторизация действующим паролем пира ограничена режимом `install`
61. снятие rollback guard доказывается, а не объявляется: отсутствие маркера `auto-rollback-fired` и `ActiveState=inactive` обоих юнитов — предусловие записи `phase: installed`
62. сработавший guard запрещает фиксацию успеха, каким бы ни был результат smoke, и получает собственную причину отказа `firewall_guard_fired`
63. smoke сверяет **эффективный** firewall с конфигурацией операции, а не только разбирает `/etc/nftables.conf`
64. автоматический откат firewall сообщает о частичном восстановлении отказом юнита, а не молчаливым кодом 0, и сохраняет данные восстановления
65. откат восстанавливает `enabled`/`active` состояние `nftables.service`, а не только файлы правил
66. операции жизненного цикла сериализованы эксклюзивным замком: вторая операция отказывает до первой мутации, а `status`/`diagnostics` не блокируются
67. замок снимается при любом завершении держателя, включая `Ctrl+C`, SIGTERM и обрыв SSH; замок мёртвого держателя переиспользуется безопасно
+66
View File
@@ -156,6 +156,72 @@ hy2xs-orchestrator repair \
Флаг обязателен и осознан: без него `repair` работает только поверх полностью
успешной установки.
### Операция отказывает: уже выполняется другая
```text
another HY2XS operation is already in progress: reconfigure (pid 4242, started at …)
```
`install`, `reconfigure`, `repair` и `doctor` сериализованы замком
`/run/lock/hy2xs-orchestrator.lock`. Это не перестраховка: production paths —
`/etc/hysteria/config.yaml`, unit-файлы, `/etc/nftables.conf`,
`/var/lib/hy2xs/install-state.json` — общие, и две одновременные операции
записывают их поверх друг друга, после чего откат одной «восстанавливает»
состояние поверх изменений другой.
Отказ происходит **до** первой мутации, поэтому сервер не тронут. Что делать:
```bash
# кто держит замок
cat /run/lock/hy2xs-orchestrator.lock
# что делает держатель
ps -o pid,etime,cmd -p "$(sed -n 's/.*"pid": *\([0-9]*\).*/\1/p' /run/lock/hy2xs-orchestrator.lock)"
```
Дождитесь завершения. Замок снимается сам при любом завершении держателя,
включая `Ctrl+C`, SIGTERM и обрыв SSH, а мёртвого держателя следующая операция
обнаруживает и переиспользует замок самостоятельно.
Удалять файл руками нужно ровно в одном случае — если оркестратор сообщил, что
содержимое замка не является корректной записью:
```text
operation lock … exists but is not a valid HY2XS lock record
```
Такой замок сознательно не снимается автоматически: непонятый файл не
доказывает, что операции нет.
`status` и `diagnostics collect` замок не берут и работают во время операции.
В отчёте `status` при этом появляется поле `operation_in_progress` — читайте
состояние как снимок незавершённой транзакции, а не как итог.
### Установка отказала с `firewall_guard_fired`
```text
automatic firewall rollback has already fired
phase: firewall_guard_fired
```
Это означает: автоматический откат firewall сработал раньше, чем операция успела
снять guard. Сервер жив и доступен, но работает на **прежнем** firewall, а не на
том, который сгенерировала операция. Именно поэтому фиксация успеха запрещена,
даже если smoke успел сойтись, — иначе сервер считался бы настроенным с чужими
правилами, что особенно дорого при смене порта Hysteria, SSH или ACME.
Окно guard — 45 секунд, и оно не обязано покрывать smoke. Причину ищите в том,
почему проход в него не уложился:
```bash
journalctl -u 'hy2xs-fw-rollback-*' --no-pager
journalctl -u hysteria-server -u hy2xs-admin --since '-10 min' --no-pager
```
Обычная причина — медленный старт одного из сервисов. После устранения
root-cause операция запускается заново; откат уже вернул сервер в исходное
состояние.
### Сервер установился, но UI не работает
Проверить:
- разложился ли bundled UI
+66
View File
@@ -81,6 +81,29 @@ 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 disarmed and proven inactive` | guard снят, и это подтверждено состоянием юнитов и отсутствием маркера срабатывания |
| `automatic firewall rollback has already fired` | guard успел сработать; сервер работает на **прежнем** firewall, операция обязана завершиться отказом |
| `firewall rollback guard <unit> is still in state "…"` | остановить guard не удалось; фиксация успеха запрещена, разбирайтесь с systemd |
Отдельно про сработавший guard. Окно 45 секунд намеренно короче худшего случая
smoke и не обязано его покрывать: доказательством служит не время, а маркер
```text
/run/hy2xs/rollback/<op-id>/auto-rollback-fired
```
Если он есть — операция откатывается независимо от результата smoke, и в
маркере установки появляется `phase: firewall_guard_fired`. Это значит: сервер
жив и работает на прежнем firewall, а причину, по которой проход не уложился в
окно, надо искать в journal — обычно это медленный старт одного из сервисов.
Юнит автоотката при частичном восстановлении уходит в `failed`, поэтому его
стоит прочитать целиком:
```bash
journalctl -u 'hy2xs-fw-rollback-*' --no-pager
```
Резервные копии не удаляются, пока восстановление не подтверждено, и переживают
долговечную запись `phase: installed`. Поэтому при разборе неудачи всегда
@@ -92,6 +115,8 @@ ls -la /etc/hy2xs/backups/ # копия состояния до пос
cat /etc/hy2xs/backups/*/manifest.json
```
Имя каталога совпадает с полем `op_id` в `/var/lib/hy2xs/install-state.json`.
Манифест прямо говорит, какие файлы существовали до операции, а какие нет:
запись `"present": false` означает, что откат обязан был файл **удалить**, а не
восстановить.
@@ -100,6 +125,47 @@ cat /etc/hy2xs/backups/*/manifest.json
проблему внутри отката: последняя — это информация о том, что осталось не
восстановленным, а не причина отказа.
## 8a. Одна операция за раз
`install`, `reconfigure`, `repair` и `doctor` сериализованы эксклюзивным
замком:
```text
/run/lock/hy2xs-orchestrator.lock
```
Вторая операция отказывает сразу и **до первой мутации** — до снятия резервной
копии, до записи конфигов, до firewall:
```text
another HY2XS operation is already in progress: reconfigure (pid 4242, started at …)
```
`doctor` тоже берёт замок: диагностика в середине транзакции описывает
промежуточное состояние сервера и выдаёт бессмысленные ошибки по временным
несоответствиям.
`status` и `diagnostics collect` замок **не** берут — они нужны в том числе во
время долгой операции, — но сообщают о ней:
```bash
hy2xs-orchestrator status --package-dir /usr/local/lib/hy2xs/package
# "operation_in_progress": "reconfigure (pid 4242, started at …)"
```
Замок снимается при любом завершении держателя: штатном, по `Ctrl+C`, по
SIGTERM от systemd и при обрыве SSH. Если процесс был убит `kill -9`, следующая
операция обнаружит мёртвого держателя и переиспользует замок сама:
```text
operation lock … is held by install (pid 1234), which is no longer running; reclaiming it
```
`/run/lock` — это tmpfs, поэтому перезагрузка снимает замок в любом случае.
Удалять файл руками нужно только если в нём оказалось непонятное содержимое:
такой замок сознательно не переиспользуется автоматически — непонятый файл не
доказывает, что операции нет.
## 9. Reconfigure flow
```bash
+17 -2
View File
@@ -152,9 +152,17 @@ sudo ./purge-v0.sh --apply --yes-i-know
`/var/lib/hysteria` (там остаётся ACME-состояние и сертификаты Hysteria),
`/usr/local/lib/hy2xs` и symlink `/usr/local/bin/hy2xs-orchestrator`;
5. удаляет `/usr/local/bin/hysteria`;
6. удаляет фрагмент `/etc/nftables.d/hy2xs.nft` и строку `include` для него из
6. удаляет артефакты незавершённой операции в `/run`: каталог отката
`/run/hy2xs` (прежние `nftables.conf`, `hy2xs.nft` и маркер срабатывания
guard) и замок операций `/run/lock/hy2xs-orchestrator.lock`, который может
пережить убитый `kill -9` процесс оркестратора и не дать запуститься
следующей установке. `/run` — tmpfs, и перезагрузка убрала бы оба, но
очистка не имеет права требовать перезагрузки;
7. удаляет `*.candidate` firewall — они остаются, если операция упала между
`nft -c` и подстановкой файла в production-путь;
8. удаляет фрагмент `/etc/nftables.d/hy2xs.nft` и строку `include` для него из
`/etc/nftables.conf`, после чего перезагружает ruleset;
7. проверяет чистоту хоста.
9. проверяет чистоту хоста.
Список удаляемых путей и список legacy-маркеров clean-host контракта описывают
одну и ту же границу: расхождение между ними ловится приёмкой сборки. Иначе
@@ -194,6 +202,13 @@ sudo rm -rf /etc/hy2xs /etc/hysteria /var/lib/hy2xs /var/lib/hy2xs-admin \
/usr/local/lib/hy2xs /usr/local/h-ui
sudo rm -f /usr/local/bin/hysteria /usr/local/bin/hy2xs-orchestrator
# артефакты незавершённой операции в /run (tmpfs)
sudo rm -rf /run/hy2xs
sudo rm -f /run/lock/hy2xs-orchestrator.lock
# candidate-файлы firewall от операции, упавшей до подстановки
sudo rm -f /etc/nftables.conf.candidate /etc/nftables.d/hy2xs.nft.candidate
sudo rm -f /etc/nftables.d/hy2xs.nft
sudo sed -i '/nftables.d\/hy2xs.nft/d' /etc/nftables.conf
sudo nft -c -f /etc/nftables.conf && sudo nft -f /etc/nftables.conf