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:
@@ -32,6 +32,97 @@ Hardening-проход перед релизом `1.0.0`. Основная те
|
|||||||
запускался, но отдельные его шаги могли молча не выполнить восстановление,
|
запускался, но отдельные его шаги могли молча не выполнить восстановление,
|
||||||
отчитаться успехом и уничтожить резервную копию.
|
отчитаться успехом и уничтожить резервную копию.
|
||||||
|
|
||||||
|
Шестой проход — управление самой транзакцией, а не копированием файлов.
|
||||||
|
Предыдущие проходы сделали надёжными шаги операции; здесь закрываются два
|
||||||
|
допущения, на которых держалась операция целиком: что снятие защиты от отката
|
||||||
|
действительно произошло и что операция на сервере ровно одна.
|
||||||
|
|
||||||
|
### Исправлено — границы транзакции
|
||||||
|
|
||||||
|
- **Снятие rollback guard было утверждением, а не фактом.** Порядок фиксации
|
||||||
|
успеха выглядел так:
|
||||||
|
|
||||||
|
```text
|
||||||
|
systemctl stop <unit>.timer <unit>.service || true
|
||||||
|
-> "firewall rollback timer disarmed"
|
||||||
|
-> phase=installed
|
||||||
|
```
|
||||||
|
|
||||||
|
Между «мы думаем, что guard снят» и «guard действительно снят» не было ни
|
||||||
|
одной проверки: `|| true` стирал код возврата, и взведённый таймер мог
|
||||||
|
вернуть прежний firewall уже ПОСЛЕ долговечной записи успеха. Просто убрать
|
||||||
|
`|| true` было нельзя — для транзиентного юнита, уже убранного systemd,
|
||||||
|
`systemctl stop` возвращает 5, и этот исход неотличим от успеха.
|
||||||
|
|
||||||
|
Введён маркер `/run/hy2xs/rollback/<op-id>/auto-rollback-fired`, который
|
||||||
|
rollback-скрипт создаёт первым действием. Снятие guard стало доказательством:
|
||||||
|
маркер отсутствует, `ActiveState` обоих юнитов равен `inactive`, и только
|
||||||
|
после этого записывается `phase: installed`.
|
||||||
|
|
||||||
|
- **Автоматический откат мог сработать во время успешного smoke, и операция
|
||||||
|
этого не замечала.** Окно guard — 45 секунд — заведомо короче худшего случая
|
||||||
|
smoke, а единственной проверкой firewall в smoke был `nft -c`: разбор
|
||||||
|
текущего файла, каким бы он ни был. Откатившийся прежний ruleset проходил её
|
||||||
|
зелёным, и сервер объявлялся успешно настроенным с **предыдущим** firewall —
|
||||||
|
особенно дорого при смене порта Hysteria, SSH или ACME.
|
||||||
|
|
||||||
|
Лечится не увеличением окна: сработавший guard теперь запрещает фиксацию
|
||||||
|
успеха независимо от результата smoke и даёт собственную причину отказа
|
||||||
|
`firewall_guard_fired`. Дополнительно smoke сверяет эффективный firewall с
|
||||||
|
конфигурацией операции — фрагмент правил, принадлежность entrypoint и
|
||||||
|
фактически загруженную таблицу `inet hy2xs`.
|
||||||
|
|
||||||
|
- **У оркестратора не было блокировки операций.** Ни `flock`, ни mutex, ни
|
||||||
|
lockfile — при том что вся архитектура отката опиралась на невысказанное
|
||||||
|
допущение об одной операции за раз. `install-state.json` замком не является:
|
||||||
|
это запись о состоянии, а не право на изменение. Два одновременных
|
||||||
|
`reconfigure` доходили до конца каждый по-своему, и уникальные `op-id` не
|
||||||
|
спасали — они разделяют резервные копии, но production paths общие. Дальше
|
||||||
|
любая из операций могла упасть и «восстановить» состояние поверх изменений
|
||||||
|
другой, отчитавшись полным успехом.
|
||||||
|
|
||||||
|
Введён эксклюзивный замок `/run/lock/hy2xs-orchestrator.lock`.
|
||||||
|
`install`/`reconfigure`/`repair`/`doctor` берут его и отказывают **до первой
|
||||||
|
мутации**; `status`/`diagnostics` не берут, но сообщают об идущей операции;
|
||||||
|
`preflight-install` отказывает до собственных проверок. Замок снимается при
|
||||||
|
любом завершении держателя, включая обрыв SSH.
|
||||||
|
|
||||||
|
- **Автоматический откат маскировал собственные ошибки.** Внутри `systemd-run`
|
||||||
|
оставались `cp ... || true` и `nft -f ... || true`, поэтому при частичном
|
||||||
|
восстановлении юнит завершался кодом 0 — ровно в сценарии, где guard является
|
||||||
|
последней линией защиты от потери SSH. Скрипт переписан: независимые стадии,
|
||||||
|
накопление кода возврата, `failed` с диагностикой в journal.
|
||||||
|
|
||||||
|
- **Откат не восстанавливал состояние `nftables.service`.** `applyFirewall`
|
||||||
|
выполняет `systemctl enable --now nftables`, но копия хранила только файлы
|
||||||
|
правил. После отката неудачной первой установки сервис оставался включённым в
|
||||||
|
автозапуск, хотя до неё был выключен. Состояние снимается вместе с файлами и
|
||||||
|
восстанавливается стадиями, идущими до применения ruleset: у
|
||||||
|
`nftables.service` `ExecStop=nft flush ruleset`, и обратный порядок стёр бы
|
||||||
|
восстановленные правила.
|
||||||
|
|
||||||
|
- **`/etc/nftables.conf.candidate` не удалялся никогда.** Успешная установка
|
||||||
|
оставляла его на сервере навсегда. Candidate-файлы убираются после успеха и
|
||||||
|
best-effort при откате; `purge-v0.sh` тоже их знает.
|
||||||
|
|
||||||
|
- **Скрипт автоотката собирался однострочником внутри `sh -c '...'`.**
|
||||||
|
Интерполяции проходили через shell-квотирование и подставлялись внутрь уже
|
||||||
|
закавыченной строки: корректность держалась на склейке соседних кавычек и на
|
||||||
|
том, что op-id не содержит пробелов. Скрипт вынесен в отдельную чистую
|
||||||
|
функцию, ключ операции проверяется, а результат покрыт тестом и разбирается
|
||||||
|
настоящим shell-парсером.
|
||||||
|
|
||||||
|
- **Ключ операции считался в двух местах и разошёлся.** `install` писал в
|
||||||
|
маркер сырой ISO-timestamp с двоеточиями, тогда как каталог отката назывался
|
||||||
|
санитизированным ключом: путь `/run/hy2xs/rollback/<op_id>`, который runbook
|
||||||
|
предлагает открыть, на сервере не существовал.
|
||||||
|
|
||||||
|
- **Стадии восстановления `reconfigure` были независимы по группе, а не по
|
||||||
|
файлу.** Отказ `cp` для `hy2xs-admin.service` отменял восстановление
|
||||||
|
`hysteria-server.service`: внешняя стадия честно попадала в список
|
||||||
|
отказавших, но принцип «восстановить максимум» на уровне файлов не
|
||||||
|
выполнялся.
|
||||||
|
|
||||||
### Исправлено — целостность отката
|
### Исправлено — целостность отката
|
||||||
|
|
||||||
- **Данные для отката уничтожались до фиксации успеха.** Успешный install
|
- **Данные для отката уничтожались до фиксации успеха.** Успешный install
|
||||||
|
|||||||
@@ -671,6 +671,28 @@ hy2xs-orchestrator status \
|
|||||||
|
|
||||||
При `managed` и `takeover` генерируется nftables‑конфигурация с default drop policy, разрешением loopback, established/related, SSH‑порта, ACME challenge‑порта, Hysteria2 UDP‑порта и ICMP echo‑request.
|
При `managed` и `takeover` генерируется nftables‑конфигурация с default drop policy, разрешением loopback, established/related, SSH‑порта, ACME challenge‑порта, Hysteria2 UDP‑порта и ICMP echo‑request.
|
||||||
|
|
||||||
|
### Защита от потери доступа при смене firewall
|
||||||
|
|
||||||
|
При `HY2XS_FIREWALL_STAGED_APPLY=true` (значение по умолчанию) перед применением новых правил HY2XS взводит rollback guard — транзиентный systemd‑юнит с окном 45 секунд. Если операция не снимет его вовремя, guard вернёт прежний firewall, и SSH останется доступным.
|
||||||
|
|
||||||
|
Окно намеренно короткое и **не** обязано покрывать smoke‑checks: на медленном сервере они идут дольше. Вместо этого guard оставляет за собой факт срабатывания в `/run/hy2xs/rollback/<op-id>/auto-rollback-fired`, и операция не имеет права объявить себя успешной, если этот файл появился, — сервер в такой момент работает на прежнем firewall, а не на том, который она сгенерировала. Установка завершится отказом с `phase: firewall_guard_fired`, и её нужно повторить после устранения причины медленного прохода.
|
||||||
|
|
||||||
|
Дополнительно smoke сверяет, что действующий firewall — именно тот, который сгенерирован для текущей конфигурации: разбора `/etc/nftables.conf` для этого недостаточно, потому что прежний ruleset тоже валиден.
|
||||||
|
|
||||||
|
## Одна операция за раз
|
||||||
|
|
||||||
|
`install`, `reconfigure`, `repair` и `doctor` сериализованы эксклюзивным замком `/run/lock/hy2xs-orchestrator.lock`. Вторая операция отказывает сразу и **до первой мутации**:
|
||||||
|
|
||||||
|
```text
|
||||||
|
another HY2XS operation is already in progress: reconfigure (pid 4242, started at …)
|
||||||
|
```
|
||||||
|
|
||||||
|
Это не перестраховка: конфиги, unit‑файлы, `/etc/nftables.conf` и маркер установки — общие, и две одновременные операции записывают их поверх друг друга, после чего откат одной «восстанавливает» состояние поверх изменений другой.
|
||||||
|
|
||||||
|
`status` и `diagnostics collect` замок не берут — они нужны в том числе во время долгой операции, — но сообщают о ней в своём выводе.
|
||||||
|
|
||||||
|
Замок снимается сам при любом завершении держателя, включая `Ctrl+C`, SIGTERM и обрыв SSH. Если процесс был убит `kill -9`, следующая операция обнаружит мёртвого держателя и переиспользует замок самостоятельно.
|
||||||
|
|
||||||
## Реконфигурация
|
## Реконфигурация
|
||||||
|
|
||||||
После изменения `/etc/hy2xs/hy2xs.env` сначала выполните dry‑run:
|
После изменения `/etc/hy2xs/hy2xs.env` сначала выполните dry‑run:
|
||||||
|
|||||||
@@ -82,12 +82,78 @@ IPv4-only policy:
|
|||||||
|
|
||||||
## Порядок применения
|
## Порядок применения
|
||||||
|
|
||||||
1. Подготовить candidate-файл (`/etc/nftables.d/hy2xs.nft`).
|
1. Снять резервную копию `/etc/nftables.conf`, `/etc/nftables.d/hy2xs.nft` и
|
||||||
2. Проверить `nft -c -f`.
|
состояния юнита `nftables.service` в `/run/hy2xs/rollback/<op-id>/` и
|
||||||
3. Создать rollback timer.
|
**доказать**, что копия создана. Отказ здесь останавливает операцию до
|
||||||
4. Применить candidate и проверить SSH/Hysteria/UI.
|
первой мутации.
|
||||||
5. При успехе отменить rollback timer.
|
2. Подготовить candidate-файлы и проверить их `nft -c -f`.
|
||||||
6. При провале — rollback.
|
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 с конфигурацией.
|
||||||
|
|
||||||
## Что не делаем
|
## Что не делаем
|
||||||
|
|
||||||
|
|||||||
@@ -245,6 +245,59 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
|
|||||||
- успешный smoke фиксируется отдельной фазой до снятия таймера;
|
- успешный smoke фиксируется отдельной фазой до снятия таймера;
|
||||||
- уборка после точки фиксации выполняется best-effort.
|
- уборка после точки фиксации выполняется 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)
|
## A5b. Долговечная запись маркера (unit)
|
||||||
|
|
||||||
`orchestrator/test/atomic-write.test.ts`:
|
`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`, и в нём перечислены все семь путей, включая отсутствовавшие с
|
`manifest.json`, и в нём перечислены все семь путей, включая отсутствовавшие с
|
||||||
`"present": false`.
|
`"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. Проход установки не спотыкается о собственный маркер
|
## D1a. Проход установки не спотыкается о собственный маркер
|
||||||
|
|
||||||
Проверяется на чистом хосте, обычной успешной установкой.
|
Проверяется на чистом хосте, обычной успешной установкой.
|
||||||
@@ -1031,8 +1189,20 @@ hy2xs-orchestrator doctor
|
|||||||
- при `HY2XS_FIREWALL_MODE=managed` install/reconfigure блокируются;
|
- при `HY2XS_FIREWALL_MODE=managed` install/reconfigure блокируются;
|
||||||
- при `HY2XS_FIREWALL_MODE=takeover` создаются backup/rollback guard и apply проходит.
|
- при `HY2XS_FIREWALL_MODE=takeover` создаются backup/rollback guard и apply проходит.
|
||||||
|
|
||||||
4. **Rollback guard cleanup**:
|
4. **Rollback guard cleanup** (сценарий D1g):
|
||||||
- после успешного apply/smoke не остаются `hy2xs-fw-rollback-*.timer/.service`.
|
- после успешного 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**:
|
5. **Partial install + repair**:
|
||||||
- состояние `install-state` фиксирует промежуточную фазу;
|
- состояние `install-state` фиксирует промежуточную фазу;
|
||||||
@@ -1118,3 +1288,10 @@ hy2xs-orchestrator doctor
|
|||||||
58. копия привязана к операции: откат восстанавливает состояние непосредственно перед текущим проходом, а не сохранённое предыдущим
|
58. копия привязана к операции: откат восстанавливает состояние непосредственно перед текущим проходом, а не сохранённое предыдущим
|
||||||
59. ни одна команда отката не глушит свой код возврата; отказавшие стадии перечисляются, а артефакты восстановления удаляются только после подтверждённого успеха
|
59. ни одна команда отката не глушит свой код возврата; отказавшие стадии перечисляются, а артефакты восстановления удаляются только после подтверждённого успеха
|
||||||
60. `doctor` не выполняет проб, изменяющих данные в админке: авторизация действующим паролем пира ограничена режимом `install`
|
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; замок мёртвого держателя переиспользуется безопасно
|
||||||
|
|||||||
@@ -156,6 +156,72 @@ hy2xs-orchestrator repair \
|
|||||||
Флаг обязателен и осознан: без него `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 не работает
|
### Сервер установился, но UI не работает
|
||||||
Проверить:
|
Проверить:
|
||||||
- разложился ли bundled UI
|
- разложился ли bundled UI
|
||||||
|
|||||||
@@ -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 finished with N failed stage(s); manual recovery may be required` | итог: перечисленные стадии требуют ручной проверки |
|
||||||
| `rollback completed: N stage(s) succeeded` | восстановление отработало полностью |
|
| `rollback completed: N stage(s) succeeded` | восстановление отработало полностью |
|
||||||
| `manual recovery data preserved at /run/hy2xs/rollback/<op>` | firewall восстановлен не полностью; прежние `nftables.conf` и `hy2xs.nft` лежат по этому пути |
|
| `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`. Поэтому при разборе неудачи всегда
|
долговечную запись `phase: installed`. Поэтому при разборе неудачи всегда
|
||||||
@@ -92,6 +115,8 @@ ls -la /etc/hy2xs/backups/ # копия состояния до пос
|
|||||||
cat /etc/hy2xs/backups/*/manifest.json
|
cat /etc/hy2xs/backups/*/manifest.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Имя каталога совпадает с полем `op_id` в `/var/lib/hy2xs/install-state.json`.
|
||||||
|
|
||||||
Манифест прямо говорит, какие файлы существовали до операции, а какие нет:
|
Манифест прямо говорит, какие файлы существовали до операции, а какие нет:
|
||||||
запись `"present": false` означает, что откат обязан был файл **удалить**, а не
|
запись `"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
|
## 9. Reconfigure flow
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -152,9 +152,17 @@ sudo ./purge-v0.sh --apply --yes-i-know
|
|||||||
`/var/lib/hysteria` (там остаётся ACME-состояние и сертификаты Hysteria),
|
`/var/lib/hysteria` (там остаётся ACME-состояние и сертификаты Hysteria),
|
||||||
`/usr/local/lib/hy2xs` и symlink `/usr/local/bin/hy2xs-orchestrator`;
|
`/usr/local/lib/hy2xs` и symlink `/usr/local/bin/hy2xs-orchestrator`;
|
||||||
5. удаляет `/usr/local/bin/hysteria`;
|
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;
|
`/etc/nftables.conf`, после чего перезагружает ruleset;
|
||||||
7. проверяет чистоту хоста.
|
9. проверяет чистоту хоста.
|
||||||
|
|
||||||
Список удаляемых путей и список legacy-маркеров clean-host контракта описывают
|
Список удаляемых путей и список 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
|
/usr/local/lib/hy2xs /usr/local/h-ui
|
||||||
sudo rm -f /usr/local/bin/hysteria /usr/local/bin/hy2xs-orchestrator
|
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 rm -f /etc/nftables.d/hy2xs.nft
|
||||||
sudo sed -i '/nftables.d\/hy2xs.nft/d' /etc/nftables.conf
|
sudo sed -i '/nftables.d\/hy2xs.nft/d' /etc/nftables.conf
|
||||||
sudo nft -c -f /etc/nftables.conf && sudo nft -f /etc/nftables.conf
|
sudo nft -c -f /etc/nftables.conf && sudo nft -f /etc/nftables.conf
|
||||||
|
|||||||
@@ -87,6 +87,20 @@ PATHS=(
|
|||||||
/usr/local/lib/hy2xs
|
/usr/local/lib/hy2xs
|
||||||
/usr/local/h-ui
|
/usr/local/h-ui
|
||||||
/usr/local/bin/hy2xs-orchestrator
|
/usr/local/bin/hy2xs-orchestrator
|
||||||
|
# Артефакты незавершённой операции в /run. Каталог отката хранит прежние
|
||||||
|
# nftables.conf и hy2xs.nft вместе с маркером срабатывания guard, а замок
|
||||||
|
# операций способен пережить убитый `kill -9` процесс оркестратора и не даст
|
||||||
|
# запуститься следующей установке. /run — tmpfs, и перезагрузка убрала бы оба,
|
||||||
|
# но очистка не имеет права требовать перезагрузки.
|
||||||
|
/run/hy2xs
|
||||||
|
/run/lock/hy2xs-orchestrator.lock
|
||||||
|
)
|
||||||
|
|
||||||
|
# Candidate-файлы firewall: остаются на диске, если операция упала между
|
||||||
|
# проверкой `nft -c` и подстановкой в production-путь.
|
||||||
|
CANDIDATES=(
|
||||||
|
/etc/nftables.conf.candidate
|
||||||
|
/etc/nftables.d/hy2xs.nft.candidate
|
||||||
)
|
)
|
||||||
|
|
||||||
NFT_FRAGMENT=/etc/nftables.d/hy2xs.nft
|
NFT_FRAGMENT=/etc/nftables.d/hy2xs.nft
|
||||||
@@ -119,7 +133,7 @@ show_plan() {
|
|||||||
|
|
||||||
log "будут удалены пути:"
|
log "будут удалены пути:"
|
||||||
local path
|
local path
|
||||||
for path in "${UNIT_FILES[@]}" "${PATHS[@]}" "$NFT_FRAGMENT"; do
|
for path in "${UNIT_FILES[@]}" "${PATHS[@]}" "${CANDIDATES[@]}" "$NFT_FRAGMENT"; do
|
||||||
if [ -e "$path" ]; then
|
if [ -e "$path" ]; then
|
||||||
printf ' - %s (существует)\n' "$path"
|
printf ' - %s (существует)\n' "$path"
|
||||||
else
|
else
|
||||||
@@ -167,6 +181,10 @@ remove_paths() {
|
|||||||
rm -rf "$path"
|
rm -rf "$path"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
for path in "${CANDIDATES[@]}"; do
|
||||||
|
rm -f "$path"
|
||||||
|
done
|
||||||
|
|
||||||
rm -f /usr/local/bin/hysteria
|
rm -f /usr/local/bin/hysteria
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user