From 3a4ce9c751dc8d101a8f0c67cc572471c1f3b3ab Mon Sep 17 00:00:00 2001 From: Crimson Date: Thu, 27 Aug 2026 12:16:38 +0500 Subject: [PATCH] =?UTF-8?q?docs:=20clean-install-only,=20versions.env=20?= =?UTF-8?q?=D0=B8=20=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=BA=D0=B0=20=D0=BF?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D1=8B=D0=B4=D1=83=D1=89=D0=B5=D0=B3=D0=BE=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BA=D0=BE=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Новый 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 с разбором каждого исправленного дефекта. --- CHANGELOG.md | 130 ++++++++++++- README.md | 139 ++++++++++---- docs/02-build-layer-and-package.md | 162 ++++++++++++++-- docs/04-admin-panel.md | 56 +++++- docs/08-orchestrator-spec.md | 143 +++++++++++++- docs/11-testing-and-acceptance.md | 126 +++++++++++- docs/12-operations-and-troubleshooting.md | 52 +++++ docs/13-production-runbook.md | 17 +- docs/14-legacy-cleanup.md | 221 ++++++++++++++++++++++ docs/README.md | 11 +- package/docs/README.md | 26 +++ tools/build/README.md | 133 +++++++++---- 12 files changed, 1117 insertions(+), 99 deletions(-) create mode 100644 docs/14-legacy-cleanup.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 3554c11..88e60e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,127 @@ ## [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 Первый релиз линейки `v1`. @@ -119,8 +240,13 @@ Порядок перехода: 1. Выпишите с работающего сервера список пиров и их секреты. -2. Разверните `1.0.0` на чистом Debian 13 из release-пакета. -3. Заведите пиров заново и раздайте новые клиентские ссылки. +2. Очистите сервер: `tools/legacy/purge-v0.sh` или ручная процедура из + [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` в любом случае перестанут работать: смена обфускации — это изменение wire-совместимости. diff --git a/README.md b/README.md index 689f326..9a0b398 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,12 @@ hy2xs-install/ └── 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` и передаёт управление install‑only orchestrator. +При запуске `install.sh` пакет сначала проверяет `metadata/checksums.txt` и +выполняет read‑only clean‑host preflight **из распакованного архива**. Только +после этого он устанавливает orchestrator в +`/usr/local/lib/hy2xs/hy2xs-orchestrator`, создаёт symlink +`/usr/local/bin/hy2xs-orchestrator`, копирует package assets в +`/usr/local/lib/hy2xs/package` и передаёт управление install‑only orchestrator. ## Сетевая модель по умолчанию @@ -169,13 +174,18 @@ HY2XS **не привязан к конкретному номеру верси build machine target server ───────────── ───────────── определить последнюю стабильную ─┐ -скачать артефакт, посчитать SHA-256 │ -проверить, что бинарник принимает ├─► release‑пакет ──► скачать ровно -канонический конфиг HY2XS │ version + url этот артефакт, -заморозить version/url/sha256 ─┘ + sha256 сверить SHA-256 - и `hysteria version` +взять ожидаемый SHA-256 из │ +upstream hashes.txt │ +скачать артефакт и сверить его ├─► release‑пакет ──► скачать ровно +проверить, что бинарник принимает │ version + url этот артефакт, +канонический конфиг HY2XS │ + sha256 сверить SHA-256 +заморозить version/url/sha256 ─┘ и `hysteria version` ``` +Контрольная сумма берётся из upstream‑ассета `hashes.txt`, а не считается +только локально: локальный пересчёт подтверждает, что файл не изменился после +скачивания, но не доказывает, что скачан именно ожидаемый upstream artifact. + Что это даёт: - новая установка получает актуальную Hysteria без ручного обновления version lock; @@ -459,19 +469,33 @@ HY2XS_UI_PUBLIC_ACCESS=false ./install.sh --config /root/hy2xs-target.env --non-interactive ``` -Во время установки HY2XS: +Установка идёт в две фазы с жёсткой границей между ними. -1. проверит checksums release‑пакета; -2. установит orchestrator в `/usr/local/lib/hy2xs`; -3. создаст runtime‑каталоги и service users; -4. запишет `/etc/hy2xs/hy2xs.env`; -5. разложит bundled HY2XS admin; -6. скачает закреплённый в пакете Hysteria2 binary из upstream, проверит SHA256 и фактическую версию; -7. создаст `/etc/hysteria/config.yaml`; -8. установит systemd‑юниты; -9. применит nftables‑правила; -10. выполнит smoke‑checks; -11. зафиксирует успешное состояние в `/var/lib/hy2xs/install-state.json`. +**PHASE 0 — только чтение.** До её успешного завершения на сервере не +изменяется ни один файл, включая `/usr/local/lib/hy2xs`: + +1. проверит, что запущено от root; +2. проверит checksums release‑пакета; +3. запустит clean‑host preflight **из распакованного архива**: платформа + Debian 13 amd64, отсутствие предыдущей установки, валидность конфигурации. + +**PHASE 1 — применение изменений:** + +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. выполнит smoke‑checks; +13. зафиксирует успешное состояние в `/var/lib/hy2xs/install-state.json`. + +Если PHASE 0 не прошла, установщик завершается с ошибкой и **сервер остаётся в +том же состоянии, в котором был**. HY2XS v1 не устанавливается поверх +предыдущего поколения и не мигрирует его состояние: очистка старой установки — +отдельная явная операция, см. [docs/14-legacy-cleanup.md](docs/14-legacy-cleanup.md). ### 10. Получите bootstrap‑пароль админки @@ -654,14 +678,21 @@ hy2xs-orchestrator reconfigure \ | Команда | Назначение | | --- | --- | +| `hy2xs-orchestrator preflight-install` | Read‑only проверка чистоты хоста; ничего не меняет | | `hy2xs-orchestrator status` | Показать состояние платформы, сервисов, firewall и install marker | | `hy2xs-orchestrator doctor` | Выполнить preflight и smoke‑checks текущей установки | | `hy2xs-orchestrator reconfigure --dry-run` | Проверить конфиг без применения | | `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 redact-config` | Отредактировать секреты в env/yaml перед публикацией логов | +`repair` без `--allow-partial-state` работает только поверх полностью успешной +установки. В обоих режимах он сначала проверяет, что +`/var/lib/hy2xs/install-state.json` принадлежит текущему поколению продукта +(`product`, `release_line`, `config_schema_version`), и отказывается работать +поверх чужого состояния. + Пример сбора диагностики: ```bash @@ -708,6 +739,35 @@ permitopen 127.0.0.1:8080 localhost:8080 ## 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 Причина: домен имеет IPv6 AAAA‑запись, а HY2XS production profile является IPv4‑only. @@ -787,23 +847,27 @@ git status --short Подготовьте build env: +Версии и контрольные суммы toolchain **не задаются переменными окружения**: они +объявлены в корневом [`versions.env`](versions.env), и сборка берёт их оттуда. +Раньше их приходилось передавать снаружи, из-за чего воспроизводимая сборка в +чистой Debian‑среде требовала предварительного знания четырёх SHA‑256. + ```bash -export PACKAGE_VERSION=1.0.0 export BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) # Для переносимости между x86_64-серверами без AVX2 предпочтителен baseline artifact. +# Ожидаемый digest выбирается автоматически: в versions.env зафиксированы обе суммы. export BUN_FLAVOR=x64-baseline -# Builder требует SHA256 для скачиваемых toolchain-архивов. -# Значения нужно брать из официальных release/checksum источников для конкретных версий. -export GO_ARCHIVE_SHA256= -export NODE_ARCHIVE_SHA256= -export BUN_ARCHIVE_SHA256= - # Опционально: снимает anonymous rate limit при разрешении upstream-релиза. export GITHUB_TOKEN= ``` +`PACKAGE_VERSION` тоже приходит из `versions.env` (`HY2XS_VERSION`). Шаг +`verify_versions_contract` роняет сборку, если версия продукта, схема +конфигурации, целевая платформа или `packageManager` в `package.json` +разошлись с контрактом. + Запустите сборку: ```bash @@ -812,25 +876,28 @@ export GITHUB_TOKEN= Сборка последовательно: -1. прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`); -2. определяет последнюю стабильную версию Hysteria, скачивает артефакт и считает SHA‑256; -3. проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander; -4. собирает orchestrator, frontend и backend; -5. прогоняет `go vet` и `go test` для HY2XS admin; -6. формирует архив и прогоняет acceptance‑проверки. +1. проверяет контракт `versions.env` (`verify_versions_contract`); +2. прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`); +3. определяет последнюю стабильную версию Hysteria, берёт ожидаемый SHA‑256 из upstream `hashes.txt` и сверяет с ним скачанный артефакт; +4. проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS для Gecko и для Salamander; +5. собирает orchestrator, frontend и backend, проставляя версию админки из контракта; +6. прогоняет `go vet` и `go test` для HY2XS admin; +7. формирует архив и прогоняет acceptance‑проверки. -Любой сбой на шагах 1–5 останавливает сборку до создания пакета. +Любой сбой на шагах 1–6 останавливает сборку до создания пакета. Переменные, управляющие выбором версии 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_COMPAT_GATE` | `true` | Compatibility gate; для release‑сборок обязателен | +| `HYSTERIA_VERIFY_UPSTREAM_HASHES` | `true` | Сверять артефакт с upstream `hashes.txt`; отключение — только break‑glass | | `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в lock‑файл | -Полный E2E с реальным клиентом Hysteria запускается отдельно: +Полный E2E с реальным клиентом Hysteria запускается отдельно (нужен Go: ссылка +берётся из production‑генератора, а не из отдельной реализации внутри теста): ```bash 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 ├── tools/build/ # production builder и packaging pipeline ├── tools/test/ # end-to-end проверки с реальным клиентом Hysteria +├── tools/legacy/ # purge-v0.sh: очистка сервера от предыдущего поколения ├── docs/ # спецификации baseline, тестов и эксплуатации +├── versions.env # контракт продукта, платформы и toolchain ├── CHANGELOG.md ├── README.md └── LICENSE diff --git a/docs/02-build-layer-and-package.md b/docs/02-build-layer-and-package.md index c807ef4..a7617ba 100644 --- a/docs/02-build-layer-and-package.md +++ b/docs/02-build-layer-and-package.md @@ -40,20 +40,103 @@ Builder не является частью target install flow: на target serv - шаблоны для `post-install.env` - 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= +BUN_VERSION=1.3.13 +BUN_LINUX_X64_SHA256= +BUN_LINUX_X64_BASELINE_SHA256= +NODE_VERSION=20.19.0 +NODE_LINUX_X64_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 1. Проверяет структуру проекта. -2. Прогоняет тесты и типы оркестратора. -3. Разрешает upstream-версию Hysteria и проходит compatibility gate. -4. Компилирует оркестратор из Bun/TypeScript в install-артефакт. -5. Собирает / подготавливает HY2XS admin. -6. Прогоняет тесты HY2XS admin (после сборки frontend: `go:embed all:dist` требует готовых ассетов). -7. Копирует артефакты UI в package staging directory. -8. Кладёт entrypoint, templates, docs и service files. -9. Формирует итоговый install package. -10. Считает manifest/checksum. -11. Проверяет архив и прогоняет acceptance-проверки. -12. Выдаёт один переносимый результат для target machine. +2. Загружает и проверяет контракт `versions.env`. +3. Прогоняет тесты и типы оркестратора. +4. Разрешает upstream-версию Hysteria и проходит compatibility gate. +5. Компилирует оркестратор из Bun/TypeScript в install-артефакт. +6. Собирает / подготавливает HY2XS admin и сверяет его версию с контрактом. +7. Прогоняет тесты HY2XS admin (после сборки frontend: `go:embed all:dist` требует готовых ассетов). +8. Копирует артефакты UI в package staging directory. +9. Кладёт entrypoint, templates, docs и service files. +10. Формирует итоговый install package. +11. Считает manifest/checksum. +12. Проверяет архив и прогоняет acceptance-проверки. +13. Выдаёт один переносимый результат для target machine. ## Что builder не делает @@ -110,14 +193,19 @@ project/ `tools/build/build.sh` должен быть самодостаточным для Debian 13 amd64: -1. Проверяет ОС и архитектуру. +1. Проверяет ОС и архитектуру по `versions.env` (`HY2XS_BUILD_*`). 2. Проверяет структуру репозитория и lock-файлы. 3. Доставляет отсутствующие системные build-зависимости через `apt-get`. -4. Проверяет версии Go, Bun, Node.js и pnpm. -5. При несовпадении версий скачивает управляемый локальный toolchain в `.toolchain/`. +4. Проверяет версии Go, Bun, Node.js и pnpm по `versions.env`. +5. При несовпадении версий скачивает управляемый локальный toolchain в `.toolchain/` + и **сверяет каждый архив с контрольной суммой из `versions.env`**. 6. Собирает только Linux amd64 артефакты. 7. Записывает версии toolchain в metadata пакета. +Собственных значений по умолчанию у `tools/build/lib/deps.sh` больше нет: без +загруженного контракта сборка падает сразу, а не собирает пакет на неизвестном +toolchain. + ## Отношение к Hysteria2 Сам бинарь Hysteria2 **не вендорится** в install package как baseline-правило. @@ -135,10 +223,13 @@ SOURCE 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) @@ -165,8 +256,43 @@ TARGET SERVER | `HYSTERIA_VERSION_OVERRIDE` | пусто | Закрепить конкретную версию `vX.Y.Z` | | `HYSTERIA_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен | | `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `tools/build/hysteria-lock.env` | +| `HYSTERIA_VERIFY_UPSTREAM_HASHES` | `true` | Сверять артефакт с upstream `hashes.txt`; отключение — только break-glass | | `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`); - способ выбора версии фиксируется в `metadata/hysteria.resolution` и `metadata/package.env`; @@ -180,7 +306,7 @@ Gate защищает от ситуации, когда upstream меняет с Порядок: -1. скачать артефакт и сверить SHA-256; +1. скачать артефакт и сверить SHA-256 с upstream `hashes.txt`; 2. сверить `hysteria version` с разрешённой версией; 3. отрендерить канонический конфиг HY2XS тем же кодом, что работает на target (`orchestrator/tools/render-canonical-config.ts`); 4. запустить реальный бинарник Hysteria с этим конфигом — для Gecko и для Salamander; @@ -206,3 +332,5 @@ BUILD FAILED: unsupported Hysteria stable v2.13.0 6. Hysteria2 подтягивается install layer'ом с upstream по замороженным координатам, а не собирается на target из исходников 7. выход новой версии Hysteria после сборки не меняет содержимое уже собранного пакета 8. несовместимый upstream ломает сборку, а не установку у пользователя +9. контрольная сумма Hysteria подтверждена upstream-ассетом `hashes.txt`, а не только локальным пересчётом +10. версии продукта, платформы и toolchain объявлены в одном месте, а рассинхрон роняет сборку до создания tarball diff --git a/docs/04-admin-panel.md b/docs/04-admin-panel.md index 583e435..41ef947 100644 --- a/docs/04-admin-panel.md +++ b/docs/04-admin-panel.md @@ -79,11 +79,28 @@ HY2XS admin работает как надстройка над Hysteria YAML/AP - `access_token` в auth-URL и учётные данные, встроенные в URL; - `auth.password`, `auth.userpass`; - учётные данные 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 @@ -118,10 +135,41 @@ HY2XS admin работает как надстройка над Hysteria YAML/AP - Hysteria2 запускается отдельным `hysteria-server.service`; - HY2XS admin работает как operator UI и HTTP auth/traffic layer; - HY2XS admin не запускается от root; -- смена версии Hysteria2 через UI отключена в baseline; +- смена версии Hysteria2 через UI **отсутствует как API**; - список upstream releases не является частью operator UI baseline; - 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; diff --git a/docs/08-orchestrator-spec.md b/docs/08-orchestrator-spec.md index 605866d..7b0fdee 100644 --- a/docs/08-orchestrator-spec.md +++ b/docs/08-orchestrator-spec.md @@ -19,6 +19,7 @@ ## Главная роль оркестратора Оркестратор работает **только на target machine** и умеет: +- выполнить read-only проверку чистоты хоста (`preflight-install`) - выполнить первичную установку (`install`) - выполнить явную реконфигурацию (`reconfigure --dry-run|--apply`) - разложить bundled UI @@ -48,6 +49,93 @@ Если машина уже «жила своей жизнью», 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 должен попадать уже готовый package, содержащий: @@ -74,7 +162,7 @@ ## Что делает оркестратор по шагам -1. Проверяет, что ОС — Debian 13. +1. Проверяет, что ОС — Debian 13, и что хост чист (**до любой мутации**). 2. Проверяет базовые зависимости и install context. 3. Создаёт каталоги установки. 4. Разворачивает bundled HY2XS admin. @@ -115,19 +203,70 @@ ## CLI baseline Команды: +- `preflight-install --package-dir [--config ]` - `install --package-dir [--config ]` - `reconfigure --package-dir --config /etc/hy2xs/hy2xs.env --dry-run` - `reconfigure --package-dir --config /etc/hy2xs/hy2xs.env --apply` +- `repair --package-dir --config /etc/hy2xs/hy2xs.env [--allow-partial-state]` +- `redact-config --config (--in-place | --out ) [--format auto|env|yaml]` + +`preflight-install` не принимает `--skip-*`: эти флаги влияют на мутацию, а +PHASE 0 ничего не меняет. + +`--allow-partial-state` допустим только для `repair`. Инварианты: - только IPv4 bind/listen; - TLS modes: `acme | file | self_signed_dev`; - `trafficStats.secret` отдельный от `JWT_SECRET`; +- `HY2XS_CONFIG_SCHEMA_VERSION` — обязательное поле; его отсутствие трактуется + как legacy-конфигурация и отклоняется, а не заменяется значением по умолчанию; - install flow фиксирует фактически установленную версию Hysteria в snapshot; - версия/URL/SHA256 Hysteria берутся из metadata install package; -- `reconfigure` не обновляет бинарник Hysteria, только runtime-слой. +- `reconfigure` не обновляет бинарник Hysteria, только runtime-слой; - при `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 diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index 93d6f66..29e633d 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -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:`, верхний регистр, +противоречивые записи, отсутствие нужной строки. + ## 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:/hui/hysteria2/auth?access_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 diff --git a/docs/12-operations-and-troubleshooting.md b/docs/12-operations-and-troubleshooting.md index 15bf3d0..b2e270e 100644 --- a/docs/12-operations-and-troubleshooting.md +++ b/docs/12-operations-and-troubleshooting.md @@ -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 не работает Проверить: - разложился ли bundled UI diff --git a/docs/13-production-runbook.md b/docs/13-production-runbook.md index bb9a465..a19638f 100644 --- a/docs/13-production-runbook.md +++ b/docs/13-production-runbook.md @@ -156,5 +156,20 @@ hy2xs-orchestrator redact-config --config /etc/hysteria/config.yaml --out /root/ Инварианты: - команда не выводит исходные секреты в stdout; - требуется выбрать ровно один режим: `--in-place` или `--out `; -- `--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` на сервере, +который мы обещали не трогать. diff --git a/docs/14-legacy-cleanup.md b/docs/14-legacy-cleanup.md new file mode 100644 index 0000000..56ef4dc --- /dev/null +++ b/docs/14-legacy-cleanup.md @@ -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 && sudo systemctl disable +# sudo rm -f /etc/systemd/system/ + +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` неприменим и нужна очистка по этому документу. diff --git a/docs/README.md b/docs/README.md index 139231d..c204438 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,8 @@ - standalone update / rollback / uninstall subcommands: **вне scope** - сборка и упаковка: **отдельный локальный build layer** - post-install state: **`/etc/hysteria/post-install.env`** +- установка: **только на чистый хост**, миграция с 0.x не поддерживается +- контракт версий продукта/платформы/toolchain: **корневой `versions.env`** - клиентский delivery/access layer: **вне baseline этого пакета docs** ## Главная архитектурная схема @@ -38,7 +40,13 @@ 4. На target нет `npm` / `pnpm` / `yarn` / `bun install` / transpile step. 5. На target нет standalone логики update / rollback / uninstall. 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) 12. [12-operations-and-troubleshooting.md](12-operations-and-troubleshooting.md) 13. [13-production-runbook.md](13-production-runbook.md) +14. [14-legacy-cleanup.md](14-legacy-cleanup.md) История изменений проекта — в [CHANGELOG.md](../CHANGELOG.md). diff --git a/package/docs/README.md b/package/docs/README.md index 2b2c12f..60db7c5 100644 --- a/package/docs/README.md +++ b/package/docs/README.md @@ -23,3 +23,29 @@ ``` В 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`. Установщик её никогда не запускает сам. diff --git a/tools/build/README.md b/tools/build/README.md index c37f185..4b0c47d 100644 --- a/tools/build/README.md +++ b/tools/build/README.md @@ -16,7 +16,9 @@ dist/hy2xs-install-.tar.gz Основные части проекта: +- [`versions.env`](../../versions.env) — контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм. - [`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/package.sh`](lib/package.sh) — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива. - [`tools/build/lib/verify.sh`](lib/verify.sh) — проверка структуры репозитория и итогового архива. @@ -53,24 +55,49 @@ Windows и macOS можно использовать для редактиров При запуске builder: -1. Проверяет, что host — Debian 13 amd64. -2. Проверяет структуру репозитория. -3. Устанавливает недостающие системные build dependencies через `apt-get`. -4. Проверяет или скачивает локальные версии: - - Go `1.21.13`; - - Bun `1.3.13`; - - Node.js `20.19.0`; - - pnpm `9.15.9`. -5. Прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`). -6. Разрешает upstream-версию Hysteria, скачивает артефакт и считает SHA-256. -7. Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS. -8. Копирует package skeleton. -9. Собирает install-only orchestrator в standalone binary. -10. Собирает frontend и backend HY2XS admin в Linux amd64 binary. -11. Прогоняет `go vet` и `go test` для HY2XS admin (после сборки frontend, потому что `go:embed all:dist` требует готовых ассетов). -12. Записывает metadata и checksums. -13. Создаёт `dist/hy2xs-install-.tar.gz`. -14. Проверяет архив и прогоняет acceptance-проверки. +1. Загружает и валидирует [`versions.env`](../../versions.env). +2. Проверяет, что host соответствует `HY2XS_BUILD_*` (по умолчанию Debian 13 amd64). +3. Проверяет структуру репозитория. +4. Устанавливает недостающие системные build dependencies через `apt-get`. +5. Проверяет или скачивает локальные версии Go/Bun/Node.js/pnpm **из контракта** + и сверяет каждый архив с контрольной суммой из `versions.env`. +6. Выполняет `verify_versions_contract`: рассинхрон версий роняет сборку до создания tarball. +7. Прогоняет тесты и типы оркестратора (`bun test`, `tsc --noEmit`). +8. Разрешает upstream-версию Hysteria, берёт ожидаемый SHA-256 из upstream `hashes.txt` и сверяет с ним скачанный артефакт. +9. Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS. +10. Копирует package skeleton. +11. Собирает install-only orchestrator в standalone binary. +12. Собирает frontend и backend HY2XS admin в Linux amd64 binary, проставляя версию админки через ldflags. +13. Прогоняет `go vet` и `go test` для HY2XS admin (после сборки frontend, потому что `go:embed all:dist` требует готовых ассетов). +14. Записывает metadata и checksums. +15. Создаёт `dist/hy2xs-install-.tar.gz`. +16. Проверяет архив и прогоняет 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 @@ -81,11 +108,32 @@ Builder не хранит версию Hysteria вручную. По умолч 1. канонический upstream — `HyNetworks/hysteria`; 2. принимаются только стабильные релизы, без draft и prerelease; 3. тег должен иметь вид `app/vX.Y.Z`; -4. берётся ровно один артефакт `hysteria-linux-amd64`; +4. берётся ровно один артефакт `hysteria-linux-amd64` и ровно один `hashes.txt`; 5. URL используется в том виде, в каком его вернул upstream API, без пересборки строки; -6. SHA-256 считается локально от скачанного файла; +6. ожидаемый SHA-256 берётся из upstream `hashes.txt`, и скачанный бинарник сверяется с ним; 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`. После разрешения обязателен 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_COMPAT_GATE` | `true` | Compatibility gate; для release-сборок обязателен | +| `HYSTERIA_VERIFY_UPSTREAM_HASHES` | `true` | Сверять артефакт с upstream `hashes.txt`; отключение — только break-glass | | `HYSTERIA_WRITE_LOCK` | `false` | Записать разрешённые значения обратно в `hysteria-lock.env` | | `HYSTERIA_GATE_PORT` | `34443` | UDP-порт для временного запуска Hysteria в 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-baseline`. +Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В +[`versions.env`](../../versions.env) зафиксированы обе: + +```bash +BUN_LINUX_X64_SHA256= +BUN_LINUX_X64_BASELINE_SHA256= +``` + +Ожидаемый digest выбирается уже **после** `select_bun_artifact()`. + Можно принудительно задать flavor: ```bash @@ -171,12 +230,10 @@ BUN_FLAVOR=x64-baseline ./tools/build/build.sh ./tools/build/build.sh ``` -С явной версией и build id: +Версия пакета берётся из `versions.env`, поэтому обычно нужен только build id: ```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/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 всегда выполняет: ```bash @@ -225,7 +286,7 @@ bun install --frozen-lockfile Правила управления пакетами frontend: - [`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 всегда: ```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)` - `BUN_FLAVOR=auto|x64|x64-baseline` - `FRONTEND_NODE_OLD_SPACE_SIZE=2048` (default memory limit for frontend build step) - `TOOLCHAIN_DIR=/custom/path/.toolchain` - `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=` -- `NODE_ARCHIVE_SHA256=` -- `BUN_ARCHIVE_SHA256=` +Контрольные суммы toolchain больше **не передаются через окружение**: они +объявлены в `versions.env`. Раньше воспроизводимая сборка в чистой Debian-среде +требовала предварительного знания четырёх SHA-256 и не запускалась одной +командой. ## Политика памяти при сборке frontend @@ -292,7 +356,7 @@ echo "bun_exit=$?" rm -rf .toolchain/bun .toolchain/bun-tmp rm -f .toolchain/downloads/bun-linux-x64-*.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: @@ -300,7 +364,10 @@ PACKAGE_VERSION=1.0.0 ./tools/build/build.sh ```bash bash -n tools/build/build.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/hysteria.sh bash -n tools/build/lib/package.sh bash -n tools/build/lib/verify.sh +bash -n tools/build/lib/acceptance.sh ```