Разбор предыдущего прохода со сверкой по исходникам systemd v257.13 — той самой линии, что стоит на Debian 13. Тема та же и слоем глубже: контракт, объявленный шире, чем его принимает чужая сторона. Прошлый проход сделал транспорт lossless для значений, которые systemd принимает, но не спросил, какие значения он принимает вообще. 1. Домен значений файла окружения Перед тем как принять пару, systemd прогоняет ключ и значение через utf8_is_valid (src/basic/env-file.c, check_utf8ness_and_warn), и отказ там возвращает -EINVAL — то есть НЕзагруженный EnvironmentFile= и юнит, который не стартует, а не предупреждение. unichar_is_valid (src/basic/utf8.c) отвергает суррогаты, U+FDD0..U+FDEF и все code points вида *FFFE/*FFFF, а сам utf8_is_valid — встроенный NUL и невалидный UTF-8. Пароль "abcde" + U+FDD0 — шесть символов, восемь байт, ни одного управляющего — проходил панель, оркестратор, DTO и хеширование, записывался в hy2xs.env, и после этого админка не поднималась. Тот же класс дефекта, ради уничтожения которого контракт и существует, только слоем ниже. Введён IsEnvTransportableText (Go) / isEnvTransportable (TS), повторяющий множество systemd точно — не шире и не уже. Отдельно отвергаются одиночные суррогаты: строка JavaScript вправе их содержать, а TextEncoder молча заменяет непарный суррогат на U+FFFD, то есть без проверки в файл уехал бы ДРУГОЙ секрет, а не отказ. Заодно разделены домен транспорта и политика продукта. Проверка отвергала C0 и DEL с формулировкой «формат управляющих символов не несёт» — неправда: внутри двойных кавычек перевод строки накапливается как обычный байт и переживает round-trip. Именно эта подмена и позволила проверке не знать про noncharacters. Политика HY2XS теперь запрещает категорию Cc целиком (была шире кода ровно на C1) плюс U+FEFF — последний отдельным решением продукта, а не форматом: 0xFEFF & 0xFFFE это 0xFEFE, и systemd такое значение принимает. 2. Рецепт восстановления выполнял env-файл как код В docs/operations/12, раздел «Забыт пароль администратора», стояло `set -a; . /etc/hy2xs/hy2xs.env; set +a`. Строка стала опасной ровно тогда, когда файл научился нести произвольные значения. Для systemd HY2XS_ADMIN_INITIAL_PASSWORD="$(...)" — буквальное значение: подстановок в EnvironmentFile= нет вовсе. Но `.` обрабатывает файл bash, а bash внутри двойных кавычек выполняет подстановку команд — от root, прямо в рецепте восстановления доступа. Соседний раздел той же страницы при этом уже правильно запрещал source/eval для bootstrap-admin.secret: документ запрещал действие и тут же его предлагал. Рецепт читает нужные значения как ДАННЫЕ. Поставлен гейт приёмки, запрещающий возврат source/./eval над этими файлами в командах документации и в скриптах; гейт смотрит только внутрь ```-блоков, чтобы объяснение, называющее убранную конструкцию по имени, его не роняло. 3. Отказ приходил после мутаций хоста Проверка транспорта жила только внутри renderRuntimeEnv, то есть срабатывала на шаге «write runtime env» — уже после bootstrap оркестратора, установки пакетов и раскладки файловой системы, — а read-only preflight-install говорил PASS: он зовёт parseRuntimeEnv и ничего не рендерит. Детерминированно известная ошибка конфигурации роняла операцию, оставив за собой изменённый хост, что прямо противоречит контракту PHASE 0. validateRuntimeEnvTransport вызывается теперь из parseRuntimeEnv и проходит по ВСЕМ парам runtimeEnvEntries: ограничение принадлежит формату, а не полю пароля, и HY2XS_ADMIN_CON_PASS сломал бы загрузку юнита так же. 4. Точность порта автомата и его описания - в состоянии DOUBLE_QUOTE_VALUE_ESCAPE systemd пишет `c != '\n'`, а не проверку на любой перевод строки (в VALUE_ESCAPE — наоборот, strchr(NEWLINE, c)). Порт съедал и \<LF>, и \<CR>; - комментарий обещал одно намеренное расхождение с systemd, а их два: кроме строки без `=`, HY2XS отказывает и на незакрытой кавычке в конце файла. Оба fail-closed и теперь названы оба. Тесты: граничная таблица во всех слоях дополнена значениями вне домена (U+FDD0, U+FDEF, U+FFFE, U+FFFF, U+1FFFF, U+10FFFF, невалидный UTF-8), соседями диапазонов (U+FDCF, U+FDF0, U+FFFD, U+10FFFD), C1 и U+FEFF, одиночным суррогатом. Добавлены TestEnvTransportDomainMatchesSystemd (домен не шире и не уже) и TestProductPolicyIsWiderThanTransportDomain (домен и политика различимы), а также проверки fail-closed порядка: parseRuntimeEnv отвергает непригодную конфигурацию, проверяются все значения файла, запись и проверка ходят по одному списку пар. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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:
- Загружает и валидирует
versions.env. - Проверяет, что host соответствует
HY2XS_BUILD_*(по умолчанию Debian 13 amd64). - Проверяет структуру репозитория.
- Устанавливает недостающие системные build dependencies через
apt-get. - Проверяет или скачивает локальные версии Go/Bun/Node.js/pnpm из контракта
и сверяет каждый архив с контрольной суммой из
versions.env. - Выполняет
verify_versions_contract: рассинхрон версий роняет сборку до создания tarball. - Прогоняет тесты и типы оркестратора (
bun test,tsc --noEmit). - Разрешает upstream-версию Hysteria, берёт ожидаемый SHA-256 из upstream
hashes.txtи сверяет с ним скачанный артефакт. - Проходит compatibility gate: реальный бинарник Hysteria должен принять канонический конфиг HY2XS.
- Копирует package skeleton.
- Собирает install-only orchestrator в standalone binary.
- Собирает frontend и backend HY2XS admin в Linux amd64 binary, проставляя версию админки через ldflags.
- Прогоняет
go vetиgo testдля HY2XS admin (после сборки frontend, потому чтоgo:embed all:distтребует готовых ассетов). - Записывает metadata и checksums.
- Создаёт
dist/hy2xs-install-<version>.tar.gz. - Проверяет архив и прогоняет 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 вручную. По умолчанию он определяет последнюю стабильную версию сам и замораживает её в пакете.
Правила разрешения:
- канонический upstream —
HyNetworks/hysteria; - принимаются только стабильные релизы, без draft и prerelease;
- тег должен иметь вид
app/vX.Y.Z; - берётся ровно один артефакт
hysteria-linux-amd64и ровно одинhashes.txt; - URL используется в том виде, в каком его вернул upstream API, без пересборки строки;
- ожидаемый SHA-256 берётся из upstream
hashes.txt, и скачанный бинарник сверяется с ним; - разрешённые значения попадают в 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 |
Тесты, проверка типов и проверка зависимостей переменными не управляются: у них
нет аварийного выхода. Готовый пакет объявляет об этом полями tests_gate=true
и dependency_security_gate=true в metadata/package.env.
Обновить 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-baselineFRONTEND_NODE_OLD_SPACE_SIZE=2048(default memory limit for frontend build step)TOOLCHAIN_DIR=/custom/path/.toolchainVERIFY_TOOLCHAIN_CHECKSUMS=trueVERSIONS_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 -qw avx2 <<<"$(grep -m1 '^flags' /proc/cpuinfo)" && 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