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
+128 -2
View File
@@ -8,6 +8,127 @@
## [Unreleased] ## [Unreleased]
Hardening-проход перед релизом `1.0.0`. Основная тема — сделать политику
«только чистая установка» настоящим системным инвариантом, а не строчкой в
документации.
### Исправлено
- **Установщик мог повредить работающий сервер до того, как откажется его
трогать.** `install.sh` переписывал `/usr/local/lib/hy2xs`, раскладывал
runtime-пакет и перезаписывал `/var/lib/hy2xs/install-state.json`, и лишь
потом запускал clean-host preflight. При ошибочном запуске поверх старого
сервера rollback дополнительно выполнял `stop` и `disable` для работающих
`hysteria-server` и `hy2xs-admin`.
Установка разделена на две фазы с жёсткой границей: **PHASE 0 — read only**,
**PHASE 1 — mutation**. Read-only проверка выполняется новой командой
`hy2xs-orchestrator preflight-install` из распакованного архива, а граница
держится runtime-guard'ом, а не соглашением.
- **Отсутствие `HY2XS_CONFIG_SCHEMA_VERSION` считалось текущей схемой.** До v1
этого поля не существовало, поэтому именно пустое значение — самый вероятный
признак конфигурации `0.x`. Теперь оно отклоняется как legacy с указанием на
чистую установку. Тест, закреплявший прежнее поведение, инвертирован.
- **`reconfigure` и `repair` работали поверх любого маркера установки.**
Проверялся только флаг `installed`, который мог остаться и от `0.x`.
Маркер получил идентификацию поколения (`product`, `release_line`,
`config_schema_version`), и обе команды проверяют её до всего остального.
- **Классификация отказа шла по тексту сообщения об ошибке.** Ошибка
preflight со словом `nftables` классифицировалась как отказ firewall и
приводила к откату чужого ruleset. Теперь классификация опирается на то, что
операция реально успела применить. `systemctl stop/disable` выполняется
только для юнитов, развёрнутых текущей операцией, а `fatal_pre_apply` по
определению не выполняет системный откат и не собирает diagnostics-бандл.
- **Diagnostics-бандл уносил machine token наружу.** Построчное правило
редакции `auth:` подставляло маркер в заголовок mapping'а и оставляло
нетронутым вложенный `auth.http.url` с `access_token=<секрет>` — тем самым,
что открывает и trafficStats API, и auth-endpoint. Редакция YAML переписана
структурно; в env-файлах секреты теперь вырезаются и из URL-значений
(`HY2_AUTH_URL` не подходил ни под один маркер имени).
- **`quic.maxIdleTimeout` не проверялся** семантической проверкой конфига, хотя
присутствовал в production-профиле. Заодно `auth.http.url` теперь сверяется
целиком (host/port/path/token), а не по наличию подстроки `access_token=`;
добавлены проверки `auth.http.insecure`, полей ACME и отсутствия посторонних
секций верхнего уровня.
- **Версия админки разъехалась с версией пакета**: пакет `1.0.0` сообщал
`HY2XS admin version v0.0.22`. Константа заменена переменной, которую
проставляет сборка через ldflags из `versions.env`.
- **Кнопки в панели, которые всегда возвращали ошибку.** «Перезапустить панель»
и загрузка сертификатов обращались к заглушкам. Маршруты и UI удалены.
### Добавлено
- **`versions.env`** — единственный источник истины для контракта
«продукт / платформа / toolchain»: версия продукта, линия релиза, схема
конфигурации, целевая платформа, версии и контрольные суммы Go/Bun/Node/pnpm,
политика выбора Hysteria. Прикладные зависимости и конкретная версия
Hysteria сюда намеренно не переносятся: у них есть собственные lock-механизмы.
- **Шаг сборки `verify_versions_contract`.** Роняет сборку до создания tarball,
если разошлись `PACKAGE_VERSION`, `packageManager` в двух `package.json`,
схема в `package/config/hy2xs.env`, константы, скомпилированные в
оркестратор, директива `go` в `apps/go.mod`, metadata пакета или версия,
которую сообщает собранный `hy2xs-admin`.
- **Контрольные суммы toolchain в контракте**, включая **обе** сборки Bun
(`bun-linux-x64` и `bun-linux-x64-baseline`): артефакт выбирается по наличию
AVX2, поэтому одной суммы архитектурно недостаточно. Передавать суммы через
окружение больше не нужно — production-сборка запускается одной командой.
- **Проверка происхождения артефакта Hysteria.** Ожидаемый SHA-256 берётся из
upstream-ассета `hashes.txt` и сверяется со скачанным бинарником до записи в
HY2XS lock. Раньше сумма считалась локально от уже скачанного файла, то есть
была trust-on-first-use.
- **Полный clean-host контракт.** Список маркеров чужой установки расширен с
двух до четырнадцати: состояние, runtime-пакет, конфиги, бинарник Hysteria,
фрагмент nftables, systemd-юниты, база админки и наследие `0.x`. Пути
установки и данных берутся из конфигурации, а не захардкожены.
- **`tools/legacy/purge-v0.sh`** и [docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md) —
явная очистка сервера от предыдущего поколения. По умолчанию скрипт
показывает план и ничего не делает; выполнение требует
`--apply --yes-i-know`. Из установщика он не вызывается никогда: это вернуло
бы destructive migration logic в путь свежей установки.
- **Явный флаг `--allow-partial-state` для `repair`.** Прежде согласие на
работу поверх незавершённой установки подразумевалось молча.
### Изменено
- **E2E подключается по ссылке из production-кода.** Внутри
`tools/test/e2e-hysteria.sh` жила вторая реализация `hysteria2://` URI на
bash: дрейф любой из двух реализаций оставлял обе группы тестов зелёными.
Теперь ссылку выдаёт `service.BuildHysteria2ShareURI` через
`apps/tools/share-uri`. Единственное расхождение — `insecure=1` для
самоподписанного сертификата, и оно ограничено тестами с двух сторон.
- **Формулировка гарантии санитайза экспорта.** Вместо «любой будущий секрет
будет удалён» — «известные секреты и неизвестные поля с секретоподобным
именем». Список маркеров расширен (`apiKey`, `privateKey`, `authorization`,
`cookie`, `bearer`, `passphrase`, `signature`, …) и синхронизирован между
Go-админкой и оркестратором.
### Удалено
- Маршруты, операциями которых продукт не владеет:
`POST /hysteria2ChangeVersion`, `GET /listRelease`,
`POST /config/updateHysteria2Config`, `POST /config/importHysteria2Config`,
`POST /config/restartServer`, `POST /config/uploadCertFile`,
`GET /config/hysteria2AcmePath`. Вместе с ними — соответствующие сервисы,
клиентские функции фронтенда, кнопки и строки i18n.
Маршруты удалены, а не оставлены отвечающими «feature disabled»: API-контракт
не должен обещать updater, которого у продукта нет, а неиспользуемый маршрут
остаётся attack surface.
## [1.0.0] — 2026-08-27 ## [1.0.0] — 2026-08-27
Первый релиз линейки `v1`. Первый релиз линейки `v1`.
@@ -119,8 +240,13 @@
Порядок перехода: Порядок перехода:
1. Выпишите с работающего сервера список пиров и их секреты. 1. Выпишите с работающего сервера список пиров и их секреты.
2. Разверните `1.0.0` на чистом Debian 13 из release-пакета. 2. Очистите сервер: `tools/legacy/purge-v0.sh` или ручная процедура из
3. Заведите пиров заново и раздайте новые клиентские ссылки. [docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md).
3. Разверните `1.0.0` на чистом Debian 13 из release-пакета.
4. Заведите пиров заново и раздайте новые клиентские ссылки.
Установщик `1.0.0` обнаружит остатки предыдущей установки на шаге PHASE 0,
откажется работать и **не изменит на сервере ничего**.
Клиентские ссылки `0.x` в любом случае перестанут работать: смена Клиентские ссылки `0.x` в любом случае перестанут работать: смена
обфускации — это изменение wire-совместимости. обфускации — это изменение wire-совместимости.
+104 -35
View File
@@ -138,7 +138,12 @@ hy2xs-install/
└── metadata/ └── metadata/
``` ```
При запуске `install.sh` пакет проверяет `metadata/checksums.txt`, устанавливает orchestrator в `/usr/local/lib/hy2xs/hy2xs-orchestrator`, создаёт symlink `/usr/local/bin/hy2xs-orchestrator`, копирует package assets в `/usr/local/lib/hy2xs/package` и передаёт управление installonly orchestrator. При запуске `install.sh` пакет сначала проверяет `metadata/checksums.txt` и
выполняет readonly cleanhost preflight **из распакованного архива**. Только
после этого он устанавливает orchestrator в
`/usr/local/lib/hy2xs/hy2xs-orchestrator`, создаёт symlink
`/usr/local/bin/hy2xs-orchestrator`, копирует package assets в
`/usr/local/lib/hy2xs/package` и передаёт управление installonly orchestrator.
## Сетевая модель по умолчанию ## Сетевая модель по умолчанию
@@ -169,13 +174,18 @@ HY2XS **не привязан к конкретному номеру верси
build machine target server build machine target server
───────────── ───────────── ───────────── ─────────────
определить последнюю стабильную ─┐ определить последнюю стабильную ─┐
скачать артефакт, посчитать SHA-256 взять ожидаемый SHA-256 из
проверить, что бинарник принимает ├─► release‑пакет ──► скачать ровно upstream hashes.txt │
канонический конфиг HY2XS │ version + url этот артефакт, скачать артефакт и сверить его ├─► release‑пакет ──► скачать ровно
заморозить version/url/sha256 ─┘ + sha256 сверить SHA-256 проверить, что бинарник принимает │ version + url этот артефакт,
и `hysteria version` канонический конфиг HY2XS │ + sha256 сверить SHA-256
заморозить version/url/sha256 ─┘ и `hysteria version`
``` ```
Контрольная сумма берётся из upstream‑ассета `hashes.txt`, а не считается
только локально: локальный пересчёт подтверждает, что файл не изменился после
скачивания, но не доказывает, что скачан именно ожидаемый upstream artifact.
Что это даёт: Что это даёт:
- новая установка получает актуальную Hysteria без ручного обновления version lock; - новая установка получает актуальную Hysteria без ручного обновления version lock;
@@ -459,19 +469,33 @@ HY2XS_UI_PUBLIC_ACCESS=false
./install.sh --config /root/hy2xs-target.env --non-interactive ./install.sh --config /root/hy2xs-target.env --non-interactive
``` ```
Во время установки HY2XS: Установка идёт в две фазы с жёсткой границей между ними.
1. проверит checksums release‑пакета; **PHASE 0 — только чтение.** До её успешного завершения на сервере не
2. установит orchestrator в `/usr/local/lib/hy2xs`; изменяется ни один файл, включая `/usr/local/lib/hy2xs`:
3. создаст runtime‑каталоги и service users;
4. запишет `/etc/hy2xs/hy2xs.env`; 1. проверит, что запущено от root;
5. разложит bundled HY2XS admin; 2. проверит checksums release‑пакета;
6. скачает закреплённый в пакете Hysteria2 binary из upstream, проверит SHA256 и фактическую версию; 3. запустит cleanhost preflight **из распакованного архива**: платформа
7. создаст `/etc/hysteria/config.yaml`; Debian 13 amd64, отсутствие предыдущей установки, валидность конфигурации.
8. установит systemd‑юниты;
9. применит nftables‑правила; **PHASE 1 — применение изменений:**
10. выполнит smokechecks;
11. зафиксирует успешное состояние в `/var/lib/hy2xs/install-state.json`. 4. установит orchestrator в `/usr/local/lib/hy2xs` и разложит runtime‑пакет;
5. создаст runtime‑каталоги и service users;
6. запишет `/etc/hy2xs/hy2xs.env`;
7. разложит bundled HY2XS admin;
8. скачает закреплённый в пакете Hysteria2 binary из upstream, проверит SHA256 и фактическую версию;
9. создаст `/etc/hysteria/config.yaml`;
10. установит systemd‑юниты;
11. применит nftables‑правила;
12. выполнит smokechecks;
13. зафиксирует успешное состояние в `/var/lib/hy2xs/install-state.json`.
Если PHASE 0 не прошла, установщик завершается с ошибкой и **сервер остаётся в
том же состоянии, в котором был**. HY2XS v1 не устанавливается поверх
предыдущего поколения и не мигрирует его состояние: очистка старой установки —
отдельная явная операция, см. [docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md).
### 10. Получите bootstrap‑пароль админки ### 10. Получите bootstrap‑пароль админки
@@ -654,14 +678,21 @@ hy2xs-orchestrator reconfigure \
| Команда | Назначение | | Команда | Назначение |
| --- | --- | | --- | --- |
| `hy2xs-orchestrator preflight-install` | Read‑only проверка чистоты хоста; ничего не меняет |
| `hy2xs-orchestrator status` | Показать состояние платформы, сервисов, firewall и install marker | | `hy2xs-orchestrator status` | Показать состояние платформы, сервисов, firewall и install marker |
| `hy2xs-orchestrator doctor` | Выполнить preflight и smokechecks текущей установки | | `hy2xs-orchestrator doctor` | Выполнить preflight и smokechecks текущей установки |
| `hy2xs-orchestrator reconfigure --dry-run` | Проверить конфиг без применения | | `hy2xs-orchestrator reconfigure --dry-run` | Проверить конфиг без применения |
| `hy2xs-orchestrator reconfigure --apply` | Применить runtime‑конфигурацию | | `hy2xs-orchestrator reconfigure --apply` | Применить runtime‑конфигурацию |
| `hy2xs-orchestrator repair` | Попытаться восстановить partial install state | | `hy2xs-orchestrator repair --allow-partial-state` | Довести до конца незавершённую установку **текущего поколения** |
| `hy2xs-orchestrator diagnostics collect` | Собрать diagnostic bundle в `/var/log/hy2xs/diagnostics` | | `hy2xs-orchestrator diagnostics collect` | Собрать diagnostic bundle в `/var/log/hy2xs/diagnostics` |
| `hy2xs-orchestrator redact-config` | Отредактировать секреты в env/yaml перед публикацией логов | | `hy2xs-orchestrator redact-config` | Отредактировать секреты в env/yaml перед публикацией логов |
`repair` без `--allow-partial-state` работает только поверх полностью успешной
установки. В обоих режимах он сначала проверяет, что
`/var/lib/hy2xs/install-state.json` принадлежит текущему поколению продукта
(`product`, `release_line`, `config_schema_version`), и отказывается работать
поверх чужого состояния.
Пример сбора диагностики: Пример сбора диагностики:
```bash ```bash
@@ -708,6 +739,35 @@ permitopen 127.0.0.1:8080 localhost:8080
## Troubleshooting ## Troubleshooting
### Установка отказывается: обнаружена предыдущая установка
```text
[hy2xs] ERROR: На сервере обнаружена предыдущая или посторонняя установка.
HY2XS v1 не поддерживает установку поверх и не мигрирует состояние 0.x.
Ни один файл на сервере не изменён.
```
Это ожидаемое поведение, а не сбой. Отказ происходит в PHASE 0, до любой
мутации: сервер остался в том состоянии, в котором был.
Что делать:
1. сохраните нужные данные (база пиров, конфиг) — см.
[docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md);
2. посмотрите план очистки: `sudo ./purge-v0.sh`;
3. выполните очистку: `sudo ./purge-v0.sh --apply --yes-i-know`;
4. повторите установку.
Отдельный случай — отказ вида
`HY2XS_CONFIG_SCHEMA_VERSION отсутствует в конфигурации`. Он означает, что
переданный `--config` относится к предыдущему поколению: до v1 этого поля не
существовало. Создайте конфиг заново по разделу «Создайте конфиг для своего
сервера».
Если установка HY2XS v1 упала **после** начала применения изменений, полная
очистка не нужна — используйте
`hy2xs-orchestrator repair --allow-partial-state`.
### Установка падает на DNS AAAA ### Установка падает на DNS AAAA
Причина: домен имеет IPv6 AAAA‑запись, а HY2XS production profile является IPv4only. Причина: домен имеет IPv6 AAAA‑запись, а HY2XS production profile является IPv4only.
@@ -787,23 +847,27 @@ git status --short
Подготовьте build env: Подготовьте build env:
Версии и контрольные суммы toolchain **не задаются переменными окружения**: они
объявлены в корневом [`versions.env`](versions.env), и сборка берёт их оттуда.
Раньше их приходилось передавать снаружи, из-за чего воспроизводимая сборка в
чистой Debian‑среде требовала предварительного знания четырёх SHA‑256.
```bash ```bash
export PACKAGE_VERSION=1.0.0
export BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) export BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ)
# Для переносимости между x86_64-серверами без AVX2 предпочтителен baseline artifact. # Для переносимости между x86_64-серверами без AVX2 предпочтителен baseline artifact.
# Ожидаемый digest выбирается автоматически: в versions.env зафиксированы обе суммы.
export BUN_FLAVOR=x64-baseline export BUN_FLAVOR=x64-baseline
# Builder требует SHA256 для скачиваемых toolchain-архивов.
# Значения нужно брать из официальных release/checksum источников для конкретных версий.
export GO_ARCHIVE_SHA256=<sha256-go1.21.13-linux-amd64.tar.gz>
export NODE_ARCHIVE_SHA256=<sha256-node-v20.19.0-linux-x64.tar.xz>
export BUN_ARCHIVE_SHA256=<sha256-bun-linux-x64-baseline-1.3.13.zip>
# Опционально: снимает anonymous rate limit при разрешении upstream-релиза. # Опционально: снимает anonymous rate limit при разрешении upstream-релиза.
export GITHUB_TOKEN=<token> export GITHUB_TOKEN=<token>
``` ```
`PACKAGE_VERSION` тоже приходит из `versions.env` (`HY2XS_VERSION`). Шаг
`verify_versions_contract` роняет сборку, если версия продукта, схема
конфигурации, целевая платформа или `packageManager` в `package.json`
разошлись с контрактом.
Запустите сборку: Запустите сборку:
```bash ```bash
@@ -812,25 +876,28 @@ export GITHUB_TOKEN=<token>
Сборка последовательно: Сборка последовательно:
1. прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`); 1. проверяет контракт `versions.env` (`verify_versions_contract`);
2. определяет последнюю стабильную версию Hysteria, скачивает артефакт и считает SHA‑256; 2. прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`);
3. проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander; 3. определяет последнюю стабильную версию Hysteria, берёт ожидаемый SHA‑256 из upstream `hashes.txt` и сверяет с ним скачанный артефакт;
4. собирает orchestrator, frontend и backend; 4. проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander;
5. прогоняет `go vet` и `go test` для HY2XS admin; 5. собирает orchestrator, frontend и backend, проставляя версию админки из контракта;
6. формирует архив и прогоняет acceptance‑проверки. 6. прогоняет `go vet` и `go test` для HY2XS admin;
7. формирует архив и прогоняет acceptance‑проверки.
Любой сбой на шагах 1–5 останавливает сборку до создания пакета. Любой сбой на шагах 1–6 останавливает сборку до создания пакета.
Переменные, управляющие выбором версии Hysteria: Переменные, управляющие выбором версии Hysteria:
| Переменная | По умолчанию | Назначение | | Переменная | По умолчанию | Назначение |
| --- | --- | --- | | --- | --- | --- |
| `HYSTERIA_CHANNEL` | `stable` | `stable` — разрешить последнюю стабильную; `pinned` — офлайн‑сборка по `tools/build/hysteria-lock.env` | | `HYSTERIA_CHANNEL` | из `versions.env` (`stable`) | `stable` — разрешить последнюю стабильную; `pinned` — офлайн‑сборка по `tools/build/hysteria-lock.env` |
| `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` | | `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` |
| `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release‑сборок обязателен | | `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release‑сборок обязателен |
| `HYSTERIA_VERIFY_UPSTREAM_HASHES` | `true` | Сверять артефакт с upstream `hashes.txt`; отключение — только break‑glass |
| `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в lock‑файл | | `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в lock‑файл |
Полный E2E с реальным клиентом Hysteria запускается отдельно: Полный E2E с реальным клиентом Hysteria запускается отдельно (нужен Go: ссылка
берётся из production‑генератора, а не из отдельной реализации внутри теста):
```bash ```bash
HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
@@ -863,7 +930,9 @@ tar -tzf dist/hy2xs-install-1.0.0.tar.gz | grep -E \
├── package/ # skeleton будущего install package ├── package/ # skeleton будущего install package
├── tools/build/ # production builder и packaging pipeline ├── tools/build/ # production builder и packaging pipeline
├── tools/test/ # end-to-end проверки с реальным клиентом Hysteria ├── tools/test/ # end-to-end проверки с реальным клиентом Hysteria
├── tools/legacy/ # purge-v0.sh: очистка сервера от предыдущего поколения
├── docs/ # спецификации baseline, тестов и эксплуатации ├── docs/ # спецификации baseline, тестов и эксплуатации
├── versions.env # контракт продукта, платформы и toolchain
├── CHANGELOG.md ├── CHANGELOG.md
├── README.md ├── README.md
└── LICENSE └── LICENSE
+145 -17
View File
@@ -40,20 +40,103 @@ Builder не является частью target install flow: на target serv
- шаблоны для `post-install.env` - шаблоны для `post-install.env`
- package metadata - package metadata
## Контракт версий: `versions.env`
Корневой `versions.env`**единственный источник истины** для контракта
«продукт / платформа / toolchain».
Что в нём есть:
```bash
HY2XS_VERSION=1.0.0
HY2XS_RELEASE_LINE=1
HY2XS_CONFIG_SCHEMA_VERSION=2
HY2XS_BUILD_OS=debian
HY2XS_BUILD_OS_VERSION=13
HY2XS_BUILD_ARCH=amd64
HY2XS_TARGET_OS=debian
HY2XS_TARGET_OS_VERSION=13
HY2XS_TARGET_ARCH=amd64
GO_VERSION=1.21.13
GO_LINUX_AMD64_SHA256=<sha256>
BUN_VERSION=1.3.13
BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>
NODE_VERSION=20.19.0
NODE_LINUX_X64_SHA256=<sha256>
PNPM_VERSION=9.15.9
HYSTERIA_CHANNEL=stable
```
Чего в нём **нет** и быть не должно:
1. **Прикладных зависимостей** (Vue, Gin, GORM, npm/Go модули). У них уже есть
канонические lock-механизмы: `apps/frontend/pnpm-lock.yaml`,
`orchestrator/bun.lock`, `apps/go.sum`. Второй слой неизбежно разъедется с
настоящим графом зависимостей.
2. **Конкретной версии Hysteria.** Здесь живёт только *политика* выбора
(`HYSTERIA_CHANNEL`); результат резолва замораживается в
`tools/build/hysteria-lock.env`. Пин версии здесь вернул бы ручное
обновление, от которого мы ушли.
Два Bun-артефакта зафиксированы отдельно намеренно: `select_bun_artifact()`
выбирает `bun-linux-x64` или `bun-linux-x64-baseline` по наличию AVX2, поэтому
одной контрольной суммы архитектурно недостаточно.
### Проверка, а не генерация
`profile.ts`, `package/config/hy2xs.env` и `packageManager` в двух `package.json`
остаются обычными файлами. Сборка их **не генерирует**, а сверяет шагом
`verify_versions_contract`.
Причина: генерируемые исходники ломают чистый чекаут — `bun test`, `tsc` и
`go test` должны работать до запуска сборки. Проверка даёт тот же инвариант
дешевле.
`verify_versions_contract` сверяет:
| Что | С чем |
| --- | --- |
| `PACKAGE_VERSION` | `HY2XS_VERSION` |
| `orchestrator/package.json``packageManager` | `bun@$BUN_VERSION` |
| `apps/frontend/package.json``packageManager` | `pnpm@$PNPM_VERSION` |
| `package/config/hy2xs.env` → схема | `HY2XS_CONFIG_SCHEMA_VERSION` |
| константы, **скомпилированные** в оркестратор | схема, release line, целевая платформа |
| `apps/go.mod` → директива `go` | `GO_VERSION` |
| `metadata/package.env` | версия, release line, схема, target |
| `hy2xs-admin version` (готовый бинарь) | `v$HY2XS_VERSION` |
Контракт оркестратора сверяется не grep'ом по исходникам, а выводом
`orchestrator/tools/print-contract.ts`: это доказывает, что в бинарь попало то
же значение.
Версия админки приходит в бинарь через ldflags:
```bash
-ldflags "-s -w -X 'hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}'"
```
Собственной константы версии в Go-коде больше нет: она уже успела разъехаться с
версией пакета.
## Что делает builder ## Что делает builder
1. Проверяет структуру проекта. 1. Проверяет структуру проекта.
2. Прогоняет тесты и типы оркестратора. 2. Загружает и проверяет контракт `versions.env`.
3. Разрешает upstream-версию Hysteria и проходит compatibility gate. 3. Прогоняет тесты и типы оркестратора.
4. Компилирует оркестратор из Bun/TypeScript в install-артефакт. 4. Разрешает upstream-версию Hysteria и проходит compatibility gate.
5. Собирает / подготавливает HY2XS admin. 5. Компилирует оркестратор из Bun/TypeScript в install-артефакт.
6. Прогоняет тесты HY2XS admin (после сборки frontend: `go:embed all:dist` требует готовых ассетов). 6. Собирает / подготавливает HY2XS admin и сверяет его версию с контрактом.
7. Копирует артефакты UI в package staging directory. 7. Прогоняет тесты HY2XS admin (после сборки frontend: `go:embed all:dist` требует готовых ассетов).
8. Кладёт entrypoint, templates, docs и service files. 8. Копирует артефакты UI в package staging directory.
9. Формирует итоговый install package. 9. Кладёт entrypoint, templates, docs и service files.
10. Считает manifest/checksum. 10. Формирует итоговый install package.
11. Проверяет архив и прогоняет acceptance-проверки. 11. Считает manifest/checksum.
12. Выдаёт один переносимый результат для target machine. 12. Проверяет архив и прогоняет acceptance-проверки.
13. Выдаёт один переносимый результат для target machine.
## Что builder не делает ## Что builder не делает
@@ -110,14 +193,19 @@ project/
`tools/build/build.sh` должен быть самодостаточным для Debian 13 amd64: `tools/build/build.sh` должен быть самодостаточным для Debian 13 amd64:
1. Проверяет ОС и архитектуру. 1. Проверяет ОС и архитектуру по `versions.env` (`HY2XS_BUILD_*`).
2. Проверяет структуру репозитория и lock-файлы. 2. Проверяет структуру репозитория и lock-файлы.
3. Доставляет отсутствующие системные build-зависимости через `apt-get`. 3. Доставляет отсутствующие системные build-зависимости через `apt-get`.
4. Проверяет версии Go, Bun, Node.js и pnpm. 4. Проверяет версии Go, Bun, Node.js и pnpm по `versions.env`.
5. При несовпадении версий скачивает управляемый локальный toolchain в `.toolchain/`. 5. При несовпадении версий скачивает управляемый локальный toolchain в `.toolchain/`
и **сверяет каждый архив с контрольной суммой из `versions.env`**.
6. Собирает только Linux amd64 артефакты. 6. Собирает только Linux amd64 артефакты.
7. Записывает версии toolchain в metadata пакета. 7. Записывает версии toolchain в metadata пакета.
Собственных значений по умолчанию у `tools/build/lib/deps.sh` больше нет: без
загруженного контракта сборка падает сразу, а не собирает пакет на неизвестном
toolchain.
## Отношение к Hysteria2 ## Отношение к Hysteria2
Сам бинарь Hysteria2 **не вендорится** в install package как baseline-правило. Сам бинарь Hysteria2 **не вендорится** в install package как baseline-правило.
@@ -135,10 +223,13 @@ SOURCE
resolve latest stable (HyNetworks/hysteria, только теги app/vX.Y.Z) resolve latest stable (HyNetworks/hysteria, только теги app/vX.Y.Z)
resolve exact release asset (hysteria-linux-amd64) resolve exact release asset (hysteria-linux-amd64 + hashes.txt)
download + compute SHA-256 download hashes.txt → ожидаемый SHA-256 от upstream
download artifact + сверка с ожидаемым SHA-256
compatibility gate (реальный бинарник принимает канонический конфиг HY2XS) compatibility gate (реальный бинарник принимает канонический конфиг HY2XS)
@@ -165,8 +256,43 @@ TARGET SERVER
| `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` | | `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` |
| `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен | | `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен |
| `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `tools/build/hysteria-lock.env` | | `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `tools/build/hysteria-lock.env` |
| `HYSTERIA_VERIFY_UPSTREAM_HASHES` | `true` | Сверять артефакт с upstream `hashes.txt`; отключение — только break-glass |
| `GITHUB_TOKEN` | пусто | Опционально, чтобы не упереться в anonymous rate limit | | `GITHUB_TOKEN` | пусто | Опционально, чтобы не упереться в anonymous rate limit |
### Проверка происхождения артефакта
Раньше SHA-256 считался локально от уже скачанного файла. Это защищает target от
последующей подмены, но не доказывает, что builder скачал именно ожидаемый
upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было
(trust-on-first-use).
Upstream публикует контрольные суммы релиза отдельным ассетом `hashes.txt`:
```text
6493dfff…f94 build/hysteria-linux-amd64
f24f63be…189 build/hysteria-linux-amd64-avx
```
Поэтому порядок теперь такой:
```text
download hysteria-linux-amd64
download hashes.txt
ожидаемый SHA-256 из upstream
сверка скачанного бинарника
и только после этого — запись SHA-256 в HY2XS lock и metadata
```
Сопоставление идёт по базовому имени и строго на равенство: `build/` — часть
пути, а `hysteria-linux-amd64-avx` — другой артефакт, который не должен совпасть
по префиксу. Разбор вынесен в `parseUpstreamHashes` и покрыт тестами.
Источник ожидаемой суммы фиксируется в `metadata/package.env`
(`hysteria_sha_source=upstream-hashes | hy2xs-lock | local-download`).
Дополнительно: Дополнительно:
- версия, URL и SHA256 фиксируются в metadata install package (`metadata/hysteria.version`, `metadata/hysteria.url`, `metadata/hysteria.sha256`); - версия, URL и SHA256 фиксируются в metadata install package (`metadata/hysteria.version`, `metadata/hysteria.url`, `metadata/hysteria.sha256`);
- способ выбора версии фиксируется в `metadata/hysteria.resolution` и `metadata/package.env`; - способ выбора версии фиксируется в `metadata/hysteria.resolution` и `metadata/package.env`;
@@ -180,7 +306,7 @@ Gate защищает от ситуации, когда upstream меняет с
Порядок: Порядок:
1. скачать артефакт и сверить SHA-256; 1. скачать артефакт и сверить SHA-256 с upstream `hashes.txt`;
2. сверить `hysteria version` с разрешённой версией; 2. сверить `hysteria version` с разрешённой версией;
3. отрендерить канонический конфиг HY2XS тем же кодом, что работает на target (`orchestrator/tools/render-canonical-config.ts`); 3. отрендерить канонический конфиг HY2XS тем же кодом, что работает на target (`orchestrator/tools/render-canonical-config.ts`);
4. запустить реальный бинарник Hysteria с этим конфигом — для Gecko и для Salamander; 4. запустить реальный бинарник Hysteria с этим конфигом — для Gecko и для Salamander;
@@ -206,3 +332,5 @@ BUILD FAILED: unsupported Hysteria stable v2.13.0
6. Hysteria2 подтягивается install layer'ом с upstream по замороженным координатам, а не собирается на target из исходников 6. Hysteria2 подтягивается install layer'ом с upstream по замороженным координатам, а не собирается на target из исходников
7. выход новой версии Hysteria после сборки не меняет содержимое уже собранного пакета 7. выход новой версии Hysteria после сборки не меняет содержимое уже собранного пакета
8. несовместимый upstream ломает сборку, а не установку у пользователя 8. несовместимый upstream ломает сборку, а не установку у пользователя
9. контрольная сумма Hysteria подтверждена upstream-ассетом `hashes.txt`, а не только локальным пересчётом
10. версии продукта, платформы и toolchain объявлены в одном месте, а рассинхрон роняет сборку до создания tarball
+52 -4
View File
@@ -79,11 +79,28 @@ HY2XS admin работает как надстройка над Hysteria YAML/AP
- `access_token` в auth-URL и учётные данные, встроенные в URL; - `access_token` в auth-URL и учётные данные, встроенные в URL;
- `auth.password`, `auth.userpass`; - `auth.password`, `auth.userpass`;
- учётные данные ACME DNS-провайдера; - учётные данные ACME DNS-провайдера;
- любые **неизвестные** поля, имя которых содержит `password`, `secret`, `token` или `credential`. - **неизвестные** поля с секретоподобным именем: `password`, `passwd`,
`passphrase`, `secret`, `token`, `credential`, `apiKey` / `api_key`,
`privateKey` / `private_key`, `accessKey`, `secretKey`, `authorization`,
`cookie`, `bearer`, `signature`.
Последний пункт — обратная сторона сохранения неизвестных полей: новое upstream-поле с секретом вырезается ещё до того, как HY2XS про него узнает. Последний пункт — обратная сторона сохранения неизвестных полей: новое
upstream-поле с секретом вырезается ещё до того, как HY2XS про него узнает.
Пути к файлам (`tls.cert`, `tls.key`, `ech.keyPath`, `tls.clientCA`) секретами не считаются и остаются читаемыми — они нужны для диагностики. ### Как формулируется гарантия
Точная формулировка:
> вырезаются известные секреты и неизвестные поля с секретоподобным именем.
Не «любой будущий секрет будет автоматически удалён». Обобщённый sanitizer
работает по именам полей и не может предугадать произвольное имя, которое
upstream выберет для нового секрета. Список маркеров синхронизирован с
`orchestrator/src/lib/redaction.ts`; при появлении нового поля его нужно
добавить в оба места.
Пути к файлам (`tls.cert`, `tls.key`, `ech.keyPath`, `tls.clientCA`) секретами
не считаются и остаются читаемыми — они нужны для диагностики.
## Модель современной схемы Hysteria ## Модель современной схемы Hysteria
@@ -118,10 +135,41 @@ HY2XS admin работает как надстройка над Hysteria YAML/AP
- Hysteria2 запускается отдельным `hysteria-server.service`; - Hysteria2 запускается отдельным `hysteria-server.service`;
- HY2XS admin работает как operator UI и HTTP auth/traffic layer; - HY2XS admin работает как operator UI и HTTP auth/traffic layer;
- HY2XS admin не запускается от root; - HY2XS admin не запускается от root;
- смена версии Hysteria2 через UI отключена в baseline; - смена версии Hysteria2 через UI **отсутствует как API**;
- список upstream releases не является частью operator UI baseline; - список upstream releases не является частью operator UI baseline;
- port hopping не является частью production path. - port hopping не является частью production path.
### Удалённые операции: почему не заглушки
Маршруты, которые продукт принципиально не поддерживает, **удалены**, а не
оставлены отвечающими «feature disabled»:
| Удалённый маршрут | Кто владеет операцией |
| --- | --- |
| `POST /hysteria2ChangeVersion` | install-оркестратор |
| `GET /listRelease` | build layer |
| `POST /config/updateHysteria2Config` | install-оркестратор |
| `POST /config/importHysteria2Config` | install-оркестратор |
| `POST /config/restartServer` | systemd |
| `POST /config/uploadCertFile` | оператор + оркестратор |
| `GET /config/hysteria2AcmePath` | не имел потребителя |
Причины две.
Во-первых, API-контракт не должен даже обещать updater, которого у продукта
нет: маршрут, всегда возвращающий отказ, вводит в заблуждение.
Во-вторых, это лишняя attack surface и технический мусор от прежней
архитектуры.
Вместе с маршрутами удалены соответствующие клиентские функции фронтенда,
кнопки и строки i18n. Кнопка, которая гарантированно возвращает ошибку, —
не «точка расширения на будущее», а дефект UX. Возвращение любого из этих
маршрутов ломает acceptance-проверку сборки.
Конфигурация Hysteria остаётся доступной панели **на чтение и на выгрузку**:
`GET /config/getHysteria2Config` и `POST /config/exportHysteria2Config`.
### Что нельзя делать ### Что нельзя делать
- собирать admin-компонент на target server; - собирать admin-компонент на target server;
+141 -2
View File
@@ -19,6 +19,7 @@
## Главная роль оркестратора ## Главная роль оркестратора
Оркестратор работает **только на target machine** и умеет: Оркестратор работает **только на target machine** и умеет:
- выполнить read-only проверку чистоты хоста (`preflight-install`)
- выполнить первичную установку (`install`) - выполнить первичную установку (`install`)
- выполнить явную реконфигурацию (`reconfigure --dry-run|--apply`) - выполнить явную реконфигурацию (`reconfigure --dry-run|--apply`)
- разложить bundled UI - разложить bundled UI
@@ -48,6 +49,93 @@
Если машина уже «жила своей жизнью», baseline не обещает корректной автоадаптации. Если машина уже «жила своей жизнью», baseline не обещает корректной автоадаптации.
## Двухфазный контракт установки
Установка разделена на две фазы с жёсткой границей между ними:
```text
PHASE 0 — READ ONLY
проверка прав
sha256sum -c metadata/checksums.txt
./orchestrator/hy2xs-orchestrator preflight-install --package-dir <распакованный пакет>
├── платформа Debian 13 amd64
├── clean-host контракт
└── валидация конфигурации
↓ ноль persistent writes
PHASE 0 PASSED
PHASE 1 — MUTATION
install -d /usr/local/lib/hy2xs
раскладка оркестратора и runtime-пакета
hy2xs-orchestrator install
```
Ключевые свойства:
- `preflight-install` запускается **из распакованного пакета**, а не из
установленного `/usr/local/lib/hy2xs`: до PHASE 1 этого каталога может не
существовать, и создавать его нельзя.
- Граница держится не соглашением, а **read-only guard** (`lib/guard.ts`):
под ним `writeText`/`writeTextAtomic` и мутирующие раннеры `lib/process`
кидают ошибку. Это проверяется тестами.
- Внутри `install` **`preflight()` выполняется раньше первой записи
`install-state.json`**. Отказ на этом этапе означает, что на сервере не
изменено ничего.
Полный список маркеров чужой установки и порядок очистки —
[14-legacy-cleanup.md](14-legacy-cleanup.md).
## Маркер состояния установки
`/var/lib/hy2xs/install-state.json` отвечает на вопрос «эта машина — установка
**текущего поколения** HY2XS, и в каком она состоянии». Поэтому кроме фазы он
несёт идентификацию поколения:
```json
{
"product": "hy2xs",
"release_line": 1,
"config_schema_version": 2,
"product_version": "1.0.0",
"installed": true,
"phase": "installed"
}
```
`reconfigure` и `repair` проверяют `product` / `release_line` /
`config_schema_version` **до** всего остального. Флага `installed: true`
недостаточно: такой же маркер мог остаться от 0.x.
`repair` дополнительно требует явного `--allow-partial-state`, чтобы работать
поверх незавершённой установки. Разрешение не подразумевается: молчаливое
согласие на произвольный partial marker и позволяло «чинить» чужое состояние.
## Ownership и rollback
Операция ведёт учёт того, что она реально успела применить:
```text
depsInstalled
filesystemPrepared
unitsDeployed
firewallTouched
postInstallWritten
servicesStarted
```
Классификация отказа строится **по этим флагам и фазе**, а не по тексту
сообщения об ошибке. Ранее классификация шла по подстрокам, из-за чего
preflight-ошибка со словом `nftables` приводила к откату чужого firewall.
Инварианты rollback:
- `fatal_pre_apply` по определению означает «ничего не применялось»:
system rollback не выполняется, `install-state.json` не пишется,
diagnostics-бандл не собирается (его сбор сам создал бы каталоги в
`/var/log/hy2xs`).
- `systemctl stop/disable` выполняется **только если текущая операция сама
развернула эти unit-файлы**.
## Что приходит на target ## Что приходит на target
На target должен попадать уже готовый package, содержащий: На target должен попадать уже готовый package, содержащий:
@@ -74,7 +162,7 @@
## Что делает оркестратор по шагам ## Что делает оркестратор по шагам
1. Проверяет, что ОС — Debian 13. 1. Проверяет, что ОС — Debian 13, и что хост чист (**до любой мутации**).
2. Проверяет базовые зависимости и install context. 2. Проверяет базовые зависимости и install context.
3. Создаёт каталоги установки. 3. Создаёт каталоги установки.
4. Разворачивает bundled HY2XS admin. 4. Разворачивает bundled HY2XS admin.
@@ -115,19 +203,70 @@
## CLI baseline ## CLI baseline
Команды: Команды:
- `preflight-install --package-dir <path> [--config <source-env>]`
- `install --package-dir <path> [--config <source-env>]` - `install --package-dir <path> [--config <source-env>]`
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --dry-run` - `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --dry-run`
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --apply` - `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --apply`
- `repair --package-dir <path> --config /etc/hy2xs/hy2xs.env [--allow-partial-state]`
- `redact-config --config <path> (--in-place | --out <path>) [--format auto|env|yaml]`
`preflight-install` не принимает `--skip-*`: эти флаги влияют на мутацию, а
PHASE 0 ничего не меняет.
`--allow-partial-state` допустим только для `repair`.
Инварианты: Инварианты:
- только IPv4 bind/listen; - только IPv4 bind/listen;
- TLS modes: `acme | file | self_signed_dev`; - TLS modes: `acme | file | self_signed_dev`;
- `trafficStats.secret` отдельный от `JWT_SECRET`; - `trafficStats.secret` отдельный от `JWT_SECRET`;
- `HY2XS_CONFIG_SCHEMA_VERSION` — обязательное поле; его отсутствие трактуется
как legacy-конфигурация и отклоняется, а не заменяется значением по умолчанию;
- install flow фиксирует фактически установленную версию Hysteria в snapshot; - install flow фиксирует фактически установленную версию Hysteria в snapshot;
- версия/URL/SHA256 Hysteria берутся из metadata install package; - версия/URL/SHA256 Hysteria берутся из metadata install package;
- `reconfigure` не обновляет бинарник Hysteria, только runtime-слой. - `reconfigure` не обновляет бинарник Hysteria, только runtime-слой;
- при `reconfigure --apply`: backup -> staged apply -> smoke -> rollback on fail. - при `reconfigure --apply`: backup -> staged apply -> smoke -> rollback on fail.
## Семантическая проверка сгенерированного конфига
`assertHysteriaConfigMatchesProfile` разбирает YAML и сверяет его с
production-профилем, а не ищет подстроки. Проверяются, в частности:
- `listen`, ровно один подтип `obfs` и его соответствие `obfs.type`;
- размеры пакетов Gecko;
- `bandwidth`, `disableLossCompensation`, `ignoreClientBandwidth`;
- `congestion.type` / `bbrProfile`;
- весь QUIC baseline, **включая `maxIdleTimeout`**;
- `trafficStats.listen` и непустой `secret`;
- `auth.type`, **точный** `auth.http.url` (host/port/path/token) и
`auth.http.insecure`;
- ACME: `type`, `email`, `ca`, `dir`, `listenHost`, первый домен;
- отсутствие посторонних секций верхнего уровня.
Сообщение об ошибке для `auth.http.url` намеренно не печатает сам токен: текст
уходит в логи и в diagnostics-бандл.
## Редактирование секретов
`redact-config` и diagnostics-бандл используют **структурную** редакцию: YAML
разбирается и обходится как дерево.
Это не косметика. Построчное правило `auth:\s*(.*)` подставляло маркер в
заголовок mapping'а и оставляло нетронутым вложенный
`auth.http.url` с `access_token=<секрет>`, то есть бандл уносил machine token
наружу. Значение может лежать где угодно в дереве, поэтому обходить нужно
дерево.
Редактируются:
- поля с секретоподобным именем (`password`, `secret`, `token`, `apiKey`,
`privateKey`, `authorization`, `cookie`, `bearer`, `signature`, …);
- карты, где секретны все значения (`auth.userpass`, `acme.dns.config`);
- учётные данные и секретные query-параметры внутри URL — в том числе в
env-файлах, где имя ключа (`HY2_AUTH_URL`) ни под один маркер не подходит.
Гарантия формулируется честно: **known secrets + secret-shaped unknown
fields**. Обобщённый sanitizer не может пообещать, что под правило попадёт
любой будущий секрет.
## Что не реализовывать ## Что не реализовывать
- update subcommands - update subcommands
+122 -4
View File
@@ -13,7 +13,7 @@ cd orchestrator && bun install --frozen-lockfile && bun run check && bun test
# Тесты и статический анализ HY2XS admin # Тесты и статический анализ HY2XS admin
cd apps && go vet ./... && go test ./... 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 HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
# Production-сборка: прогоняет тесты, резолвер и compatibility gate # Production-сборка: прогоняет тесты, резолвер и compatibility gate
@@ -98,6 +98,14 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
| неположительный/нецелый `min` | отклонено | | неположительный/нецелый `min` | отклонено |
| пустой obfs-пароль | автогенерация, а не пустое значение в конфиге | | пустой obfs-пароль | автогенерация, а не пустое значение в конфиге |
| `HY2XS_CONFIG_SCHEMA_VERSION=1` | отклонено с указанием на чистую установку | | `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-конфига попал литерал вместо значения из конфигурации». Отдельно — round-trip `parse(render(config)) == config`. Этот тест ловит класс ошибок «в рендер runtime-конфига попал литерал вместо значения из конфигурации».
@@ -110,6 +118,63 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
- пароль с пробелами и спецсимволами экранируется; - пароль с пробелами и спецсимволами экранируется;
- YAML-инъекция через пароль отклоняется даже в обход env-валидации. - 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 ## B. Target install tests
### На чистом Debian 13 проверяем ### На чистом Debian 13 проверяем
@@ -174,7 +239,8 @@ congestion:
quic: quic:
disableStatelessReset == false disableStatelessReset == false
окна и таймауты == baseline окна, maxIncomingStreams, disablePathMTUDiscovery == baseline
maxIdleTimeout == 30s
trafficStats: trafficStats:
listen == runtime env listen == runtime env
@@ -182,13 +248,27 @@ trafficStats:
auth: auth:
type == http 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: TLS:
acme-режим не содержит секции tls acme-режим не содержит секции tls
acme: type/email/ca/dir/listenHost/первый домен == профиль
file-режим не содержит секции acme file-режим не содержит секции acme
верхний уровень:
нет секций вне production-профиля
``` ```
`maxIdleTimeout` присутствовал в профиле, но не проверялся: конфиг с уехавшим
idle timeout проходил семантическую проверку. Точно так же `auth.http.url`
раньше сверялся только на наличие подстроки `access_token=`, из-за чего
уехавший порт или путь остались бы незамеченными — а это единственный канал
допуска пиров.
Сообщение об ошибке для `auth.http.url` намеренно не печатает сам токен: текст
уходит в логи и в diagnostics-бандл. Это закреплено отдельным тестом.
## C2. End-to-end с реальным клиентом ## C2. End-to-end с реальным клиентом
`tools/test/e2e-hysteria.sh`, отдельно для Gecko и Salamander: `tools/test/e2e-hysteria.sh`, отдельно для Gecko и Salamander:
@@ -198,7 +278,7 @@ TLS:
3. handshake с обфускацией; 3. handshake с обфускацией;
4. HTTP auth HY2XS: разрешённый пир принят; 4. HTTP auth HY2XS: разрешённый пир принят;
5. HTTP auth HY2XS: неразрешённый пир отклонён; 5. HTTP auth HY2XS: неразрешённый пир отклонён;
6. клиент подключается **именно по сгенерированной `hysteria2://` ссылке**; 6. клиент подключается **именно по ссылке, которую выдаёт production-код**;
7. TCP forwarding; 7. TCP forwarding;
8. UDP forwarding; 8. UDP forwarding;
9. `trafficStats` с валидным secret; 9. `trafficStats` с валидным secret;
@@ -209,6 +289,28 @@ TLS:
Пункт 6 — тот самый, который ловит класс ошибок, неизбежный при наивном включении Gecko: сервер работает, ссылка формально валидна, а клиент по ней не подключается. Пункт 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) ## C3. Share URI (unit)
`apps/service/hysteria2_api_test.go`: `apps/service/hysteria2_api_test.go`:
@@ -232,6 +334,22 @@ TLS:
- вырезается **неизвестное** поле с секретным именем; - вырезается **неизвестное** поле с секретным именем;
- пути к файлам (`tls.key`, `ech.keyPath`, `clientCA`) остаются видимыми. - пути к файлам (`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 ## D. Negative tests
1. не Debian 13 1. не Debian 13
+52
View File
@@ -104,6 +104,58 @@ curl -sS \
## Типовые проблемы ## Типовые проблемы
### Установка отказывается: обнаружена предыдущая установка
Отказ происходит в **PHASE 0**, до любой мутации. Сервер остался в том
состоянии, в котором был: ни `/usr/local/lib/hy2xs`, ни
`/var/lib/hy2xs/install-state.json`, ни работающие службы не тронуты.
В тексте отказа перечислены конкретные найденные маркеры. Порядок действий —
[14-legacy-cleanup.md](14-legacy-cleanup.md): сохранить данные, посмотреть план
`tools/legacy/purge-v0.sh`, выполнить очистку, установить заново.
Проверить хост, ничего не устанавливая:
```bash
./orchestrator/hy2xs-orchestrator preflight-install --package-dir "$(pwd)"
```
### `reconfigure`/`repair` отказываются: маркер чужого поколения
```text
Маркер установки /var/lib/hy2xs/install-state.json не относится к текущему
поколению HY2XS.
```
`installed: true` сам по себе ничего не доказывает: такой же маркер мог
остаться от `0.x`. Обе команды проверяют `product`, `release_line` и
`config_schema_version`.
Посмотреть, что видит оркестратор:
```bash
hy2xs-orchestrator status --package-dir /usr/local/lib/hy2xs/package \
| grep -o '"install_state_generation":"[^"]*"'
```
`"current"` — маркер текущего поколения; `"foreign"` — требуется чистая
переустановка; `"absent"` — установки нет.
### Незавершённая установка текущего поколения
Если установка упала **после** начала применения изменений, полная очистка не
нужна:
```bash
hy2xs-orchestrator repair \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env \
--allow-partial-state
```
Флаг обязателен и осознан: без него `repair` работает только поверх полностью
успешной установки.
### Сервер установился, но UI не работает ### Сервер установился, но UI не работает
Проверить: Проверить:
- разложился ли bundled UI - разложился ли bundled UI
+16 -1
View File
@@ -156,5 +156,20 @@ hy2xs-orchestrator redact-config --config /etc/hysteria/config.yaml --out /root/
Инварианты: Инварианты:
- команда не выводит исходные секреты в stdout; - команда не выводит исходные секреты в stdout;
- требуется выбрать ровно один режим: `--in-place` или `--out <path>`; - требуется выбрать ровно один режим: `--in-place` или `--out <path>`;
- `--format auto` пытается определить формат по имени файла, при неоднозначности используйте `--format env|yaml`. - `--format auto` пытается определить формат по имени файла, при неоднозначности используйте `--format env|yaml`;
- YAML редактируется структурно (документ разбирается и обходится как дерево),
поэтому вложенные секреты вроде `auth.http.url?access_token=…` не переживают
редакцию, а результат остаётся валидным YAML;
- в env-файлах секрет вырезается и из URL-значения, даже если имя ключа
несекретное — например, `HY2_AUTH_URL` в `post-install.env`.
Та же редакция применяется к diagnostics-бандлу
(`hy2xs-orchestrator diagnostics collect`), который собирается автоматически при
неудачной установке или реконфигурации. Бандл предназначен для передачи наружу,
поэтому попадающие в него `hy2xs.env`, `post-install.env` и `config.yaml`
редактируются перед упаковкой.
При отказе **до** начала применения изменений (`fatal_pre_apply`) бандл не
собирается: его сбор сам создал бы каталоги в `/var/log/hy2xs` на сервере,
который мы обещали не трогать.
+221
View File
@@ -0,0 +1,221 @@
# Очистка сервера от предыдущей установки
## Зачем этот документ
HY2XS v1 **не поддерживает установку поверх** и **не мигрирует состояние 0.x**.
Это осознанное решение продукта, а не временное ограничение: попытка угадать,
как устроен произвольный старый сервер, приводит к полурабочим установкам,
которые невозможно диагностировать.
Отсюда следует жёсткий системный инвариант:
```text
обнаружена старая установка
НОЛЬ изменений на сервере
понятный отказ
явная очистка (этот документ)
установка HY2XS v1 с нуля
```
Установщик **никогда** не выполняет очистку самостоятельно. Удаление чужого
состояния — операция оператора, а не побочный эффект запуска `install.sh`.
## Как выглядит отказ
Установщик проверяет чистоту хоста в **PHASE 0** — до того, как изменит хотя бы
один persistent path, включая `/usr/local/lib/hy2xs`:
```text
[hy2xs-install] PHASE 0: read-only checks (no persistent path is modified)
[hy2xs-install] verifying package checksums
[hy2xs-install] running clean-host preflight from the unpacked package
[hy2xs] ERROR: На сервере обнаружена предыдущая или посторонняя установка.
HY2XS v1 не поддерживает установку поверх и не мигрирует состояние 0.x.
Ни один файл на сервере не изменён.
Найденные маркеры:
- /etc/hysteria/post-install.env (post-install.env предыдущей установки HY2XS)
- hy2xs-admin.service (systemd-юнит админки HY2XS)
Очистите сервер и установите HY2XS заново: см. docs/14-legacy-cleanup.md
```
Если вы видите этот текст — сервер в том же состоянии, в котором был до запуска.
Отдельный случай — конфигурация без маркера схемы:
```text
HY2XS_CONFIG_SCHEMA_VERSION отсутствует в конфигурации.
Похоже на конфигурацию предыдущего поколения (0.x) или на неизвестный формат.
```
До v1 поля `HY2XS_CONFIG_SCHEMA_VERSION` не существовало, поэтому его отсутствие
трактуется как legacy, а не как «текущая схема по умолчанию».
## Что именно проверяется
Контракт чистого хоста объявлен в `orchestrator/src/steps/cleanHost.ts` и покрыт
тестами. Установка отказывается, если найден хотя бы один из объектов:
| Объект | Что это |
| --- | --- |
| `/etc/hysteria/post-install.env` | post-install.env предыдущей установки |
| `/etc/hy2xs/hy2xs.env` | runtime-конфигурация предыдущей установки |
| `/etc/hy2xs/bootstrap-admin.secret` | bootstrap-секрет администратора |
| `/var/lib/hy2xs/install-state.json` | маркер состояния установки |
| `/usr/local/lib/hy2xs/package` | runtime-пакет предыдущей установки |
| `/etc/hysteria/config.yaml` | сгенерированный серверный конфиг |
| `/usr/local/bin/hysteria` | уже установленный бинарник Hysteria |
| `/etc/nftables.d/hy2xs.nft` | nftables-фрагмент HY2XS |
| `hy2xs-admin.service` | systemd-юнит админки |
| `hysteria-server.service` | systemd-юнит сервера Hysteria |
| `h-ui.service`, `/usr/local/h-ui` | наследие панели поколения 0.x |
| `HY2XS_INSTALL_DIR` (по умолчанию `/opt/hy2xs-admin`) | каталог приложения |
| `HY2XS_DATA_DIR` (по умолчанию `/var/lib/hy2xs-admin`) | каталог данных и БД |
Последние два пути берутся из конфигурации, а не захардкожены: нестандартная
установка тоже должна быть обнаружена.
## Перед очисткой
Очистка **разрушительная**. Она удаляет базу админки вместе с учётными записями
пиров: выданные пользователям ссылки перестанут работать.
Сохраните то, что вам нужно:
```bash
# ссылки и учётные записи пиров (если старая панель ещё работает)
sudo sqlite3 /var/lib/hy2xs-admin/h_ui.db '.dump' > ~/hy2xs-peers-dump.sql
# серверный конфиг Hysteria
sudo cp -a /etc/hysteria/config.yaml ~/hysteria-config.yaml.bak
# post-install-справка предыдущей установки
sudo cp -a /etc/hysteria/post-install.env ~/post-install.env.bak
```
Файлы содержат секреты. Снимите с них лишние права и не пересылайте как есть:
```bash
chmod 600 ~/hy2xs-peers-dump.sql ~/hysteria-config.yaml.bak ~/post-install.env.bak
```
Для безопасной передачи конфига наружу используйте редактирование секретов:
```bash
hy2xs-orchestrator redact-config --config ~/hysteria-config.yaml.bak --out ~/hysteria-config.redacted.yaml
```
## Очистка скриптом
Скрипт `tools/legacy/purge-v0.sh` лежит в репозитории. Скопируйте его на сервер.
Сначала — план. Без флагов скрипт **ничего не меняет**:
```bash
sudo ./purge-v0.sh
```
Он покажет, какие службы будут остановлены, какие пути удалены и какие из них
существуют прямо сейчас.
Затем — выполнение. Требуются оба флага, `--apply` без подтверждения не работает:
```bash
sudo ./purge-v0.sh --apply --yes-i-know
```
Если бинарник Hysteria нужно оставить (например, вы проверяете им что-то ещё):
```bash
sudo ./purge-v0.sh --apply --yes-i-know --keep-hysteria-binary
```
В конце скрипт сам проверяет, что хост стал чистым по тому же контракту, который
применяет установщик. Если что-то осталось, он назовёт конкретные объекты и
завершится с ошибкой.
## Что скрипт делает и чего не делает
Делает:
1. останавливает и выключает `hysteria-server`, `hy2xs-admin`, `h-ui`;
2. снимает таймеры отката firewall `hy2xs-fw-rollback-*` — они переживают
неудачную установку и иначе продолжили бы менять ruleset уже после очистки;
3. удаляет unit-файлы и выполняет `daemon-reload`;
4. удаляет каталоги приложения, конфигурации, данных и логов;
5. удаляет фрагмент `/etc/nftables.d/hy2xs.nft` и строку `include` для него из
`/etc/nftables.conf`, после чего перезагружает ruleset;
6. проверяет чистоту хоста.
Не делает:
- не трогает `sshd` и его конфигурацию;
- не удаляет `/etc/nftables.conf` целиком — остальной ruleset принадлежит
оператору;
- не удаляет системные пакеты, установленные ранее;
- не запускается автоматически из установщика.
## Ручная очистка
Если запускать скрипт нежелательно, те же шаги вручную:
```bash
sudo systemctl stop hysteria-server hy2xs-admin h-ui
sudo systemctl disable hysteria-server hy2xs-admin h-ui
sudo systemctl reset-failed hysteria-server hy2xs-admin h-ui
# таймеры отката firewall от незавершённой установки
sudo systemctl list-units --all 'hy2xs-fw-rollback-*'
# для каждого найденного юнита:
# sudo systemctl stop <unit> && sudo systemctl disable <unit>
# sudo rm -f /etc/systemd/system/<unit>
sudo rm -f /etc/systemd/system/hysteria-server.service \
/etc/systemd/system/hy2xs-admin.service \
/etc/systemd/system/h-ui.service
sudo systemctl daemon-reload
sudo rm -rf /etc/hy2xs /etc/hysteria /var/lib/hy2xs /var/lib/hy2xs-admin \
/var/lib/hysteria /var/log/hy2xs /opt/hy2xs-admin \
/usr/local/lib/hy2xs /usr/local/h-ui
sudo rm -f /usr/local/bin/hysteria /usr/local/bin/hy2xs-orchestrator
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
```
## После очистки
Устанавливайте HY2XS v1 обычным путём. PHASE 0 установщика повторит проверку
чистоты хоста и подтвердит, что всё в порядке:
```bash
sudo ./install.sh
```
## Незавершённая установка v1 — это другой случай
Если установка HY2XS v1 упала **после** начала применения изменений, полная
очистка не нужна. У такой машины есть корректный маркер состояния текущего
поколения, и её чинит `repair`:
```bash
sudo hy2xs-orchestrator repair \
--package-dir /usr/local/lib/hy2xs/package \
--config /etc/hy2xs/hy2xs.env \
--allow-partial-state
```
Флаг `--allow-partial-state` обязателен и осознан: без него `repair` работает
только поверх полностью успешной установки. При этом `repair` всё равно
проверяет, что маркер принадлежит текущему поколению (`product`,
`release_line`, `config_schema_version`), и откажется чинить чужое состояние.
Отказ вида «install state marker … не относится к текущему поколению HY2XS»
означает, что `repair` неприменим и нужна очистка по этому документу.
+10 -1
View File
@@ -11,6 +11,8 @@
- standalone update / rollback / uninstall subcommands: **вне scope** - standalone update / rollback / uninstall subcommands: **вне scope**
- сборка и упаковка: **отдельный локальный build layer** - сборка и упаковка: **отдельный локальный build layer**
- post-install state: **`/etc/hysteria/post-install.env`** - post-install state: **`/etc/hysteria/post-install.env`**
- установка: **только на чистый хост**, миграция с 0.x не поддерживается
- контракт версий продукта/платформы/toolchain: **корневой `versions.env`**
- клиентский delivery/access layer: **вне baseline этого пакета docs** - клиентский delivery/access layer: **вне baseline этого пакета docs**
## Главная архитектурная схема ## Главная архитектурная схема
@@ -38,7 +40,13 @@
4. На target нет `npm` / `pnpm` / `yarn` / `bun install` / transpile step. 4. На target нет `npm` / `pnpm` / `yarn` / `bun install` / transpile step.
5. На target нет standalone логики update / rollback / uninstall. 5. На target нет standalone логики update / rollback / uninstall.
6. В install/reconfigure есть bounded rollback для failure-сценариев firewall/systemd/config/smoke. 6. В install/reconfigure есть bounded rollback для failure-сценариев firewall/systemd/config/smoke.
7. Выдача доступа пользователям, Telegram-бот, billing, backend профилей и похожие контуры **не входят** в этот baseline. Rollback опирается на то, что операция реально успела применить: сервисы,
которые она не разворачивала, не останавливаются никогда.
7. Установка двухфазная: **PHASE 0 — read only**, **PHASE 1 — mutation**.
До успешного clean-host preflight на сервере не изменяется ни один
persistent path. Очистка предыдущей установки — отдельная явная операция
оператора, см. [14-legacy-cleanup.md](14-legacy-cleanup.md).
8. Выдача доступа пользователям, Telegram-бот, billing, backend профилей и похожие контуры **не входят** в этот baseline.
## Состав документов ## Состав документов
@@ -55,6 +63,7 @@
11. [11-testing-and-acceptance.md](11-testing-and-acceptance.md) 11. [11-testing-and-acceptance.md](11-testing-and-acceptance.md)
12. [12-operations-and-troubleshooting.md](12-operations-and-troubleshooting.md) 12. [12-operations-and-troubleshooting.md](12-operations-and-troubleshooting.md)
13. [13-production-runbook.md](13-production-runbook.md) 13. [13-production-runbook.md](13-production-runbook.md)
14. [14-legacy-cleanup.md](14-legacy-cleanup.md)
История изменений проекта — в [CHANGELOG.md](../CHANGELOG.md). История изменений проекта — в [CHANGELOG.md](../CHANGELOG.md).
+26
View File
@@ -23,3 +23,29 @@
``` ```
В baseline нет target-side JavaScript, TypeScript, frontend или Go build step. В baseline нет target-side JavaScript, TypeScript, frontend или Go build step.
## Установка выполняется в две фазы
```text
PHASE 0 — READ ONLY
проверка прав и checksums пакета
clean-host preflight из распакованного архива
↓ ноль изменений на сервере
PHASE 1 — MUTATION
установка orchestrator, раскладка runtime-пакета
install
```
HY2XS v1 **не устанавливается поверх** предыдущей установки и не мигрирует её
состояние. Если PHASE 0 обнаружит старую установку, установщик завершится с
ошибкой и **не изменит на сервере ничего**.
Проверить хост, ничего не устанавливая:
```sh
./orchestrator/hy2xs-orchestrator preflight-install --package-dir "$(pwd)"
```
Очистка предыдущей установки — отдельная явная операция оператора, она описана
в `docs/14-legacy-cleanup.md` репозитория проекта и выполняется скриптом
`tools/legacy/purge-v0.sh`. Установщик её никогда не запускает сам.
+100 -33
View File
@@ -16,7 +16,9 @@ dist/hy2xs-install-<version>.tar.gz
Основные части проекта: Основные части проекта:
- [`versions.env`](../../versions.env) — контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм.
- [`tools/build/build.sh`](build.sh) — главный entrypoint сборки. - [`tools/build/build.sh`](build.sh) — главный entrypoint сборки.
- [`tools/build/lib/versions.sh`](lib/versions.sh) — загрузка `versions.env` и `verify_versions_contract`.
- [`tools/build/lib/deps.sh`](lib/deps.sh) — проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm. - [`tools/build/lib/deps.sh`](lib/deps.sh) — проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm.
- [`tools/build/lib/package.sh`](lib/package.sh) — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива. - [`tools/build/lib/package.sh`](lib/package.sh) — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива.
- [`tools/build/lib/verify.sh`](lib/verify.sh) — проверка структуры репозитория и итогового архива. - [`tools/build/lib/verify.sh`](lib/verify.sh) — проверка структуры репозитория и итогового архива.
@@ -53,24 +55,49 @@ Windows и macOS можно использовать для редактиров
При запуске builder: При запуске builder:
1. Проверяет, что host — Debian 13 amd64. 1. Загружает и валидирует [`versions.env`](../../versions.env).
2. Проверяет структуру репозитория. 2. Проверяет, что host соответствует `HY2XS_BUILD_*` (по умолчанию Debian 13 amd64).
3. Устанавливает недостающие системные build dependencies через `apt-get`. 3. Проверяет структуру репозитория.
4. Проверяет или скачивает локальные версии: 4. Устанавливает недостающие системные build dependencies через `apt-get`.
- Go `1.21.13`; 5. Проверяет или скачивает локальные версии Go/Bun/Node.js/pnpm **из контракта**
- Bun `1.3.13`; и сверяет каждый архив с контрольной суммой из `versions.env`.
- Node.js `20.19.0`; 6. Выполняет `verify_versions_contract`: рассинхрон версий роняет сборку до создания tarball.
- pnpm `9.15.9`. 7. Прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`).
5. Прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`). 8. Разрешает upstream-версию Hysteria, берёт ожидаемый SHA-256 из upstream `hashes.txt` и сверяет с ним скачанный артефакт.
6. Разрешает upstream-версию Hysteria, скачивает артефакт и считает SHA-256. 9. Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS.
7. Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS. 10. Копирует package skeleton.
8. Копирует package skeleton. 11. Собирает install-only orchestrator в standalone binary.
9. Собирает install-only orchestrator в standalone binary. 12. Собирает frontend и backend HY2XS admin в Linux amd64 binary, проставляя версию админки через ldflags.
10. Собирает frontend и backend HY2XS admin в Linux amd64 binary. 13. Прогоняет `go vet` и `go test` для HY2XS admin (после сборки frontend, потому что `go:embed all:dist` требует готовых ассетов).
11. Прогоняет `go vet` и `go test` для HY2XS admin (после сборки frontend, потому что `go:embed all:dist` требует готовых ассетов). 14. Записывает metadata и checksums.
12. Записывает metadata и checksums. 15. Создаёт `dist/hy2xs-install-<version>.tar.gz`.
13. Создаёт `dist/hy2xs-install-<version>.tar.gz`. 16. Проверяет архив и прогоняет acceptance-проверки.
14. Проверяет архив и прогоняет acceptance-проверки.
## Контракт версий
Версии продукта, платформы и toolchain объявлены в корневом
[`versions.env`](../../versions.env). Собственных значений по умолчанию у
`deps.sh` больше нет: без загруженного контракта сборка падает сразу.
Подход — **проверка, а не генерация**. `profile.ts`,
`package/config/hy2xs.env` и `packageManager` в обоих `package.json` остаются
обычными файлами, чтобы `bun test`, `tsc` и `go test` работали из чистого
чекаута до запуска сборки. `verify_versions_contract` сверяет их с контрактом и
роняет сборку при расхождении.
Контракт оркестратора сверяется не grep'ом по исходникам, а выводом
[`orchestrator/tools/print-contract.ts`](../../orchestrator/tools/print-contract.ts):
это доказывает, что в бинарь попало то же значение.
Версия админки приезжает в бинарь через ldflags
(`-X 'hy2xs-admin/model/constant.Version=v${HY2XS_VERSION}'`) и проверяется
запуском собранного `hy2xs-admin version`. Захардкоженной константы версии в
Go-коде больше нет: она уже успела разъехаться с версией пакета.
Чего в `versions.env` нет намеренно: прикладных зависимостей (для них есть
`pnpm-lock.yaml`, `bun.lock`, `go.sum`) и конкретной версии Hysteria (здесь
только политика `HYSTERIA_CHANNEL`, результат резолва — в
[`hysteria-lock.env`](hysteria-lock.env)).
## Версия Hysteria: разрешение и compatibility gate ## Версия Hysteria: разрешение и compatibility gate
@@ -81,11 +108,32 @@ Builder не хранит версию Hysteria вручную. По умолч
1. канонический upstream — `HyNetworks/hysteria`; 1. канонический upstream — `HyNetworks/hysteria`;
2. принимаются только стабильные релизы, без draft и prerelease; 2. принимаются только стабильные релизы, без draft и prerelease;
3. тег должен иметь вид `app/vX.Y.Z`; 3. тег должен иметь вид `app/vX.Y.Z`;
4. берётся ровно один артефакт `hysteria-linux-amd64`; 4. берётся ровно один артефакт `hysteria-linux-amd64` и ровно один `hashes.txt`;
5. URL используется в том виде, в каком его вернул upstream API, без пересборки строки; 5. URL используется в том виде, в каком его вернул upstream API, без пересборки строки;
6. SHA-256 считается локально от скачанного файла; 6. ожидаемый SHA-256 берётся из upstream `hashes.txt`, и скачанный бинарник сверяется с ним;
7. разрешённые значения попадают в metadata пакета. 7. разрешённые значения попадают в metadata пакета.
### Почему hashes.txt, а не локальный пересчёт
Раньше SHA-256 считался от уже скачанного файла. Это защищает target от
последующей подмены, но не доказывает, что builder скачал именно ожидаемый
upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было.
Формат ассета:
```text
6493dfff…f94 build/hysteria-linux-amd64
f24f63be…189 build/hysteria-linux-amd64-avx
```
Сопоставление идёт по базовому имени и строго на равенство: `build/` — часть
пути, а `hysteria-linux-amd64-avx` — другой артефакт, который не должен
совпасть по префиксу. Разбор вынесен в `parseUpstreamHashes`
([`hysteriaRelease.ts`](../../orchestrator/src/build/hysteriaRelease.ts)) и
покрыт юнит-тестами.
Источник ожидаемой суммы фиксируется в metadata как `hysteria_sha_source`.
Сравнение версий числовое, поэтому `v2.9.10` считается новее `v2.9.2`. Сравнение версий числовое, поэтому `v2.9.10` считается новее `v2.9.2`.
После разрешения обязателен compatibility gate: После разрешения обязателен compatibility gate:
@@ -114,9 +162,10 @@ BUILD FAILED: unsupported Hysteria stable v2.13.0
| Переменная | По умолчанию | Назначение | | Переменная | По умолчанию | Назначение |
| --- | --- | --- | | --- | --- | --- |
| `HYSTERIA_CHANNEL` | `stable` | `stable` — разрешить последнюю стабильную через upstream API; `pinned` — офлайн-сборка по `hysteria-lock.env` | | `HYSTERIA_CHANNEL` | из `versions.env` (`stable`) | `stable` — разрешить последнюю стабильную через upstream API; `pinned` — офлайн-сборка по `hysteria-lock.env` |
| `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` | | `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` |
| `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен | | `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен |
| `HYSTERIA_VERIFY_UPSTREAM_HASHES` | `true` | Сверять артефакт с upstream `hashes.txt`; отключение — только break-glass |
| `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `hysteria-lock.env` | | `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `hysteria-lock.env` |
| `HYSTERIA_GATE_PORT` | `34443` | UDP-порт для временного запуска Hysteria в gate | | `HYSTERIA_GATE_PORT` | `34443` | UDP-порт для временного запуска Hysteria в gate |
| `HYSTERIA_GATE_STATS_PORT` | `34712` | TCP-порт trafficStats в gate | | `HYSTERIA_GATE_STATS_PORT` | `34712` | TCP-порт trafficStats в gate |
@@ -151,6 +200,16 @@ BUN_FLAVOR=auto
- если CPU поддерживает AVX2, используется `bun-linux-x64`; - если CPU поддерживает AVX2, используется `bun-linux-x64`;
- если CPU не поддерживает AVX2, используется `bun-linux-x64-baseline`. - если CPU не поддерживает AVX2, используется `bun-linux-x64-baseline`.
Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В
[`versions.env`](../../versions.env) зафиксированы обе:
```bash
BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>
```
Ожидаемый digest выбирается уже **после** `select_bun_artifact()`.
Можно принудительно задать flavor: Можно принудительно задать flavor:
```bash ```bash
@@ -171,12 +230,10 @@ BUN_FLAVOR=x64-baseline ./tools/build/build.sh
./tools/build/build.sh ./tools/build/build.sh
``` ```
С явной версией и build id: Версия пакета берётся из `versions.env`, поэтому обычно нужен только build id:
```bash ```bash
PACKAGE_VERSION=1.0.0 \ BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh
BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) \
./tools/build/build.sh
``` ```
## Проверка результата ## Проверка результата
@@ -207,9 +264,13 @@ tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env
Обновлять и коммитить [`orchestrator/bun.lock`](../../orchestrator/bun.lock) следует только когда: Обновлять и коммитить [`orchestrator/bun.lock`](../../orchestrator/bun.lock) следует только когда:
- изменился [`orchestrator/package.json`](../../orchestrator/package.json); - изменился [`orchestrator/package.json`](../../orchestrator/package.json);
- изменился `BUN_REQUIRED` в [`tools/build/lib/deps.sh`](lib/deps.sh); - изменился `BUN_VERSION` в [`versions.env`](../../versions.env);
- зависимости оркестратора обновляются осознанно. - зависимости оркестратора обновляются осознанно.
При смене `BUN_VERSION` нужно обновить и `packageManager` в
`orchestrator/package.json`, и обе контрольные суммы Bun: иначе
`verify_versions_contract` остановит сборку.
Production builder всегда выполняет: Production builder всегда выполняет:
```bash ```bash
@@ -225,7 +286,7 @@ bun install --frozen-lockfile
Правила управления пакетами frontend: Правила управления пакетами frontend:
- [`apps/frontend/package.json`](../../apps/frontend/package.json) объявляет `"packageManager": "pnpm@9.15.9"`; - [`apps/frontend/package.json`](../../apps/frontend/package.json) объявляет `"packageManager": "pnpm@9.15.9"`;
- builder использует закреплённый pnpm `9.15.9` из [`PNPM_REQUIRED`](lib/deps.sh); - builder использует закреплённый pnpm из `PNPM_VERSION` в [`versions.env`](../../versions.env), и `verify_versions_contract` сверяет эти два значения;
- production-путь установки frontend всегда: - production-путь установки frontend всегда:
```bash ```bash
@@ -236,18 +297,21 @@ pnpm install --frozen-lockfile
## Полезные переменные ## Полезные переменные
- `PACKAGE_VERSION=1.0.0`
- `BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ)` - `BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ)`
- `BUN_FLAVOR=auto|x64|x64-baseline` - `BUN_FLAVOR=auto|x64|x64-baseline`
- `FRONTEND_NODE_OLD_SPACE_SIZE=2048` (default memory limit for frontend build step) - `FRONTEND_NODE_OLD_SPACE_SIZE=2048` (default memory limit for frontend build step)
- `TOOLCHAIN_DIR=/custom/path/.toolchain` - `TOOLCHAIN_DIR=/custom/path/.toolchain`
- `VERIFY_TOOLCHAIN_CHECKSUMS=true` - `VERIFY_TOOLCHAIN_CHECKSUMS=true`
- `VERSIONS_ENV_FILE=/custom/path/versions.env`
По умолчанию `VERIFY_TOOLCHAIN_CHECKSUMS=true` в [`tools/build/lib/deps.sh`](lib/deps.sh), поэтому для production-сборки обязательно передавать контрольные суммы: `PACKAGE_VERSION` берётся из `versions.env` (`HY2XS_VERSION`); переопределять
его вручную нужно только для отладочных сборок, и `verify_versions_contract`
такую сборку отклонит.
- `GO_ARCHIVE_SHA256=<sha256>` Контрольные суммы toolchain больше **не передаются через окружение**: они
- `NODE_ARCHIVE_SHA256=<sha256>` объявлены в `versions.env`. Раньше воспроизводимая сборка в чистой Debian-среде
- `BUN_ARCHIVE_SHA256=<sha256>` требовала предварительного знания четырёх SHA-256 и не запускалась одной
командой.
## Политика памяти при сборке frontend ## Политика памяти при сборке frontend
@@ -292,7 +356,7 @@ echo "bun_exit=$?"
rm -rf .toolchain/bun .toolchain/bun-tmp rm -rf .toolchain/bun .toolchain/bun-tmp
rm -f .toolchain/downloads/bun-linux-x64-*.zip rm -f .toolchain/downloads/bun-linux-x64-*.zip
rm -f .toolchain/downloads/bun-linux-x64-baseline-*.zip rm -f .toolchain/downloads/bun-linux-x64-baseline-*.zip
PACKAGE_VERSION=1.0.0 ./tools/build/build.sh ./tools/build/build.sh
``` ```
Проверить shell syntax: Проверить shell syntax:
@@ -300,7 +364,10 @@ PACKAGE_VERSION=1.0.0 ./tools/build/build.sh
```bash ```bash
bash -n tools/build/build.sh bash -n tools/build/build.sh
bash -n tools/build/lib/common.sh bash -n tools/build/lib/common.sh
bash -n tools/build/lib/versions.sh
bash -n tools/build/lib/deps.sh bash -n tools/build/lib/deps.sh
bash -n tools/build/lib/hysteria.sh
bash -n tools/build/lib/package.sh bash -n tools/build/lib/package.sh
bash -n tools/build/lib/verify.sh bash -n tools/build/lib/verify.sh
bash -n tools/build/lib/acceptance.sh
``` ```