Второй разбор того же слоя, уже по состоянию после 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: через этот
код отчитываются восемь операций, а для удалённого пира прежняя фраза «новые
подключения пира запрещены» просто бессмысленна.
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