docs: clean-install-only, versions.env и очистка предыдущего поколения

Новый docs/14-legacy-cleanup.md: как выглядит отказ установщика, полный
список маркеров чужой установки, что сохранить перед очисткой, работа
purge-v0.sh, ручная процедура и отдельно - случай незавершённой
установки текущего поколения, где нужен repair, а не очистка.

Обновлено под фактическое поведение:

- README и package/docs: установка описана как две фазы, PHASE 0 ничего
  не меняет; добавлен troubleshooting по отказу clean-host; версии
  toolchain больше не передаются через окружение;
- 02-build-layer: раздел про versions.env (что в нём есть и чего нет и
  почему), verify_versions_contract, проверка происхождения артефакта
  по upstream hashes.txt;
- 08-orchestrator-spec: двухфазный контракт, read-only guard,
  идентификация поколения в install-state, ownership-aware rollback,
  расширенная семантическая проверка конфига, структурная редакция;
- 04-admin-panel: таблица удалённых маршрутов и почему они удалены, а
  не оставлены заглушками; сужена формулировка гарантии санитайза;
- 11-testing: новые unit-наборы, полный список инвариантов конфига,
  раздел про одну реализацию URI вместо двух, сценарий проверки
  границы установки на живом сервере;
- 12-operations и 13-runbook: диагностика отказов по поколению,
  поведение diagnostics-бандла;
- tools/build/README: контракт версий, обе суммы Bun, hashes.txt.

CHANGELOG: раздел Unreleased с разбором каждого исправленного дефекта.
This commit is contained in:
2026-08-27 12:16:38 +05:00
parent 42db78c6a0
commit 3a4ce9c751
12 changed files with 1117 additions and 99 deletions
+122 -4
View File
@@ -13,7 +13,7 @@ cd orchestrator && bun install --frozen-lockfile && bun run check && bun test
# Тесты и статический анализ HY2XS admin
cd apps && go vet ./... && go test ./...
# Полный E2E с реальным клиентом Hysteria (Debian 13 amd64)
# Полный E2E с реальным клиентом Hysteria (Debian 13 amd64; нужен Go)
HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
# Production-сборка: прогоняет тесты, резолвер и compatibility gate
@@ -98,6 +98,14 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
| неположительный/нецелый `min` | отклонено |
| пустой obfs-пароль | автогенерация, а не пустое значение в конфиге |
| `HY2XS_CONFIG_SCHEMA_VERSION=1` | отклонено с указанием на чистую установку |
| `HY2XS_CONFIG_SCHEMA_VERSION` отсутствует | отклонено как legacy-конфигурация |
| `HY2XS_CONFIG_SCHEMA_VERSION=` (пусто) | отклонено как legacy-конфигурация |
| `HY2XS_CONFIG_SCHEMA_VERSION=2` | принято |
Отсутствие маркера схемы отклоняется намеренно: до v1 этого поля не
существовало, поэтому именно пустое значение — самый вероятный признак
конфигурации 0.x. Любой fallback здесь молча превращал бы legacy-конфиг в
якобы валидный.
Отдельно — round-trip `parse(render(config)) == config`. Этот тест ловит класс ошибок «в рендер runtime-конфига попал литерал вместо значения из конфигурации».
@@ -110,6 +118,63 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
- пароль с пробелами и спецсимволами экранируется;
- YAML-инъекция через пароль отклоняется даже в обход env-валидации.
## A5. Граница установки и поколение (unit)
`orchestrator/test/clean-host.test.ts`:
- чистый хост проходит;
- **каждый** маркер по отдельности останавливает установку;
- список покрывает состояние, юниты, бинарник Hysteria и наследие 0.x;
- пути из конфигурации (`HY2XS_INSTALL_DIR`, `HY2XS_DATA_DIR`) попадают в
список, а не только значения по умолчанию;
- `/usr/local/lib/hy2xs/package` — маркер в PHASE 0, но не в PHASE 1: между
фазами его создаёт сам `install.sh`;
- сообщение перечисляет найденные маркеры и говорит, что хост не изменён.
`orchestrator/test/install-boundary.test.ts`:
- под read-only guard недоступны `writeText`, `writeTextAtomic`, `runVisible`,
`runHidden`, `runRawVisible`;
- классификация отказа зависит от ownership-флагов и фазы, а **не** от текста
ошибки;
- пока операция ничего не применила, отказ — `fatal_pre_apply`.
`orchestrator/test/install-state.test.ts`:
- маркер текущего поколения принимается;
- маркер без полей поколения отклоняется, **несмотря на `installed: true`**;
- чужой `product`, `release_line` или `config_schema_version` отклоняются;
- записываемый маркер всегда несёт идентификацию поколения;
- незавершённая установка подсказывает `repair --allow-partial-state`.
## A6. Редактирование секретов (unit)
`orchestrator/test/redaction.test.ts`:
- machine token не переживает редакцию серверного конфига — регрессия на
построчное правило `auth:`, оставлявшее нетронутым `auth.http.url`;
- obfs-пароль не переживает редакцию;
- результат остаётся валидным YAML;
- несекретные поля сохраняются: диагностика должна оставаться полезной;
- неизвестное поле с секретоподобным именем вырезается;
- `acme.dns.config` вырезается целиком;
- невалидный YAML не роняет редакцию и всё равно чистится;
- секрет внутри URL-значения в env вырезается, даже если имя ключа несекретное
(`HY2_AUTH_URL`).
## A7. Контракт версий (build)
Шаг `verify_versions_contract` (`tools/build/lib/versions.sh`) роняет сборку до
создания tarball при рассинхроне `versions.env` с `PACKAGE_VERSION`,
`packageManager` обоих `package.json`, схемой в `package/config/hy2xs.env`,
константами, скомпилированными в оркестратор, директивой `go` в `apps/go.mod`,
metadata пакета и версией, которую сообщает собранный `hy2xs-admin`.
Разбор upstream `hashes.txt` покрыт `orchestrator/test/hysteria-release.test.ts`:
реальный формат релиза, отсутствие путаницы `hysteria-linux-amd64` с
`hysteria-linux-amd64-avx`, форма `sha256:<hex>`, верхний регистр,
противоречивые записи, отсутствие нужной строки.
## B. Target install tests
### На чистом Debian 13 проверяем
@@ -174,7 +239,8 @@ congestion:
quic:
disableStatelessReset == false
окна и таймауты == baseline
окна, maxIncomingStreams, disablePathMTUDiscovery == baseline
maxIdleTimeout == 30s
trafficStats:
listen == runtime env
@@ -182,13 +248,27 @@ trafficStats:
auth:
type == http
url содержит machine access_token
url == http://127.0.0.1:<UI_PORT>/hui/hysteria2/auth?access_token=<machine token>
insecure == (tlsMode == self_signed_dev)
TLS:
acme-режим не содержит секции tls
acme: type/email/ca/dir/listenHost/первый домен == профиль
file-режим не содержит секции acme
верхний уровень:
нет секций вне production-профиля
```
`maxIdleTimeout` присутствовал в профиле, но не проверялся: конфиг с уехавшим
idle timeout проходил семантическую проверку. Точно так же `auth.http.url`
раньше сверялся только на наличие подстроки `access_token=`, из-за чего
уехавший порт или путь остались бы незамеченными — а это единственный канал
допуска пиров.
Сообщение об ошибке для `auth.http.url` намеренно не печатает сам токен: текст
уходит в логи и в diagnostics-бандл. Это закреплено отдельным тестом.
## C2. End-to-end с реальным клиентом
`tools/test/e2e-hysteria.sh`, отдельно для Gecko и Salamander:
@@ -198,7 +278,7 @@ TLS:
3. handshake с обфускацией;
4. HTTP auth HY2XS: разрешённый пир принят;
5. HTTP auth HY2XS: неразрешённый пир отклонён;
6. клиент подключается **именно по сгенерированной `hysteria2://` ссылке**;
6. клиент подключается **именно по ссылке, которую выдаёт production-код**;
7. TCP forwarding;
8. UDP forwarding;
9. `trafficStats` с валидным secret;
@@ -209,6 +289,28 @@ TLS:
Пункт 6 — тот самый, который ловит класс ошибок, неизбежный при наивном включении Gecko: сервер работает, ссылка формально валидна, а клиент по ней не подключается.
### Одна реализация URI, а не две
Ссылка берётся из production-генератора через `apps/tools/share-uri`, который
вызывает ту же `service.BuildHysteria2ShareURI`, что и панель.
Раньше внутри e2e жила **вторая** реализация URI на bash. Go-юнит-тесты
проверяли production-генератор, e2e проверял свою функцию — и дрейф любой из
них оставлял обе группы тестов зелёными.
Единственное расхождение с пользовательской ссылкой — `insecure=1`: e2e
работает на самоподписанном сертификате. Это расхождение ограничено с двух
сторон:
- e2e отдельно печатает и проверяет **production-вариант** ссылки
(`insecure=0`, корректные `obfs` и `sni`);
- Go-тест `TestBuildHysteria2ShareURI_InsecureDiffersOnlyInThatParam`
доказывает, что кроме этого параметра ссылки совпадают побайтово;
- Go-тест `TestBuildHysteria2Url_ProductionPathNeverDisablesVerification`
фиксирует, что production-путь никогда не передаёт `insecure=1`.
Для запуска e2e нужен Go (`GO_BIN`).
## C3. Share URI (unit)
`apps/service/hysteria2_api_test.go`:
@@ -232,6 +334,22 @@ TLS:
- вырезается **неизвестное** поле с секретным именем;
- пути к файлам (`tls.key`, `ech.keyPath`, `clientCA`) остаются видимыми.
## D0. Граница установки на живом сервере
Проверяется на хосте, где уже стоит предыдущая установка:
1. `install.sh` завершается отказом на PHASE 0;
2. `/usr/local/lib/hy2xs` **не создан и не изменён**;
3. `/var/lib/hy2xs/install-state.json` не перезаписан;
4. `hysteria-server` и `hy2xs-admin` остались `active`;
5. в тексте отказа перечислены найденные маркеры и указан
`docs/14-legacy-cleanup.md`;
6. после `tools/legacy/purge-v0.sh --apply --yes-i-know` установка проходит.
Пункты 2–4 — прямая регрессия: прежний установщик успевал переписать
`/usr/local/lib/hy2xs` и `install-state.json`, а затем откатом останавливал и
выключал работающие службы старой установки.
## D. Negative tests
1. не Debian 13