Files
HY2XS_flamy/tools/build
founder 6d1686b2be fix(admin): свести access-control к одному правилу и одному пути отзыва
Второй разбор того же слоя, уже по состоянию после 162759c. Тема: границы между
частями access-control. Прошлый проход починил одну операцию отзыва доступа и
оставил остальные; правило доступа при этом продолжало существовать в двух
экземплярах. Проведены три границы: состояние пира -> решение о доступе,
сохранённое изменение -> живая сессия, планировщик -> принадлежащая ему работа.

Правило доступа. Оно было записано двумя разными SQL-условиями: одним в выборке
Hysteria2Auth, другим в выборке cron. Второе не является отрицанием первого, и
расхождение приходилось ровно на границы — quota=0, usage=quota, now=expiresAt,
now=bannedUntil: авторизация отказывала, cron сессию не рвал. Условие cron
требовало СТРОГОГО превышения квоты, а счётчики растут порциями по ответу
Traffic Stats API, поэтому точное равенство — обычный исход очередного сбора.
Пир с исчерпанной квотой не пускался заново, но его живая сессия не разрывалась
никогда. Политика вынесена в peerAccessDenied; авторизация ищет пира только по
secret_digest, cron применяет ту же функцию. quota=-1 — единственный безлимит,
quota=0 — ноль байтов, bannedUntil=now — блокировка уже закончилась. Строка без
решающего поля трактуется как повреждённая и ведёт к отказу.

Операции, оставлявшие живую сессию. DeletePeer состоял из одного dao.DeletePeer:
строка исчезала вместе с auth_id, то есть вместе с единственным, чем эту сессию
можно было завершить, — состояние становилось невосстановимым. Разрыв при
изменении выполнялся только при disabled=1, поэтому мимо проходили смена
секрета, урезание квоты ниже израсходованного, перенос срока в прошлое и
снижение maxDevices. Импорт переписывает auth_id, секрет, квоту, срок и disabled
целиком и не трогал сессий вовсе. Все операции идут теперь через один
reconcileLiveSessions, а он — через disconnectAuthIDs, единственный вход к /kick:
он принимает готовые идентификаторы, дедуплицирует их, разбивает на части и не
обращается к базе. Импорт собирает старые auth_id ВНУТРИ транзакции (после
commit их в базе уже нет) и рвёт ПОСЛЕ commit (до него клиент успел бы
переподключиться к ещё не изменённому пиру). Правило асимметрично намеренно:
ограничение применяется немедленно, послабление — нет.

Цикл учёта. CronHandleAccount запускала горутину, которая запускала ещё две, —
для планировщика джоба заканчивалась почти мгновенно, поэтому StopCron не ждал
настоящей работы: releaseResource закрывал SQLite, а горутины продолжали в неё
писать. Параллельность обеих половин означала ещё и то, что enforcement читал
счётчики до записи снятой дельты. Джоба стала синхронной, под одним мьютексом на
весь цикл, порядок строгий. Закрыты три nil-разыменования — trafficSecretConfig,
item.AuthId и item.Id, — каждое из которых роняло процесс целиком вместе с
обработчиком machine-auth. Гейт Hysteria2IsRunning убран: util.Exec не отличает
«служба неактивна» от «спросить не удалось», и сломанный systemctl при живой
Hysteria молча отключал и учёт, и enforcement. Потеря дельты при отказе SQLite
больше не молчит: чтение /traffic?clear=1 деструктивно, и каждая потеря
считается. Checkpoint accounting в 1.0.0 намеренно не вводится — квота здесь
операционный предел доступа, а не учёт с финансово значимым каждым байтом.

Лимит устройств. Между чтением /online и ответом allow место ничем не
удерживалось: при online=max-1 два одновременных запроса получали разрешение
оба. Мьютекс вокруг /online этого не чинит — ответив allow, админка не создаёт
подключение, и следующий запрос продолжает видеть прежнее число. Появился
process-local учёт выданных, но ещё не проявившихся разрешений: решение по сумме
«подключено плюс зарезервировано», рост online снимает соответствующее их число,
протухшие снимаются по внутреннему TTL. Сеть опрашивается вне блокировки.

Гейты. Проверка «авторизация не возвращает успех из ветки ошибки» была записана
регуляркой err != nil \{[\s\S]*?return \*peer\.Id, а ленивый [\s\S]*? свободно
пересекает границы блоков: она даёт совпадение на коде из HEAD, то есть гейт
нельзя было удовлетворить, не сломав продукт. Тело ветки теперь выделяется по
балансу фигурных скобок, и логика проверена в обе стороны. go test -race стал
обязательным шагом сборки: состояние трекера разрешений и мьютекс цикла учёта
принадлежат процессу, и их корректность не наблюдаема ни в go test, ни в go vet;
пропуск при недоступном компиляторе не предусмотрен.

Панель. importPeerApi не объявлял skipErrorToast, а handleImport не имел ни try,
ни catch: после появления частичного результата отказ уходил бы необработанным
отклонением промиса, список не обновлялся бы при уже изменённой базе, а общий
перехватчик показал бы предупреждение красной ошибкой. Формулировка
peer_disconnect_failed во всех трёх местах сделана operation-neutral: через этот
код отчитываются восемь операций, а для удалённого пира прежняя фраза «новые
подключения пира запрещены» просто бессмысленна.
2026-09-01 20:46:21 +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

Тесты, проверка типов и проверка зависимостей переменными не управляются: у них нет аварийного выхода. Готовый пакет объявляет об этом полями 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-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 -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