fix(v1): сделать отзыв доступа, бэкап и диагностику соответствующими своим именам

Проход по операциям, которые делают не то, что обещает их имя.

P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать
пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS
продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор
удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же
секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config:
«создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной
транзакцией.

P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и
отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь
недоступный секрет любого пира отклоняет весь запрос с указанием имени.

P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет,
если оно не начиналось с v1: — остаток поколения с открытыми секретами.

P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на
проблему обрывала все живые соединения.

P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в
/etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine
auth переставал совпадать.

P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести
артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и
документации, отсутствие проверяется приёмкой.

P2. UPDATE по отсутствующей строке config считался успехом, и cron
перепланировался при несохранённом значении. Решение по RowsAffected.

P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения
ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage.

P2. Удалены алиасы /:id/client-url и /:id/qr.

Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go —
языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют
среду с versions.env.
This commit is contained in:
2026-08-30 06:48:50 +05:00
parent e30fdaa004
commit cf094f6e6f
33 changed files with 1976 additions and 154 deletions
+55
View File
@@ -369,6 +369,52 @@ wildcard-маршрутом фронтенда или дублирующая р
- удаление **разрешено**: это осознанное действие оператора, и расхождения
между базой и `bootstrap-admin.secret` оно не создаёт.
## A9e. Жизненный цикл пира установщика (unit, настоящая SQLite)
`apps/dao/bootstrap_peer_test.go` — проверяется не функция, а поведение сервиса
при перезапуске: дефект, ради которого написан этот файл, проявлялся только на
ВТОРОМ запуске, поэтому каждый тест прогоняет полную последовательность
`InitSqlAt` дважды на одной базе.
- первый запуск создаёт пира и выставляет отметку `BOOTSTRAP_PEER_SEEDED`;
- обычный перезапуск не пересоздаёт пира и не плодит дублей (`id` тот же,
запись ровно одна);
- **удаление переживает перезапуск**: после `DELETE` и рестарта пир не
возвращается, хотя `HY2XS_ADMIN_CON_PASS` остаётся в окружении;
- то же после трёх перезапусков подряд;
- отключённый пир сохраняет `disabled = 1` и свой `secret_digest`;
- отметка и пир пишутся одной транзакцией: при конфликте `UNIQUE(name)` внутри
транзакции отметка не остаётся выставленной;
- отсутствие `HY2XS_ADMIN_CON_PASS` на чистой базе — отказ старта;
- перезапуск установленного сервиса без этой переменной проходит штатно;
- `HYSTERIA2_TRAFFIC_STATS_SECRET`: пустой env при пустой базе — отказ старта,
сгенерированного токена в базе не появляется; токен, уже согласованный
ранее, принимается без переменной.
## A9f. Резервная копия пиров (unit)
`apps/service/peer_export_backup_test.go`:
- `includeSecrets=true` на исправных данных отдаёт секрет каждого пира;
- нерасшифровываемый секрет хотя бы одного пира отклоняет **весь** запрос,
сообщение называет пира, частичное содержимое не возвращается;
- пир вовсе без шифртекста — тот же отказ;
- `includeSecrets=false` повреждённых данных не замечает и пустой `secret`
отдаёт штатно: это и есть смысл безопасного режима.
## A9g. Слой данных: «нет записи» против «база не ответила» (unit)
`apps/dao/config_test.go`:
- `UpdateConfig` по отсутствующей строке — **отказ**, а не тихий успех: UPDATE
без совпавших строк не является ошибкой SQL, и раньше оператор получал
подтверждение изменения, которого не произошло, а планировщик тут же получал
новое расписание;
- `UpdateConfig` не создаёт строк: это работа `UpsertConfigValue`;
- транзакционная партия откатывается целиком, если одна из строк отсутствует;
- `GetConfig`/`GetPeer` возвращают `ErrConfigNotFound`/`ErrPeerNotFound`,
отличимые через `errors.Is` от `ErrStorage`.
## A10. Импорт пиров (unit)
`apps/service/peer_import_test.go`:
@@ -811,3 +857,12 @@ hy2xs-orchestrator doctor
32. документация нигде не утверждает, что Salamander — фиксированный инвариант
33. документация не фиксирует конкретный номер версии как «текущую версию», а объясняет latest-stable build policy
34. форма создания пира содержит примеры значений и пояснения для полей «Пир», «Комментарий» и «Секрет»
35. `hy2xs-orchestrator doctor` не перезапускает сервисы и не рвёт живые соединения
36. удаление `bootstrap-admin-peer` переживает `systemctl restart` и `reboot`: пир не воскресает
37. отключённый `bootstrap-admin-peer` остаётся отключённым после перезапуска
38. резервная копия с `includeSecrets=true` завершается ошибкой целиком, если секрет хотя бы одного пира недоступен
39. админка не генерирует `HYSTERIA2_TRAFFIC_STATS_SECRET` сама: пустой env при пустой базе — отказ старта
40. проверка зависимостей на уязвимости не имеет обходов ни в сборке, ни в документации
41. `apps/go.mod` объявляет `toolchain`, совпадающий с `GO_VERSION` из `versions.env`
42. `tools/dev/doctor.sh` / `doctor.ps1` показывают расхождение среды разработки с `versions.env`
43. маршруты-алиасы `/:id/client-url` и `/:id/qr` удалены и не входят в публичный API v1