Files
HY2XS_flamy/tools/build
founder 672d455467 fix: закрыть каналы утечки секретов и сделать PHASE 1 владением оркестратора
Hardening-проход перед первой сборкой на Debian. Три из найденного не
воспроизводились ни на одном dry-run и проявились бы только на живом сервере.

Установка

* preflight внутри install вызывался дважды и оба раза проверял clean-host.
  Ко второму вызову на диске лежал собственный /var/lib/hy2xs/install-state.json,
  записанный после первого preflight, и опознавался как маркер посторонней
  установки: КАЖДАЯ чистая установка падала сразу после apt-get с
  fatal_post_apply и оставляла сервер наполовину настроенным. Чистота хоста —
  условие входа в операцию, возможности платформы проверяются уже внутри
  PHASE 1, поэтому checkCleanHost стал отдельным параметром без умолчания.

* PHASE 1 начиналась в install.sh: shell сам создавал /usr/local/lib/hy2xs,
  ставил бинарник, вешал symlink и копировал runtime-пакет, и только потом
  запускал оркестратор с его собственным preflight. Отказ того preflight
  объявлялся fatal_pre_apply — «на сервере ничего не изменено» — при уже
  созданном каталоге оркестратора. Отследить владение мутацией невозможно,
  пока мутируют двое: install.sh больше не изменяет ничего, раскладку
  выполняет steps/bootstrap.ts под ownership.bootstrapTouched, пути попали
  в owned_paths. Как следствие удалено деление clean-host на фазы.

* diagnosticsCollect стояла перед rollback обычным await в install и в
  reconfigure. На заполненном диске она падает сама и отменяла откат целиком.
  Диагностика — best effort, откат — обязателен.

* reconfigure/repair выбирали записываемую фазу отказа регулярным выражением
  по тексту ошибки. Переведено на ownership-флаги.

Секреты

* Журнал админки писал RequestURI, то есть путь вместе с query. Hysteria
  обращается к /internal/hysteria/auth?access_token=<секрет> при каждом
  подключении пира, поэтому действующий machine token оседал открытым текстом
  в hy2xs-admin.log, который отдаётся через ExportLog и попадает в
  diagnostics-бандл. Логируется путь; значения query не пишутся, имена —
  пишутся. Канала было два: gin.Default() печатает path?query в stdout,
  оттуда в journald и в тот же бандл, — панель переведена на gin.New() +
  Recovery(). Журналы внутри бандла и журнал Hysteria из ExportLog теперь
  проходят санитайз. Сравнение токена — constant time.

* Config API позволял прочитать и подменить ключи приложения: getConfig и
  listConfig принимали произвольный ключ, а проверка записи была denylist'ом
  из трёх ключей оркестратора. Запрос ?key=PEER_SECRET_ENCRYPTION_KEY отдавал
  master-key шифрования секретов пиров. Доступ переведён на allowlist, маршрут
  getConfig удалён целиком — потребителей у него не было ни одного.

Пиры

* Импорт применялся по одной записи вне транзакции, вопреки собственному
  контракту. Валидация не знает, что уже лежит в базе: cross-conflict по
  UNIQUE(name) оставлял часть файла применённой. Применение выполняется одной
  транзакцией, криптоматериал считается до её открытия.

* Файл импорта мог содержать хвостовой JSON-документ, который молча не
  применялся. После разбора проверяется io.EOF.

* Экспорт разделён на «Экспорт настроек» и «Резервная копия» с секретами и
  подтверждением: обычный экспорт выдаёт пирам новые секреты при импорте, и
  прежние клиентские ссылки после переноса переставали работать.

Сборка

* Два stale-грепа в приёмке роняли build.sh в самом конце, внутри
  verify_archive. Первый искал в smoke.ts исчезнувший литерал URL, второй
  совпадал с router_test.go, который перечисляет удалённые маршруты, потому
  что проверяет их отсутствие: добавление регрессионного теста ломало сборку.

* verify_archive требовал наличия мутирующей строки в install.sh. Инвариант
  перевёрнут: их не должно быть ни одной.

Очистка

* Удалены entity.LegacyAccount, миграции 002/003 и мёртвые хелперы
  listSQLMigrationFiles и envInt: v1 не мигрирует базу 0.x ни при каком
  сценарии. Номера оставшихся миграций сохранены. H UI-словарь убран из
  обычных доков, в docs/14 он остаётся — там это имена объектов для удаления.

* Список непубличных IPv4 приведён к IANA Special-Purpose Address Registry:
  203.0.113.5 из RFC-примеров считался публичным адресом сервера. Отказ
  резолвера отделён от отсутствия A-записи.

Проверено: bun test 233, go test 71, tsc/vue-tsc, bash -n 11 скриптов,
приёмка прогнана против дерева.
2026-08-28 05:27:10 +05:00
..

HY2XS production builder

Этот каталог содержит production builder для HY2XS.

Builder собирает один переносимый install-archive:

dist/hy2xs-install-<version>.tar.gz

Этот архив переносится на production server, распаковывается и устанавливается через install.sh.

На production server не должно быть сборки из исходников: без go build, без bun install, без pnpm install, без frontend build и без TypeScript transpilation.

Что где лежит

Основные части проекта:

  • versions.env — контракт продукта, платформы и toolchain. Единственный источник истины для версий и контрольных сумм.
  • tools/build/build.sh — главный entrypoint сборки.
  • tools/build/lib/versions.sh — загрузка versions.env и verify_versions_contract.
  • tools/build/lib/deps.sh — проверка Debian/amd64, установка build dependencies, установка Go/Bun/Node.js/pnpm.
  • tools/build/lib/package.sh — сборка orchestrator, сборка HY2XS admin, создание stage directory и tar.gz архива.
  • tools/build/lib/verify.sh — проверка структуры репозитория и итогового архива.
  • tools/build/lib/acceptance.sh — acceptance-проверки production-контракта.
  • tools/build/lib/hysteria.sh — разрешение upstream-версии Hysteria и compatibility gate.
  • tools/build/hysteria-lock.env — fallback-значения для офлайн-сборки (HYSTERIA_CHANNEL=pinned).
  • tools/test/e2e-hysteria.sh — end-to-end проверка с реальным клиентом Hysteria.
  • orchestrator — TypeScript/Bun install-only orchestrator.
  • apps — HY2XS admin: Go backend и Vue frontend.
  • package — skeleton будущего install package: install.sh, templates, systemd units, default config.
  • dist — итоговые архивы. Создаётся builder'ом, в git обычно не хранится.
  • .toolchain — локальный toolchain builder'а. Создаётся автоматически, в git не хранится.

Требования к build machine

Production build поддерживается только на:

  • Debian 13;
  • amd64 / x86_64;
  • root или пользователь с sudo;
  • доступ в интернет.

Нужен outbound HTTPS/DNS до:

  • Debian apt repositories;
  • go.dev;
  • github.com;
  • nodejs.org;
  • npm registry.

Windows и macOS можно использовать для редактирования исходников, но финальную production-сборку надо делать на Debian 13 amd64.

Что builder делает сам

При запуске builder:

  1. Загружает и валидирует 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-<version>.tar.gz.
  16. Проверяет архив и прогоняет acceptance-проверки.

Контракт версий

Версии продукта, платформы и toolchain объявлены в корневом 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: это доказывает, что в бинарь попало то же значение.

Версия админки приезжает в бинарь через 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: разрешение и compatibility gate

Builder не хранит версию Hysteria вручную. По умолчанию он определяет последнюю стабильную версию сам и замораживает её в пакете.

Правила разрешения:

  1. канонический upstream — HyNetworks/hysteria;
  2. принимаются только стабильные релизы, без draft и prerelease;
  3. тег должен иметь вид app/vX.Y.Z;
  4. берётся ровно один артефакт hysteria-linux-amd64 и ровно один hashes.txt;
  5. URL используется в том виде, в каком его вернул upstream API, без пересборки строки;
  6. ожидаемый SHA-256 берётся из upstream hashes.txt, и скачанный бинарник сверяется с ним;
  7. разрешённые значения попадают в metadata пакета.

Почему hashes.txt, а не локальный пересчёт

Раньше SHA-256 считался от уже скачанного файла. Это защищает target от последующей подмены, но не доказывает, что builder скачал именно ожидаемый upstream artifact: сумма фиксирует то, что пришло, каким бы оно ни было.

Формат ассета:

6493dfff…f94  build/hysteria-linux-amd64
f24f63be…189  build/hysteria-linux-amd64-avx

Сопоставление идёт по базовому имени и строго на равенство: build/ — часть пути, а hysteria-linux-amd64-avx — другой артефакт, который не должен совпасть по префиксу. Разбор вынесен в parseUpstreamHashes (hysteriaRelease.ts) и покрыт юнит-тестами.

Источник ожидаемой суммы фиксируется в metadata как hysteria_sha_source.

Сравнение версий числовое, поэтому v2.9.10 считается новее v2.9.2.

После разрешения обязателен compatibility gate:

скачать бинарник
      ↓
сверить SHA-256 и `hysteria version`
      ↓
отрендерить канонический конфиг HY2XS тем же кодом, что и на target
      ↓
запустить настоящий Hysteria с этим конфигом (gecko и salamander)
      ↓
только после этого собирать release package

При несовместимости сборка останавливается:

BUILD FAILED: unsupported Hysteria stable v2.13.0

Это осознанное решение: ошибка должна проявиться на build machine, а не на production-сервере.

Переменные:

Переменная По умолчанию Назначение
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
GITHUB_TOKEN пусто Опционально: снимает anonymous rate limit GitHub API
SKIP_TESTS false Аварийное отключение тестов; для release-сборок недопустимо

Обновить lock-файл под текущий upstream:

HYSTERIA_WRITE_LOCK=true ./tools/build/build.sh

Важное про Bun и старые CPU

Bun имеет два Linux x64 artifact'а:

  • bun-linux-x64 — обычный build;
  • bun-linux-x64-baseline — build для CPU без AVX2.

Если CPU не поддерживает AVX2, обычный Bun может падать с Illegal instruction.

Builder автоматически проверяет /proc/cpuinfo.

По умолчанию:

BUN_FLAVOR=auto

Логика:

  • если CPU поддерживает AVX2, используется bun-linux-x64;
  • если CPU не поддерживает AVX2, используется bun-linux-x64-baseline.

Поскольку артефакта два, одной контрольной суммы архитектурно недостаточно. В versions.env зафиксированы обе:

BUN_LINUX_X64_SHA256=<sha256>
BUN_LINUX_X64_BASELINE_SHA256=<sha256>

Ожидаемый digest выбирается уже после select_bun_artifact().

Можно принудительно задать flavor:

BUN_FLAVOR=x64 ./tools/build/build.sh

или:

BUN_FLAVOR=x64-baseline ./tools/build/build.sh

Запуск

Из корня репозитория:

./tools/build/build.sh

Версия пакета берётся из versions.env, поэтому обычно нужен только build id:

BUILD_ID=prod-$(date -u +%Y%m%dT%H%M%SZ) ./tools/build/build.sh

Проверка результата

Проверка архива:

ls -lh dist/hy2xs-install-1.0.0.tar.gz
sha256sum dist/hy2xs-install-1.0.0.tar.gz | tee dist/hy2xs-install-1.0.0.tar.gz.sha256

Проверка обязательных файлов:

tar -tzf dist/hy2xs-install-1.0.0.tar.gz | grep -E '^(hy2xs-install/install.sh|hy2xs-install/orchestrator/hy2xs-orchestrator|hy2xs-install/ui/hy2xs-admin/hy2xs-admin|hy2xs-install/metadata/checksums.txt)$'

Проверка metadata:

tar -xOzf dist/hy2xs-install-1.0.0.tar.gz hy2xs-install/metadata/package.env

Когда обновлять orchestrator/bun.lock

Не перегенерируйте orchestrator/bun.lock во время production-сборки.

Обновлять и коммитить orchestrator/bun.lock следует только когда:

  • изменился orchestrator/package.json;
  • изменился BUN_VERSION в versions.env;
  • зависимости оркестратора обновляются осознанно.

При смене BUN_VERSION нужно обновить и packageManager в orchestrator/package.json, и обе контрольные суммы Bun: иначе verify_versions_contract остановит сборку.

Production builder всегда выполняет:

bun install --frozen-lockfile

Если команда падает, исправьте и закоммитьте lockfile в системе контроля версий. Не убирайте --frozen-lockfile.

Дисциплина lockfile для frontend

Не перегенерируйте frontend lock data во время обычной production-сборки.

Правила управления пакетами frontend:

  • apps/frontend/package.json объявляет "packageManager": "pnpm@9.15.9";
  • builder использует закреплённый pnpm из PNPM_VERSION в versions.env, и verify_versions_contract сверяет эти два значения;
  • production-путь установки frontend всегда:
pnpm install --frozen-lockfile

Если frozen install падает, обновите зависимости осознанно в системе контроля версий и закоммитьте изменения lockfile. Не убирайте --frozen-lockfile из сборочного потока.

Полезные переменные

  • 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

PACKAGE_VERSION берётся из versions.env (HY2XS_VERSION); переопределять его вручную нужно только для отладочных сборок, и verify_versions_contract такую сборку отклонит.

Контрольные суммы toolchain больше не передаются через окружение: они объявлены в versions.env. Раньше воспроизводимая сборка в чистой Debian-среде требовала предварительного знания четырёх SHA-256 и не запускалась одной командой.

Политика памяти при сборке frontend

Builder задаёт безопасный лимит heap для Node.js внутри bundle_ui():

--max-old-space-size=2048

Способы переопределения:

  • изменить значение по умолчанию для этой политики:
FRONTEND_NODE_OLD_SPACE_SIZE=3072 ./tools/build/build.sh
  • или передать полный набор собственных Node-опций (если --max-old-space-size там уже задан, builder не добавит второй):
NODE_OPTIONS="--max-old-space-size=3072" ./tools/build/build.sh

Диагностика

Проверить AVX2:

grep -m1 '^flags' /proc/cpuinfo | grep -qw avx2 && echo 'CPU has AVX2' || echo 'CPU has NO AVX2; Bun baseline is required'

Проверить Bun:

.toolchain/bun/bin/bun --version
echo "bun_exit=$?"

Если есть Illegal instruction, удалите старый Bun и пересоберите:

rm -rf .toolchain/bun .toolchain/bun-tmp
rm -f .toolchain/downloads/bun-linux-x64-*.zip
rm -f .toolchain/downloads/bun-linux-x64-baseline-*.zip
./tools/build/build.sh

Проверить shell syntax:

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