diff --git a/README.md b/README.md index e2a11b3..167712a 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ HY2XS — production installer/runtime-manager для развёртывания - production builder на Debian 12 amd64 создаёт один переносимый архив; - install-only оркестратор на Bun + TypeScript поставляется на target как готовый compiled artifact; - HY2XS admin поставляется в составе пакета как bundled fork; -- vanilla Hysteria2 скачивается с official upstream во время установки; +- vanilla Hysteria2 устанавливается как pinned binary (version/url/sha256 из metadata install package); - runtime-конфиг управляется через `/etc/hy2xs/hy2xs.env` и команду `reconfigure`. В baseline намеренно не входят target-side build, rollback/uninstall orchestration, Telegram bot delivery, port hopping и generic server-manager функции. @@ -19,7 +19,6 @@ HY2XS — production installer/runtime-manager для развёртывания - [`package`](package) — skeleton итогового install package: entrypoint, templates, systemd units, package docs. - [`apps`](apps) — исходники HY2XS admin fork. - [`docs`](docs) — архитектурная документация и acceptance. -- [`hy2xs_implementation_plan-no_git`](hy2xs_implementation_plan-no_git) — roadmap и task breakdown. - [`dist`](dist) — итоговые install archives, создаются builder'ом и не хранятся в git. ## Сборка diff --git a/apps/controller/account.go b/apps/controller/account.go index 55a6ca4..63f9627 100644 --- a/apps/controller/account.go +++ b/apps/controller/account.go @@ -11,6 +11,7 @@ import ( "hy2xs-admin/service" "hy2xs-admin/util" "io" + "path/filepath" "strings" "time" ) @@ -299,7 +300,7 @@ func ExportAccount(c *gin.Context) { } fileName := fmt.Sprintf("AccountExport-%s.json", time.Now().Format("20060102150405")) - filePath := constant.ExportPathDir + fileName + filePath := filepath.Join(constant.ExportPathDir, fileName) if err = util.ExportFile(filePath, accountExports, 0); err != nil { vo.Fail(err.Error(), c) diff --git a/apps/controller/config.go b/apps/controller/config.go index e360748..6d87c79 100644 --- a/apps/controller/config.go +++ b/apps/controller/config.go @@ -14,6 +14,7 @@ import ( "hy2xs-admin/service" "hy2xs-admin/util" "io" + "path/filepath" "strconv" "strings" "time" @@ -230,7 +231,7 @@ func ExportHysteria2Config(c *gin.Context) { hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405")) - filePath := constant.ExportPathDir + fileName + filePath := filepath.Join(constant.ExportPathDir, fileName) if err = util.ExportFile(filePath, hysteria2ServerConfig, 1); err != nil { vo.Fail(err.Error(), c) @@ -258,7 +259,7 @@ func ExportConfig(c *gin.Context) { return } fileName := fmt.Sprintf("SystemConfig-%s.json", time.Now().Format("20060102150405")) - filePath := constant.ExportPathDir + fileName + filePath := filepath.Join(constant.ExportPathDir, fileName) if err = util.ExportFile(filePath, configs, 0); err != nil { vo.Fail(err.Error(), c) diff --git a/apps/middleware/local_only.go b/apps/middleware/local_only.go new file mode 100644 index 0000000..63dbd1a --- /dev/null +++ b/apps/middleware/local_only.go @@ -0,0 +1,26 @@ +package middleware + +import ( + "net" + "net/http" + + "github.com/gin-gonic/gin" + "hy2xs-admin/model/vo" +) + +func LocalOnlyHandler() gin.HandlerFunc { + return func(c *gin.Context) { + host, _, err := net.SplitHostPort(c.Request.RemoteAddr) + if err != nil { + host = c.ClientIP() + } + + if host != "127.0.0.1" && host != "::1" { + vo.Fail("local access only", c) + c.AbortWithStatus(http.StatusForbidden) + return + } + + c.Next() + } +} diff --git a/apps/router/hysteria2.go b/apps/router/hysteria2.go index ce2ceb6..188f859 100644 --- a/apps/router/hysteria2.go +++ b/apps/router/hysteria2.go @@ -5,12 +5,14 @@ import ( "hy2xs-admin/controller" ) -func initHysteria2AuthRouter(hysteria2Api *gin.RouterGroup) { +func initHysteria2MachineAuthRouter(hysteria2Api *gin.RouterGroup) { hysteria2 := hysteria2Api.Group("/hysteria2") { hysteria2.POST("/auth", controller.Hysteria2Auth) - } +} + +func initHysteria2SubscribeRouter(hysteria2Api *gin.RouterGroup) { hysteria2Api.GET("/:conPass", controller.Hysteria2Subscribe) } diff --git a/apps/router/router.go b/apps/router/router.go index 69cdeaf..ae5d3ea 100644 --- a/apps/router/router.go +++ b/apps/router/router.go @@ -8,34 +8,35 @@ import ( ) func Router(router *gin.Engine, huiWebContext *string) { - // global context relativePath := "/" if huiWebContext != nil && strings.HasPrefix(*huiWebContext, "/") { relativePath = *huiWebContext } + + frontend.InitFrontend(router, relativePath) + globalGroup := router.Group(relativePath) - { - globalGroup.Use(middleware.FilterHandler(), middleware.LogHandler(), middleware.RateLimiterHandler()) - frontend.InitFrontend(router, relativePath) + machineApi := globalGroup.Group("/hui") + machineApi.Use(middleware.LocalOnlyHandler(), middleware.LogHandler()) + initHysteria2MachineAuthRouter(machineApi) - authApi := globalGroup.Group("/hui") - { - initAuthRouter(authApi) - initHysteria2AuthRouter(authApi) - } + authApi := globalGroup.Group("/hui") + authApi.Use(middleware.FilterHandler(), middleware.LogHandler(), middleware.RateLimiterHandler()) + initAuthRouter(authApi) + initHysteria2SubscribeRouter(authApi) - globalGroup.Use(middleware.JWTHandler()) - - globalGroup.Use(middleware.AdminHandler()) - - huiAdminApi := globalGroup.Group("/hui") - { - initAccountAdminRouter(huiAdminApi) - initConfigRouter(huiAdminApi) - initHysteria2Router(huiAdminApi) - initLogRouter(huiAdminApi) - initMonitorRouter(huiAdminApi) - } - } + huiAdminApi := globalGroup.Group("/hui") + huiAdminApi.Use( + middleware.FilterHandler(), + middleware.LogHandler(), + middleware.RateLimiterHandler(), + middleware.JWTHandler(), + middleware.AdminHandler(), + ) + initAccountAdminRouter(huiAdminApi) + initConfigRouter(huiAdminApi) + initHysteria2Router(huiAdminApi) + initLogRouter(huiAdminApi) + initMonitorRouter(huiAdminApi) } diff --git a/docs/02-build-layer-and-package.md b/docs/02-build-layer-and-package.md index c377075..101b470 100644 --- a/docs/02-build-layer-and-package.md +++ b/docs/02-build-layer-and-package.md @@ -123,9 +123,10 @@ project/ - целевая установка должна брать его с official upstream на момент развёртывания Дополнительно: -- `HY2XS_HYSTERIA_VERSION=latest|vX.Y.Z` задаётся через runtime env; -- при `latest` оркестратор записывает **фактически установленную** версию в `post-install.env`; -- install flow использует download-to-temp + explicit execute + post-install verification binary/version. +- версия Hysteria2 фиксируется в metadata install package (`metadata/hysteria.version`); +- URL артефакта и SHA256 фиксируются в metadata install package (`metadata/hysteria.url`, `metadata/hysteria.sha256`); +- runtime `reconfigure` не обновляет и не откатывает бинарник Hysteria2; +- install flow валидирует SHA256 и фактическую версию установленного бинарника. ## Инварианты diff --git a/docs/03-server-hysteria2.md b/docs/03-server-hysteria2.md index 768145e..fbf4611 100644 --- a/docs/03-server-hysteria2.md +++ b/docs/03-server-hysteria2.md @@ -22,8 +22,9 @@ Hysteria2 — основной транспортный компонент се - по умолчанию install layer тянет **свежий upstream release / install source** - фактически установленная версия обязательно записывается в `post-install.env` -- поддерживаются политики `latest | vX.Y.Z` через `HY2XS_HYSTERIA_VERSION` -- при `vX.Y.Z` install обязан валидировать соответствие фактически установленной версии +- бинарник Hysteria2 устанавливается только на этапе `install` +- версия, URL и SHA256 фиксируются в metadata install package +- `reconfigure` не обновляет и не откатывает бинарник Hysteria2 ## Платформа diff --git a/docs/08-orchestrator-spec.md b/docs/08-orchestrator-spec.md index 3a04691..8b79ec0 100644 --- a/docs/08-orchestrator-spec.md +++ b/docs/08-orchestrator-spec.md @@ -78,7 +78,7 @@ 2. Проверяет базовые зависимости и install context. 3. Создаёт каталоги установки. 4. Разворачивает bundled HY2XS admin. -5. Скачивает installer Hysteria2 в temp-файл и выполняет install с policy `latest|vX.Y.Z`. +5. Скачивает pinned Hysteria2 binary из package metadata, проверяет SHA256 и выполняет install. 6. Генерирует Hysteria config. 7. Создаёт systemd unit для Hysteria. 8. Создаёт systemd unit для HY2XS admin. @@ -123,6 +123,8 @@ - TLS modes: `acme | file | self_signed_dev`; - `trafficStats.secret` отдельный от `JWT_SECRET`; - install flow фиксирует фактически установленную версию Hysteria в snapshot; +- версия/URL/SHA256 Hysteria берутся из metadata install package; +- `reconfigure` не обновляет бинарник Hysteria, только runtime-слой. - при `reconfigure --apply`: backup -> staged apply -> smoke -> rollback on fail. ## Что не реализовывать diff --git a/docs/12-operations-and-troubleshooting.md b/docs/12-operations-and-troubleshooting.md index ce0dfa3..041dc5d 100644 --- a/docs/12-operations-and-troubleshooting.md +++ b/docs/12-operations-and-troubleshooting.md @@ -20,7 +20,10 @@ - какую фактическую версию оркестратор установил - что записано в `HY2_VERSION` - не связано ли поведение со свежим upstream release -- какая policy была в `HY2XS_HYSTERIA_VERSION` (`latest|vX.Y.Z`) +- какая версия Hysteria зафиксирована в metadata установленного пакета + +Для обновления бинарника Hysteria2 используйте новый release install package. +Изменение runtime env не обновляет бинарник Hysteria2. ### 4. Оркестратор — Bun/TypeScript, но target не билдит его Если проблема в install flow, сначала смотреть: diff --git a/hy2xs_implementation_plan-no_git/00-README.txt b/hy2xs_implementation_plan-no_git/00-README.txt deleted file mode 100644 index e6d450f..0000000 --- a/hy2xs_implementation_plan-no_git/00-README.txt +++ /dev/null @@ -1,41 +0,0 @@ -HY2XS implementation plan - -Назначение -Этот пакет фиксирует полный план реализации системы HY2XS на основе ранее согласованных docs, но уже в прикладной форме: что именно делать, в каком порядке, как разложить проект и какие границы у каждого слоя. - -Зафиксированные решения -1. Название всей системы: HY2XS. -2. Название панели: HY2XS admin. -3. Серверный транспорт: ванильная Hysteria2 из official upstream во время установки. -4. UI: собственный fork H UI внутри проекта, поставляется вместе с пакетом. -5. Оркестратор: install-only, только под чистый Debian 12. -6. Стек оркестратора: Bun + TypeScript. -7. Builder layer: отдельно, локально, вне сервера. -8. Update / rollback / uninstall: вне scope. -9. UI переводим на русский или русский/английский, если двуязычность реализуется быстро и без раздувания scope. -10. Из UI вырезаем ссылки на оригинальный репозиторий, встроенный update-flow и любое поведение, завязанное на внешний upstream H UI. -11. Telegram-бот, backend выдачи доступа, remote profiles и похожий access layer — вне scope этого плана. - -Состав плана -01-scope-and-boundaries.txt -02-target-repo-structure.txt -03-builder-layer.txt -04-runtime-package-layout.txt -05-orchestrator-install-flow.txt -06-hysteria-runtime-layer.txt -07-hy2xs-admin-fork-plan.txt -08-localization-and-rebranding.txt -09-access-layer-out-of-scope.txt -10-post-install-env-and-config-policy.txt -11-testing-acceptance-and-smoke.txt -12-phased-roadmap.txt -13-task-breakdown-checklist.txt - -Что считать результатом -Результат этой работы — не абстрактные рассуждения, а один репозиторий/проект HY2XS, внутри которого: -- есть локальный builder; -- есть install-only orchestrator на Bun/TypeScript; -- есть встроенный fork HY2XS admin; -- есть шаблоны и unit-файлы; -- есть единый пакет для переноса на чистый Debian 12; -- сервер после установки готов к работе с Hysteria2, UI и базовым server environment. diff --git a/hy2xs_implementation_plan-no_git/01-scope-and-boundaries.txt b/hy2xs_implementation_plan-no_git/01-scope-and-boundaries.txt deleted file mode 100644 index 26933f8..0000000 --- a/hy2xs_implementation_plan-no_git/01-scope-and-boundaries.txt +++ /dev/null @@ -1,46 +0,0 @@ -HY2XS: scope and boundaries - -1. Что входит в реализацию -- Локальный builder layer. -- Итоговый install package. -- Install-only orchestrator для чистого Debian 12. -- Оркестратор на Bun + TypeScript. -- Vanilla Hysteria2 runtime, скачиваемая с official upstream во время установки. -- Встроенный fork панели HY2XS admin. -- Systemd unit-файлы. -- Базовый nftables baseline. -- post-install.env. -- Документация и acceptance checks. - -2. Что не входит в реализацию -- Telegram-бот. -- Backend/контур для remote profiles и выдачи ключей. -- Update manager. -- Rollback manager. -- Uninstall. -- Поддержка грязных или давно живущих серверов. -- Target-side build. -- Docker baseline. -- Multi-node и cluster-архитектура. -- Port hopping в первой версии. -- Автоматическая миграция старых H UI состояний. -- Полноценная собственная клиентская программа. - -3. Что считаем правильной эксплуатационной моделью -- Сборка выполняется локально. -- На сервер переносится только готовый пакет. -- Сервер выполняет только первичную установку и базовую настройку. -- Если сервер сломан или состояние стало непрозрачным, штатный путь — переустановка ОС и повторный install. - -4. Источники истины по слоям -- Runtime transport: Hysteria2 config + runtime state. -- UI layer: наш fork HY2XS admin. -- Install facts: /etc/hysteria/post-install.env. -- Install logic: исходники оркестратора на Bun/TypeScript + собранный install-артефакт. - -5. Главные архитектурные запреты -- Не скачивать HY2XS admin с внешнего upstream на target. -- Не компилировать UI на target. -- Не выполнять `bun install` / transpile / compile оркестратора на target. -- Не пытаться сделать оркестратор инструментом полного жизненного цикла. -- Не смешивать install baseline и access platform в одном scope. diff --git a/hy2xs_implementation_plan-no_git/02-target-repo-structure.txt b/hy2xs_implementation_plan-no_git/02-target-repo-structure.txt deleted file mode 100644 index 0d26bd4..0000000 --- a/hy2xs_implementation_plan-no_git/02-target-repo-structure.txt +++ /dev/null @@ -1,95 +0,0 @@ -HY2XS: target repo structure - -Цель -Структура должна жёстко разделять builder, runtime package, fork UI, документацию и server-side install logic. - -Рекомендуемая структура - -project/ -├── README.md -├── docs/ -│ ├── architecture/ -│ ├── implementation/ -│ └── operations/ -├── builder/ -│ ├── build.sh -│ ├── lib/ -│ │ ├── common.sh -│ │ ├── package.sh -│ │ ├── ui.sh -│ │ └── verify.sh -│ ├── manifests/ -│ │ └── package.manifest -│ └── output/ -├── package/ -│ ├── install.sh -│ ├── orchestrator/ -│ │ └── hy2xs-orchestrator -│ ├── templates/ -│ │ ├── hysteria/ -│ │ ├── nftables/ -│ │ └── env/ -│ ├── systemd/ -│ │ ├── hysteria-server.service -│ │ └── hy2xs-admin.service -│ ├── ui/ -│ │ └── hy2xs-admin/ -│ ├── docs/ -│ └── metadata/ -│ ├── package.version -│ ├── package.build_id -│ └── checksums.txt -├── orchestrator/ -│ ├── package.json -│ ├── bun.lock -│ ├── tsconfig.json -│ ├── src/ -│ │ ├── cli.ts -│ │ ├── commands/ -│ │ │ └── install.ts -│ │ ├── steps/ -│ │ │ ├── preflight.ts -│ │ │ ├── deps.ts -│ │ │ ├── filesystem.ts -│ │ │ ├── hysteria.ts -│ │ │ ├── ui.ts -│ │ │ ├── systemd.ts -│ │ │ ├── firewall.ts -│ │ │ ├── env.ts -│ │ │ └── smoke.ts -│ │ ├── lib/ -│ │ └── types/ -│ └── dist/ -├── ui/ -│ └── hy2xs-admin-fork/ -│ ├── upstream-base/ -│ ├── app/ -│ ├── assets/ -│ ├── locales/ -│ │ ├── ru/ -│ │ └── en/ -│ ├── branding/ -│ ├── patches/ -│ └── BUILD_NOTES.md -├── config/ -│ └── hy2xs.env -└── dist/ - └── hy2xs-install-.tar.gz - -Что важно -1. builder/ и orchestrator/ разделены. -2. package/ — staging area и состав будущего install package. -3. orchestrator/ — исходники install-only оркестратора на Bun/TypeScript. -4. package/orchestrator/ — уже собранный install-артефакт, а не исходники для target-side build. -5. ui/hy2xs-admin-fork/ — постоянный исходник нашего форка. -6. dist/ — только финальные артефакты. - -Минимальная допустимая упрощённая структура -Если хочешь не раздувать проект на старте, можно сократить до: -- builder/ -- orchestrator/ -- package/ -- ui/hy2xs-admin-fork/ -- docs/ -- dist/ -Но логическое разделение всё равно должно сохраниться. diff --git a/hy2xs_implementation_plan-no_git/03-builder-layer.txt b/hy2xs_implementation_plan-no_git/03-builder-layer.txt deleted file mode 100644 index 18ae506..0000000 --- a/hy2xs_implementation_plan-no_git/03-builder-layer.txt +++ /dev/null @@ -1,64 +0,0 @@ -HY2XS: builder layer plan - -Цель builder layer -На Debian 12 amd64 build host собрать один чистый install package, который можно перенести на чистый Debian 12 без target-side build. - -Технологический выбор -Baseline packaging: shell-first. -- Основной packaging pipeline: sh/bash. -- Оркестратор при этом пишется на Bun + TypeScript. -- Builder на Debian 12 amd64 собирает install-артефакт оркестратора. -- На target не должно быть обязательного JS/TS toolchain шага. - -Задачи builder layer -1. Проверка структуры репозитория. -2. Проверка наличия обязательных файлов пакета. -3. Подготовка HY2XS admin fork к поставке. -4. Локальная сборка оркестратора из Bun/TypeScript. -5. Копирование UI-артефактов в package staging. -6. Копирование templates, systemd units, docs и examples. -7. Генерация metadata: version, build_id, checksums. -8. Упаковка итогового архива. - -Порядок реализации builder -Этап 1. -- Создать tools/build/build.sh. -- Создать tools/build/lib/common.sh. -- Создать tools/build/lib/verify.sh. -- Создать tools/build/lib/package.sh. -- Создать tools/build/lib/deps.sh. -- Создать tools/build/README.md. - -Production-дополнение. -- Проверять Debian 12 amd64. -- Самостоятельно доставлять apt build-зависимости. -- Проверять и фиксировать версии Go/Bun/Node.js/pnpm. -- Использовать локальный управляемый toolchain при несовпадении версий. - -Этап 2. -- Реализовать очистку package staging directory. -- Реализовать копирование package/ skeleton. -- Реализовать сборку `orchestrator/` и копирование артефакта в `package/orchestrator/`. -- Реализовать копирование ui/hy2xs-admin-fork в package/ui/hy2xs-admin. - -Этап 3. -- Реализовать запись package.version и package.build_id. -- Реализовать checksums.txt. -- Реализовать создание dist/hy2xs-install-.tar.gz. - -Этап 4. -- Добавить builder smoke-проверку: архив собрался, в нём есть install.sh, оркестратор, UI, unit-файлы, templates. - -Что builder не делает -- Не качает Hysteria2. -- Не выполняет серверные действия. -- Не меняет production state. -- Не содержит uninstall/update логики. -- Не включает access/bot backend в baseline package. - -Acceptance criteria -- Одна команда локально создаёт переносимый install package. -- В package нет builder scripts. -- В package есть встроенный HY2XS admin. -- В package есть собранный Bun/TypeScript orchestrator. -- package можно передать на сервер без дополнительной сборки. diff --git a/hy2xs_implementation_plan-no_git/04-runtime-package-layout.txt b/hy2xs_implementation_plan-no_git/04-runtime-package-layout.txt deleted file mode 100644 index 2178f03..0000000 --- a/hy2xs_implementation_plan-no_git/04-runtime-package-layout.txt +++ /dev/null @@ -1,50 +0,0 @@ -HY2XS: runtime package layout - -Цель -Зафиксировать состав итогового install package, который должен попасть на target machine. - -Состав пакета -1. install entrypoint -- install.sh - -2. orchestrator artifact -- hy2xs-orchestrator - -3. templates -- Hysteria config template -- nftables template -- post-install.env template - -4. systemd units -- hysteria-server.service -- hy2xs-admin.service - -5. bundled UI -- HY2XS admin runtime files -- статические ассеты -- локали -- branding assets - -6. metadata -- package.version -- package.build_id -- checksums.txt - -7. docs/config -- короткий README по установке -- canonical runtime source config (hy2xs.env) - -Что не должно быть в runtime package -- builder/ -- git history -- upstream remote URLs H UI -- update scripts панели -- ссылки на оригинальный бренд/репозиторий H UI в интерфейсе и документации пакета -- target-side dependency installation для оркестратора -- Telegram/access backend как обязательная часть baseline package - -Что должен делать install.sh -- быть единой точкой входа на target -- вызывать собранный артефакт оркестратора -- логировать запуск -- завершаться с понятным кодом ошибки diff --git a/hy2xs_implementation_plan-no_git/05-orchestrator-install-flow.txt b/hy2xs_implementation_plan-no_git/05-orchestrator-install-flow.txt deleted file mode 100644 index d411dee..0000000 --- a/hy2xs_implementation_plan-no_git/05-orchestrator-install-flow.txt +++ /dev/null @@ -1,86 +0,0 @@ -HY2XS: orchestrator install flow - -Главная роль -Install-only orchestrator ставит систему на чистый Debian 12 и больше ничего не обещает. - -Технологическая фиксация -- Исходники оркестратора: Bun + TypeScript. -- Сборка оркестратора: локально, builder layer'ом. -- Исполнение на target: готовый install-артефакт через thin wrapper. - -Рекомендуемый порядок модулей -1. preflight -- проверить Debian 12 -- проверить root/sudo context -- проверить, что порт свободен -- проверить, что не существует конфликтующей старой установки - -2. deps -- установить системные зависимости -- проверить наличие systemd, nft, curl/wget, tar, openssl и прочего необходимого минимума - -3. filesystem -- создать системного пользователя hysteria -- создать каталоги: - /etc/hysteria - /var/lib/hysteria - /opt/hy2xs-admin - /var/lib/hy2xs-admin - /var/log/hy2xs - /usr/local/lib/hy2xs - -4. bundled UI deploy -- разложить HY2XS admin из пакета -- назначить владельца и права - -5. Hysteria install -- скачать свежую Hysteria2 из official upstream -- установить бинарь в согласованный путь -- зафиксировать фактическую версию - -6. config generation -- сгенерировать /etc/hysteria/config.yaml -- сгенерировать параметры obfs -- записать домен, порт, bandwidth policy - -7. systemd -- разложить hysteria-server.service -- разложить hy2xs-admin.service -- daemon-reload -- enable services - -8. firewall -- применить baseline nftables -- не терять SSH - -9. post-install.env -- создать /etc/hysteria/post-install.env -- записать package version, build id, orchestrator stack/build, Hysteria version, UI build info, порты и пути - -10. start and smoke -- стартовать сервисы -- проверить systemctl is-active -- проверить UDP listen -- проверить доступность UI - -Политика ошибок -- Любой конфликт неизвестного старого состояния = stop with error. -- Никакой сложной автомиграции. -- Ошибки должны быть текстовыми и пригодными для диагностики. - -CLI baseline -Допустимые флаги: -- --non-interactive -- --port -- --ssh-port -- --skip-firewall -- --skip-start -- --ui-port -- --ui-bind-host - -Что не реализовывать -- update subcommands -- rollback subcommands -- uninstall subcommands -- reconcile logic -- bot/access-delivery subcommands diff --git a/hy2xs_implementation_plan-no_git/06-hysteria-runtime-layer.txt b/hy2xs_implementation_plan-no_git/06-hysteria-runtime-layer.txt deleted file mode 100644 index 703638d..0000000 --- a/hy2xs_implementation_plan-no_git/06-hysteria-runtime-layer.txt +++ /dev/null @@ -1,42 +0,0 @@ -HY2XS: Hysteria runtime layer plan - -Цель -Сделать серверный транспорт предсказуемым и полностью ванильным со стороны Hysteria2. - -Решения -1. Hysteria2 не форкается. -2. Скачивается во время установки с official upstream. -3. Используется одна baseline-схема без port hopping. - -Базовый runtime policy -- Listen: 0.0.0.0: -- Transport: QUIC/UDP -- IPv4-only -- obfs.type: salamander -- obfs.password: генерируется при установке -- bandwidth.up: 50 Mbps -- bandwidth.down: 50 Mbps -- ignoreClientBandwidth: false - -Что надо реализовать -1. Шаблон server config. -2. Генератор переменных для шаблона. -3. Проверку валидности конфига перед запуском. -4. Фиксацию фактической версии Hysteria в post-install.env. - -Нужно зафиксировать в коде -- Единый путь к конфигу. -- Единый путь к бинарю. -- Единый путь к data dir. -- Единый набор сетевых переменных. - -Что не смешивать -- Не смешивать UI-состояние и transport-конфиг в одном месте. -- Не полагаться на host TCP BBR как на главный механизм управления Hysteria. -- Не смешивать install runtime layer и access/delivery layer. - -Acceptance criteria -- Сервис стартует через systemd. -- Конфиг читается без ошибки. -- Тестовый совместимый клиент подключается. -- Политика speed limit соответствует согласованной серверной и клиентской конфигурации. diff --git a/hy2xs_implementation_plan-no_git/07-hy2xs-admin-fork-plan.txt b/hy2xs_implementation_plan-no_git/07-hy2xs-admin-fork-plan.txt deleted file mode 100644 index eeaee7b..0000000 --- a/hy2xs_implementation_plan-no_git/07-hy2xs-admin-fork-plan.txt +++ /dev/null @@ -1,60 +0,0 @@ -HY2XS admin: fork implementation plan - -Цель -Превратить H UI в наш поддерживаемый внутренний компонент HY2XS admin. - -Главные задачи -1. Забрать исходный UI в собственный fork внутри проекта. -2. Отвязать интерфейс от оригинального бренда, ссылок и update-потока. -3. Подготовить UI к поставке внутри install package. -4. Сделать UI отдельным systemd-сервисом. - -Обязательные изменения в форке -A. Ребрендинг -- Новое имя продукта: HY2XS. -- Новое имя панели: HY2XS admin. -- Заменить названия в заголовках, логотипах, footer, title, README панели, системных сообщениях. - -B. Удаление upstream-зависимостей -- Удалить ссылки на оригинальный GitHub/Git repository. -- Удалить кнопки, меню и тексты, предлагающие update из внешнего upstream. -- Удалить или скрыть экран/логику самопроверки обновлений, если она встроена. -- Удалить любой текст вида "official repo", "check updates", "new version available" относительно оригинального H UI. - -C. Локализация -- Минимум: полный русский. -- Предпочтительно: русский + английский, если это делается быстро через словари/locale files без переписывания UI-логики. -- Китайский как основная locale больше не нужен для поставки HY2XS. - -D. Packaging readiness -- У UI должен быть предсказуемый build/runtime output. -- UI должен работать из локально поставленных файлов, без git clone и без target-side npm/yarn/pnpm build. - -Слои задач по форку -Этап 1. Первичная инвентаризация -- Найти все brand strings. -- Найти все repo/update ссылки. -- Найти все locales. -- Найти все места, где UI показывает своё имя. - -Этап 2. Ребрендинг -- Переименовать UI в HY2XS admin. -- Подменить logo/title/favicon, если есть. -- Обновить системные тексты. - -Этап 3. Вырезание update-flow -- Удалить пункты меню обновлений. -- Удалить backend/frontend обработчики update-функций. -- Удалить внешние endpoints и тексты об обновлениях. - -Этап 4. Локализация -- Вынести строки в locale-файлы, если это ещё не сделано. -- Создать ru locale. -- Опционально создать en locale. -- Проверить, что UI не содержит жёстко зашитых китайских строк. - -Этап 5. Runtime packaging -- Подготовить результат, который builder просто копирует в package/ui/hy2xs-admin. - -Техническое правило -Source-of-truth install lifecycle остаётся у оркестратора. HY2XS admin не должен расширять scope установки и не должен превращаться в update-manager. diff --git a/hy2xs_implementation_plan-no_git/08-localization-and-rebranding.txt b/hy2xs_implementation_plan-no_git/08-localization-and-rebranding.txt deleted file mode 100644 index 7c5112d..0000000 --- a/hy2xs_implementation_plan-no_git/08-localization-and-rebranding.txt +++ /dev/null @@ -1,44 +0,0 @@ -HY2XS: localization and branding plan - -Цель -Сделать систему цельной по названию, языку интерфейса и операторскому UX. - -1. Названия -- Система: HY2XS -- Панель: HY2XS admin -- Service names: - - hysteria-server.service - - hy2xs-admin.service -- Пути и package naming должны использовать hy2xs как canonical slug - -2. Где переименовывать -- UI titles -- navbar/header/footer -- login page -- browser tab title -- package metadata -- docs -- install output -- post-install.env package name -- systemd description lines - -3. Политика языка -Минимум для первой версии: -- русский интерфейс панели -- русский install output/docs для оператора - -Предпочтительная быстрая модель: -- RU по умолчанию -- EN как дополнительная locale, если её можно добавить быстро -- никакой сложной i18n-платформы, если в исходном UI уже есть простой словарный механизм - -4. Что удалить -- китайские дефолтные тексты в видимых местах -- оригинальные названия продукта -- упоминания оригинальной панели как управляемого внешнего продукта - -5. Acceptance criteria -- В UI нет китайского языка в обычном операторском пути. -- В UI нет оригинального названия H UI. -- Во всех ключевых местах виден бренд HY2XS / HY2XS admin. -- Если включён EN, переключение не ломает layout. diff --git a/hy2xs_implementation_plan-no_git/09-access-layer-out-of-scope.txt b/hy2xs_implementation_plan-no_git/09-access-layer-out-of-scope.txt deleted file mode 100644 index 2ded114..0000000 --- a/hy2xs_implementation_plan-no_git/09-access-layer-out-of-scope.txt +++ /dev/null @@ -1,29 +0,0 @@ -HY2XS: access layer out of scope - -Цель -Явно убрать из плана всё, что не относится к install-only baseline. - -Что вне scope -- Telegram-бот. -- Backend выдачи ключей. -- Remote profile publishing. -- Deep links. -- Billing/подписки. -- Self-service кабинет. -- Любой обязательный пользовательский delivery layer. - -Что остаётся в scope -- Установка Hysteria2. -- Установка HY2XS admin. -- Настройка server config. -- Настройка systemd. -- Настройка firewall. -- Генерация post-install.env. - -Почему это важно -Если оставить access layer внутри baseline-плана, документация начинает неверно описывать продукт как платформу выдачи доступа. По факту здесь нужен только оркестратор установки и базовой серверной конфигурации. - -Минимальный deliverable -- Один install package. -- Один install-only orchestrator. -- Один reproducible install flow для чистого Debian 12. diff --git a/hy2xs_implementation_plan-no_git/10-post-install-env-and-config-policy.txt b/hy2xs_implementation_plan-no_git/10-post-install-env-and-config-policy.txt deleted file mode 100644 index 0f74238..0000000 --- a/hy2xs_implementation_plan-no_git/10-post-install-env-and-config-policy.txt +++ /dev/null @@ -1,53 +0,0 @@ -HY2XS: post-install env and config policy - -Цель -Оставить после установки один прозрачный reference file с фактами развёртывания. - -Путь -/etc/hysteria/post-install.env - -Что обязательно писать -Deploy/package: -- PACKAGE_NAME=HY2XS -- PACKAGE_VERSION -- PACKAGE_BUILD_ID -- DEPLOY_TIMESTAMP -- DEPLOY_TARGET_OS=debian-12 - -Orchestrator: -- ORCH_SOURCE_STACK=bun-typescript -- ORCH_BUILD_MODE -- ORCH_BUILD_ID -- ORCH_ENTRYPOINT - -Network/common: -- DEPLOY_DOMAIN -- SSH_PORT - -Hysteria: -- HY2_SOURCE=official-upstream -- HY2_VERSION -- HY2_LISTEN_HOST -- HY2_PORT -- HY2_OBFS_TYPE=salamander -- HY2_OBFS_PASSWORD -- HY2_BANDWIDTH_UP_Mbps=50 -- HY2_BANDWIDTH_DOWN_Mbps=50 -- HY2_IGNORE_CLIENT_BANDWIDTH=false -- HY2_CONFIG_PATH - -HY2XS admin: -- HUI_ENABLED=true -- HUI_FORK_REF -- HUI_BUILD_ID -- HUI_BIND_HOST -- HUI_PORT -- HUI_INSTALL_DIR -- HUI_DATA_DIR -- HUI_BRAND=HY2XS admin -- HUI_DEFAULT_LOCALE=ru - -Правило использования -- post-install.env — reference file. -- Изменение этого файла само по себе не должно считаться применением runtime-изменений. -- Любые ручные правки оператора должны потом осознанно переноситься в реальные рабочие конфиги и применяться документированным способом. diff --git a/hy2xs_implementation_plan-no_git/11-testing-acceptance-and-smoke.txt b/hy2xs_implementation_plan-no_git/11-testing-acceptance-and-smoke.txt deleted file mode 100644 index 1b7dafa..0000000 --- a/hy2xs_implementation_plan-no_git/11-testing-acceptance-and-smoke.txt +++ /dev/null @@ -1,48 +0,0 @@ -HY2XS: testing, acceptance and smoke plan - -1. Builder checks -- package успешно собирается локально -- в архиве есть UI, install entrypoint, orchestrator artifact, templates, units, metadata -- нет builder мусора в финальном package - -2. Target install checks -- чистый Debian 12 -- install flow проходит без ручной сборки -- Hysteria скачана с upstream -- HY2XS admin разложен из bundled package -- созданы оба systemd unit -- создан post-install.env -- post-install.env фиксирует Bun/TypeScript orchestrator stack - -3. Runtime checks -- systemctl is-active hysteria-server = active -- systemctl is-active hy2xs-admin = active -- UDP порт слушается -- SSH не потерян после firewall -- UI доступен на заданном bind host/port -- тестовый совместимый клиент подключается - -4. Branding/localization checks -- в UI нет китайских строк на основных маршрутах -- в UI нет ссылок на оригинальный репозиторий -- в UI нет update-кнопок и update-текстов -- бренд везде HY2XS / HY2XS admin - -5. Negative checks -- порт уже занят -- старое состояние найдено -- неверный домен -- ошибка скачивания Hysteria2 -- конфликтующие файлы UI -- отсутствует root context - -6. Acceptance definition -Система считается реализованной, когда: -- локальный builder собирает install package; -- target install-only flow разворачивает систему на чистом Debian 12; -- HY2XS admin работает как встроенный форк; -- Hysteria2 получена с official upstream; -- оркестратор зафиксирован как Bun/TypeScript stack; -- UI русифицирован и ребрендирован; -- upstream update/repo logic удалена из UI; -- install acceptance не зависит от bot/access layer. diff --git a/hy2xs_implementation_plan-no_git/12-phased-roadmap.txt b/hy2xs_implementation_plan-no_git/12-phased-roadmap.txt deleted file mode 100644 index a303be2..0000000 --- a/hy2xs_implementation_plan-no_git/12-phased-roadmap.txt +++ /dev/null @@ -1,59 +0,0 @@ -HY2XS: phased roadmap - -Фаза 1. Skeleton and structure -Результат: -- создана структура репозитория; -- созданы каталоги builder/, orchestrator/, package/, ui/hy2xs-admin-fork/, docs/, dist/. - -Фаза 2. Builder baseline -Результат: -- build.sh собирает package staging; -- builder локально собирает Bun/TypeScript orchestrator artifact; -- генерируется итоговый архив; -- builder проверяет минимальную целостность пакета. - -Фаза 3. Install-only orchestrator baseline -Результат: -- чистый Debian 12 проходит preflight; -- раскладываются каталоги; -- создаётся post-install.env; -- скачивается Hysteria2; -- создаются unit-файлы. - -Фаза 4. Hysteria runtime baseline -Результат: -- Hysteria2 стартует через systemd; -- слушает нужный UDP-порт; -- firewall baseline применён; -- smoke checks проходят. - -Фаза 5. HY2XS admin fork baseline -Результат: -- UI форк находится в проекте; -- поставляется в пакете; -- стартует отдельным сервисом; -- не содержит target-side build. - -Фаза 6. Rebranding and de-upstreaming -Результат: -- UI переименован в HY2XS admin; -- удалены ссылки на оригинальный repo; -- удалены update-кнопки и update-flow; -- бренд HY2XS отражён в docs, install output и metadata. - -Фаза 7. Localization -Результат: -- русский интерфейс готов полностью; -- опционально добавлен английский; -- китайские строки не видны оператору. - -Фаза 8. Scope cleanup and docs alignment -Результат: -- Telegram/access layer убран из baseline docs; -- в документации зафиксирован стек оркестратора Bun + TypeScript; -- install scope отделён от user delivery scope. - -Фаза 9. Final acceptance -Результат: -- весь install flow от builder до рабочего сервера проходит воспроизводимо; -- docs соответствуют фактической реализации. diff --git a/hy2xs_implementation_plan-no_git/13-task-breakdown-checklist.txt b/hy2xs_implementation_plan-no_git/13-task-breakdown-checklist.txt deleted file mode 100644 index dde76ad..0000000 --- a/hy2xs_implementation_plan-no_git/13-task-breakdown-checklist.txt +++ /dev/null @@ -1,56 +0,0 @@ -HY2XS: task breakdown checklist - -A. Repo and structure -[ ] Создать целевую структуру проекта. -[ ] Разнести builder, package, orchestrator, ui и docs. -[ ] Добавить dist/ и metadata policy. - -B. Builder -[ ] Реализовать build.sh. -[ ] Реализовать staging сборку пакета. -[ ] Реализовать локальную сборку Bun/TypeScript orchestrator artifact. -[ ] Реализовать checksums и build_id. -[ ] Проверить содержимое архива. - -C. Orchestrator -[ ] Реализовать preflight. -[ ] Реализовать deps. -[ ] Реализовать filesystem. -[ ] Реализовать Hysteria install from upstream. -[ ] Реализовать UI deploy from bundled package. -[ ] Реализовать config generation. -[ ] Реализовать systemd units deployment. -[ ] Реализовать nftables baseline. -[ ] Реализовать post-install.env generation. -[ ] Реализовать smoke checks. - -D. Hysteria baseline -[ ] Сделать template config. -[ ] Сделать obfs password generation. -[ ] Зафиксировать bandwidth defaults. -[ ] Проверить launch через systemd. - -E. HY2XS admin fork -[ ] Инвентаризировать brand strings. -[ ] Инвентаризировать repo/update references. -[ ] Переименовать UI в HY2XS admin. -[ ] Удалить update UI/actions. -[ ] Удалить ссылки на оригинальный repo. -[ ] Подготовить packaged runtime output. - -F. Localization -[ ] Вынести строки в локали, если нужно. -[ ] Добавить ru locale. -[ ] Опционально добавить en locale. -[ ] Проверить отсутствие китайского в основных маршрутах. - -G. Scope alignment -[ ] Убрать Telegram/access layer из baseline docs. -[ ] Убрать bot/profile assumptions из acceptance. -[ ] Зафиксировать Bun + TypeScript как стек оркестратора. -[ ] Зафиксировать deliverable как install-only server environment. - -H. Docs and acceptance -[ ] Обновить docs по факту реализации. -[ ] Прогнать acceptance checklist. -[ ] Зафиксировать финальные инварианты. diff --git a/hy2xs_implementation_plan-no_git/about_build.txt b/hy2xs_implementation_plan-no_git/about_build.txt deleted file mode 100644 index 4f14551..0000000 --- a/hy2xs_implementation_plan-no_git/about_build.txt +++ /dev/null @@ -1,8 +0,0 @@ - -### 💎 - -"Проблема" с типами в Bun вообще не ваша забота на этапе создания бинарника. При сборке они просто вырезаются. Ваш план: -1. Пишете логику оркестратора на TS, как мы обсуждали. -2. Перед финальной сборкой прогоняете `bunx tsc --noEmit` для проверки типов, если хотите перестраховаться. -3. Собираете командой `bun build ./src/main.ts --compile --outfile orchestrator`. -4. Запускаете на любом сервере: `./orchestrator`. diff --git a/orchestrator/src/commands/install.ts b/orchestrator/src/commands/install.ts index b786256..8ec6a5b 100644 --- a/orchestrator/src/commands/install.ts +++ b/orchestrator/src/commands/install.ts @@ -1,4 +1,3 @@ -import { randomBytes } from "node:crypto"; import type { InstallContext, InstallOptions } from "../types/context"; import { exists, readText, writeText } from "../lib/fs"; import { runVisible } from "../lib/process"; @@ -16,10 +15,6 @@ import { applyFirewall } from "../steps/firewall"; import { writePostInstallEnv } from "../steps/env"; import { smoke } from "../steps/smoke"; -function secret(): string { - return randomBytes(24).toString("base64url"); -} - export async function install(options: InstallOptions): Promise { const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false; if (options.sourceConfigPath && !hasSourceConfig) { @@ -30,15 +25,22 @@ export async function install(options: InstallOptions): Promise { const config = parseRuntimeEnv(sourceConfigRaw); const context: InstallContext = { + mode: "install", options, config, packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), installDate: new Date().toISOString(), - hysteriaAuthPassword: secret(), - hysteriaVersion: "unknown" + hysteriaVersion: "unknown", + hysteriaTargetVersion: await readPackageValue(options.packageDir, "hysteria.version", ""), + hysteriaArtifactUrl: await readPackageValue(options.packageDir, "hysteria.url", ""), + hysteriaArtifactSha256: await readPackageValue(options.packageDir, "hysteria.sha256", "") }; + if (!context.hysteriaTargetVersion || !context.hysteriaArtifactUrl || !context.hysteriaArtifactSha256) { + throw new Error("missing Hysteria lock metadata in package: hysteria.version/hysteria.url/hysteria.sha256"); + } + step("preflight"); await preflight(context); step("system dependencies"); @@ -48,6 +50,8 @@ export async function install(options: InstallOptions): Promise { step("write runtime env"); await runVisible`mkdir -p /etc/hy2xs`; await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600); + await runVisible`chown root:root ${options.runtimeConfigPath}`; + await runVisible`chmod 0600 ${options.runtimeConfigPath}`; step("bundled UI"); await deployUi(context); step("Hysteria2 upstream install"); diff --git a/orchestrator/src/commands/reconfigure.ts b/orchestrator/src/commands/reconfigure.ts index 38cd9d3..26d1ece 100644 --- a/orchestrator/src/commands/reconfigure.ts +++ b/orchestrator/src/commands/reconfigure.ts @@ -46,13 +46,13 @@ export async function reconfigure(options: ReconfigureOptions): Promise { const configRaw = await readText(options.sourceConfigPath); const config = parseRuntimeEnv(configRaw); - const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = { + const context: ReconfigureContext = { + mode: "reconfigure", options, config, packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), installDate: new Date().toISOString(), - hysteriaAuthPassword: "managed-by-ui-auth", hysteriaVersion: await readInstalledHysteriaVersion() }; @@ -81,6 +81,8 @@ export async function reconfigure(options: ReconfigureOptions): Promise { await applyFirewall(context); step("write env artifacts"); await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600); + await runVisible`chown root:root ${options.runtimeConfigPath}`; + await runVisible`chmod 0600 ${options.runtimeConfigPath}`; await writePostInstallEnv(context); step("smoke checks"); await smoke(context); diff --git a/orchestrator/src/config/env.ts b/orchestrator/src/config/env.ts index 1029ba1..d4df929 100644 --- a/orchestrator/src/config/env.ts +++ b/orchestrator/src/config/env.ts @@ -148,8 +148,6 @@ export function parseRuntimeEnv(content: string): RuntimeConfig { false ), hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml", - allowLatestHysteria: parseBool("HY2XS_ALLOW_LATEST_HYSTERIA", env.HY2XS_ALLOW_LATEST_HYSTERIA, false), - hysteriaVersionPolicy: env.HY2XS_HYSTERIA_VERSION || "v2.6.0", installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin", dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin", logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs", @@ -240,8 +238,6 @@ export function renderRuntimeEnv(config: RuntimeConfig): string { `HY2XS_HYSTERIA_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`, `HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`, `HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`, - `HY2XS_ALLOW_LATEST_HYSTERIA=${config.allowLatestHysteria}`, - `HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`, `HY2XS_INSTALL_DIR=${config.installDir}`, `HY2XS_DATA_DIR=${config.dataDir}`, `HY2XS_LOG_DIR=${config.logDir}` diff --git a/orchestrator/src/steps/config.ts b/orchestrator/src/steps/config.ts index 6987b97..a652dda 100644 --- a/orchestrator/src/steps/config.ts +++ b/orchestrator/src/steps/config.ts @@ -1,8 +1,8 @@ -import type { InstallContext } from "../types/context"; +import type { RuntimeContext } from "../types/context"; import { readText, renderTemplate, writeText } from "../lib/fs"; import { runVisible } from "../lib/process"; -export async function generateConfig(context: InstallContext): Promise { +export async function generateConfig(context: RuntimeContext): Promise { const tlsAcmeBlock = context.config.tlsMode === "acme" ? `acme:\n domains:\n - ${context.config.domain}\n email: ${context.config.acmeEmail}\n ca: letsencrypt\n dir: /var/lib/hysteria/acme\n listenHost: 0.0.0.0\n type: ${context.config.acmeType}` : ""; @@ -14,7 +14,6 @@ export async function generateConfig(context: InstallContext): Promise { const rendered = renderTemplate(template, { HYSTERIA_BIND_HOST: context.config.hysteriaBindHost, HYSTERIA_PORT: context.config.hysteriaPort, - HYSTERIA_AUTH_PASSWORD: context.hysteriaAuthPassword, HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword, HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost, HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort, diff --git a/orchestrator/src/steps/env.ts b/orchestrator/src/steps/env.ts index 63169ee..7207651 100644 --- a/orchestrator/src/steps/env.ts +++ b/orchestrator/src/steps/env.ts @@ -1,7 +1,8 @@ -import type { InstallContext } from "../types/context"; +import type { RuntimeContext } from "../types/context"; import { readText, renderTemplate, writeText } from "../lib/fs"; +import { runVisible } from "../lib/process"; -export async function writePostInstallEnv(context: InstallContext): Promise { +export async function writePostInstallEnv(context: RuntimeContext): Promise { const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), { PACKAGE_VERSION: context.packageVersion, PACKAGE_BUILD_ID: context.packageBuildId, @@ -30,6 +31,8 @@ export async function writePostInstallEnv(context: InstallContext): Promise { await runVisible`id -u hysteria >/dev/null 2>&1 || useradd --system --home /var/lib/hysteria --shell /usr/sbin/nologin hysteria`; await runVisible`id -u hy2xs-admin >/dev/null 2>&1 || useradd --system --home ${context.config.dataDir} --shell /usr/sbin/nologin hy2xs-admin`; - await runVisible`mkdir -p /etc/hy2xs /etc/hysteria /var/lib/hysteria ${context.config.installDir} ${context.config.dataDir} ${context.config.logDir} /usr/local/lib/hy2xs /etc/nftables.d`; - await runVisible`chown -R hysteria:hysteria /etc/hysteria /var/lib/hysteria`; - await runVisible`chown -R root:root ${context.config.installDir}`; + await runVisible`install -d -m 0700 -o root -g root /etc/hy2xs`; + await runVisible`install -d -m 0755 -o root -g root /etc/hysteria`; + await runVisible`install -d -m 0750 -o hysteria -g hysteria /var/lib/hysteria`; + await runVisible`install -d -m 0750 -o hy2xs-admin -g hy2xs-admin ${context.config.dataDir}`; + await runVisible`install -d -m 0750 -o hy2xs-admin -g hy2xs-admin ${context.config.logDir}`; + await runVisible`install -d -m 0755 -o root -g root ${context.config.installDir}`; + await runVisible`install -d -m 0755 -o root -g root /usr/local/lib/hy2xs`; + await runVisible`install -d -m 0755 -o root -g root /etc/nftables.d`; await runVisible`chmod -R go-w ${context.config.installDir}`; - await runVisible`chown -R hy2xs-admin:hy2xs-admin ${context.config.dataDir} ${context.config.logDir}`; - await runVisible`chmod 700 /etc/hy2xs`; } diff --git a/orchestrator/src/steps/firewall.ts b/orchestrator/src/steps/firewall.ts index 8428826..9112b16 100644 --- a/orchestrator/src/steps/firewall.ts +++ b/orchestrator/src/steps/firewall.ts @@ -1,4 +1,4 @@ -import type { InstallContext } from "../types/context"; +import type { RuntimeContext } from "../types/context"; import { exists, readText, renderTemplate, writeText } from "../lib/fs"; import { fail, info } from "../lib/log"; import { runVisible } from "../lib/process"; @@ -27,7 +27,7 @@ function isSafeNftablesEntrypoint(content: string): boolean { return effective === "flush ruleset"; } -export async function applyFirewall(context: InstallContext): Promise { +export async function applyFirewall(context: RuntimeContext): Promise { if (context.options.skipFirewall || !context.config.firewallEnabled) { info("firewall skipped by flag"); return; diff --git a/orchestrator/src/steps/hysteria.ts b/orchestrator/src/steps/hysteria.ts index b956097..1a26876 100644 --- a/orchestrator/src/steps/hysteria.ts +++ b/orchestrator/src/steps/hysteria.ts @@ -9,41 +9,32 @@ function normalizeInstalledVersion(raw: string): string { return raw.trim(); } -function validateVersionPolicy(value: string): void { +function validatePinnedVersion(value: string): void { if (/^v\d+\.\d+\.\d+$/.test(value)) { return; } - throw new Error(`invalid HY2XS_HYSTERIA_VERSION policy: ${value}`); + throw new Error(`invalid pinned Hysteria version in package metadata: ${value}`); } export async function installHysteria(context: InstallContext): Promise { - const policy = context.config.hysteriaVersionPolicy; - if (policy === "latest" && !context.config.allowLatestHysteria) { - throw new Error("HY2XS_HYSTERIA_VERSION=latest is not allowed in production; pin vX.Y.Z or set HY2XS_ALLOW_LATEST_HYSTERIA=true"); - } - validateVersionPolicy(policy); + validatePinnedVersion(context.hysteriaTargetVersion); - const scriptPath = "/tmp/hy2xs-install-hysteria.sh"; - await runVisible`curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location https://get.hy2.sh/ -o ${scriptPath}`; - await runVisible`test -s ${scriptPath}`; - await runVisible`chmod 700 ${scriptPath}`; + const tmp = "/tmp/hy2xs-hysteria-linux-amd64"; - if (policy === "latest") { - await runVisible`bash ${scriptPath}`; - } else { - await runVisible`bash ${scriptPath} --version ${policy}`; - } + await runVisible`curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location ${context.hysteriaArtifactUrl} -o ${tmp}`; + await runVisible`test -s ${tmp}`; + await runVisible`printf '%s %s\n' ${context.hysteriaArtifactSha256} ${tmp} | sha256sum -c -`; + await runVisible`install -m 0755 ${tmp} /usr/local/bin/hysteria`; + await runVisible`rm -f ${tmp}`; await runVisible`test -x /usr/local/bin/hysteria`; const versionOutput = await run`/usr/local/bin/hysteria version`; const installedVersion = normalizeInstalledVersion(versionOutput); context.hysteriaVersion = installedVersion; - if (policy !== "latest" && installedVersion !== policy) { + if (installedVersion !== context.hysteriaTargetVersion) { throw new Error( - `installed Hysteria version mismatch: expected ${policy}, got ${installedVersion}. Review upstream installer env contract.` + `installed Hysteria version mismatch: expected ${context.hysteriaTargetVersion}, got ${installedVersion}` ); } - - await runVisible`rm -f ${scriptPath}`; } diff --git a/orchestrator/src/steps/preflight.ts b/orchestrator/src/steps/preflight.ts index e66cc87..24167c5 100644 --- a/orchestrator/src/steps/preflight.ts +++ b/orchestrator/src/steps/preflight.ts @@ -1,4 +1,4 @@ -import type { InstallContext } from "../types/context"; +import type { RuntimeContext } from "../types/context"; import { exists, readText } from "../lib/fs"; import { fail, info } from "../lib/log"; import { run } from "../lib/process"; @@ -21,8 +21,8 @@ async function isUnitActive(unit: string): Promise { } } -export async function preflight(context: InstallContext): Promise { - const isReconfigure = context.packageVersion === "reconfigure"; +export async function preflight(context: RuntimeContext): Promise { + const isReconfigure = context.mode === "reconfigure"; if (process.getuid?.() !== 0) { fail("installer must run as root"); @@ -33,7 +33,16 @@ export async function preflight(context: InstallContext): Promise { fail("HY2XS baseline supports only clean Debian 12"); } - if (!(await exists(`${context.options.packageDir}/ui/hy2xs-admin`))) { + if (!(await exists(`${context.options.packageDir}/systemd/hy2xs-admin.service`))) { + fail("missing hy2xs-admin systemd unit in package"); + } + if (!(await exists(`${context.options.packageDir}/systemd/hysteria-server.service`))) { + fail("missing hysteria-server systemd unit in package"); + } + if (!(await exists(`${context.options.packageDir}/templates/hysteria/config.yaml.tpl`))) { + fail("missing Hysteria config template in package"); + } + if (context.mode === "install" && !(await exists(`${context.options.packageDir}/ui/hy2xs-admin/hy2xs-admin`))) { fail("bundled HY2XS admin is missing from install package"); } diff --git a/orchestrator/src/steps/smoke.ts b/orchestrator/src/steps/smoke.ts index 15f4d3d..e9442a6 100644 --- a/orchestrator/src/steps/smoke.ts +++ b/orchestrator/src/steps/smoke.ts @@ -1,8 +1,8 @@ -import type { InstallContext } from "../types/context"; +import type { RuntimeContext } from "../types/context"; import { info } from "../lib/log"; import { runHidden, runSecret, runVisible } from "../lib/process"; -export async function smoke(context: InstallContext): Promise { +export async function smoke(context: RuntimeContext): Promise { if (context.options.skipStart) { info("service start and smoke checks skipped by flag"); return; @@ -30,6 +30,9 @@ export async function smoke(context: InstallContext): Promise { await runVisible`sudo -u hysteria test -r /etc/hysteria/config.yaml`; await runVisible`sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml`; await runVisible`sudo -u hy2xs-admin test ! -w /etc/hysteria/config.yaml`; + await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/hy2xs.env`; + await runVisible`sudo -u hy2xs-admin test ! -r /etc/hy2xs/bootstrap-admin.secret`; + await runVisible`sudo -u hysteria test ! -r /etc/hy2xs/bootstrap-admin.secret`; await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`; if (context.config.uiBindHost === "127.0.0.1") { await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`; @@ -42,6 +45,13 @@ export async function smoke(context: InstallContext): Promise { throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`); } + for (let i = 0; i < 10; i += 1) { + const response = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`; + if (!/"ok"\s*:\s*false/.test(response)) { + throw new Error(`unexpected auth response during rate-limit smoke: ${response}`); + } + } + const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim(); if (!adminConPass) { throw new Error("admin connection password is empty in bootstrap secret file"); diff --git a/orchestrator/src/steps/systemd.ts b/orchestrator/src/steps/systemd.ts index 49ea8e7..33faac9 100644 --- a/orchestrator/src/steps/systemd.ts +++ b/orchestrator/src/steps/systemd.ts @@ -1,8 +1,8 @@ -import type { InstallContext } from "../types/context"; +import type { RuntimeContext } from "../types/context"; import { readText, renderTemplate, writeText } from "../lib/fs"; import { runVisible } from "../lib/process"; -export async function deploySystemd(context: InstallContext): Promise { +export async function deploySystemd(context: RuntimeContext): Promise { const values = { UI_BIND_HOST: context.config.uiBindHost, UI_PORT: context.config.uiPort, diff --git a/orchestrator/src/types/context.ts b/orchestrator/src/types/context.ts index 3cee1d0..a5b4d38 100644 --- a/orchestrator/src/types/context.ts +++ b/orchestrator/src/types/context.ts @@ -1,4 +1,4 @@ -export type InstallOptions = { +export type CommonOptions = { packageDir: string; sourceConfigPath: string; runtimeConfigPath: string; @@ -7,17 +7,15 @@ export type InstallOptions = { skipStart: boolean; }; -export type ReconfigureOptions = { - packageDir: string; - sourceConfigPath: string; - runtimeConfigPath: string; - nonInteractive: boolean; +export type InstallOptions = CommonOptions; + +export type ReconfigureOptions = CommonOptions & { dryRun: boolean; apply: boolean; - skipFirewall: boolean; - skipStart: boolean; }; +export type RunMode = "install" | "reconfigure"; + export type TlsMode = "acme" | "file" | "self_signed_dev"; export type RuntimeConfig = { @@ -54,25 +52,31 @@ export type RuntimeConfig = { hysteriaBandwidthDown: string; hysteriaIgnoreClientBandwidth: boolean; hysteriaConfigPath: string; - allowLatestHysteria: boolean; - hysteriaVersionPolicy: string; installDir: string; dataDir: string; logDir: string; bootstrapAdminSecretPath: string; }; -export type InstallContext = { - options: InstallOptions; +export type RuntimeContext = { + mode: RunMode; + options: CommonOptions; config: RuntimeConfig; packageVersion: string; packageBuildId: string; installDate: string; - hysteriaAuthPassword: string; hysteriaVersion: string; }; -export type ReconfigureContext = { - options: ReconfigureOptions; - config: RuntimeConfig; +export type InstallContext = RuntimeContext & { + mode: "install"; + options: InstallOptions; + hysteriaTargetVersion: string; + hysteriaArtifactUrl: string; + hysteriaArtifactSha256: string; +}; + +export type ReconfigureContext = RuntimeContext & { + mode: "reconfigure"; + options: ReconfigureOptions; }; diff --git a/package/config/hy2xs.env b/package/config/hy2xs.env index 23360d1..cddff0b 100644 --- a/package/config/hy2xs.env +++ b/package/config/hy2xs.env @@ -32,8 +32,6 @@ HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml -HY2XS_ALLOW_LATEST_HYSTERIA=false -HY2XS_HYSTERIA_VERSION=v2.6.0 HY2XS_INSTALL_DIR=/opt/hy2xs-admin HY2XS_DATA_DIR=/var/lib/hy2xs-admin HY2XS_LOG_DIR=/var/log/hy2xs diff --git a/package/install.sh b/package/install.sh index 2a0f81c..75213e3 100755 --- a/package/install.sh +++ b/package/install.sh @@ -24,6 +24,16 @@ if [ ! -x "$ORCHESTRATOR" ]; then fail "Missing executable orchestrator artifact: $ORCHESTRATOR" fi +if [ ! -f "$PACKAGE_DIR/metadata/checksums.txt" ]; then + fail "Missing package checksums: $PACKAGE_DIR/metadata/checksums.txt" +fi + +log "verifying package checksums" +( + cd "$PACKAGE_DIR" + sha256sum -c metadata/checksums.txt +) + install -d -m 0755 /usr/local/lib/hy2xs install -m 0755 "$ORCHESTRATOR" "$ORCHESTRATOR_INSTALL_PATH" ln -sf "$ORCHESTRATOR_INSTALL_PATH" "$ORCHESTRATOR_SYMLINK" diff --git a/tools/build/hysteria-lock.env b/tools/build/hysteria-lock.env new file mode 100644 index 0000000..a707234 --- /dev/null +++ b/tools/build/hysteria-lock.env @@ -0,0 +1,3 @@ +HYSTERIA_VERSION=v2.6.0 +HYSTERIA_ARTIFACT_URL=https://github.com/apernet/hysteria/releases/download/app%2Fv2.6.0/hysteria-linux-amd64 +HYSTERIA_ARTIFACT_SHA256=replace-with-release-sha256 diff --git a/tools/build/lib/package.sh b/tools/build/lib/package.sh index 9709bbe..3f3c4a7 100644 --- a/tools/build/lib/package.sh +++ b/tools/build/lib/package.sh @@ -3,6 +3,7 @@ set -euo pipefail STAGE_DIR="tools/build/output/hy2xs-install" ADMIN_BUILD_DIR="tools/build/output/hy2xs-admin-build" +HYSTERIA_LOCK_FILE="tools/build/hysteria-lock.env" prepare_stage() { local version="$1" @@ -62,6 +63,13 @@ write_metadata() { local version="$1" local build_id="$2" + [ -f "$HYSTERIA_LOCK_FILE" ] || fail "missing Hysteria lock file: $HYSTERIA_LOCK_FILE" + # shellcheck disable=SC1090 + . "$HYSTERIA_LOCK_FILE" + [ -n "${HYSTERIA_VERSION:-}" ] || fail "HYSTERIA_VERSION is required in $HYSTERIA_LOCK_FILE" + [ -n "${HYSTERIA_ARTIFACT_URL:-}" ] || fail "HYSTERIA_ARTIFACT_URL is required in $HYSTERIA_LOCK_FILE" + [ -n "${HYSTERIA_ARTIFACT_SHA256:-}" ] || fail "HYSTERIA_ARTIFACT_SHA256 is required in $HYSTERIA_LOCK_FILE" + { printf 'name=HY2XS\n' printf 'version=%s\n' "$version" @@ -77,8 +85,15 @@ write_metadata() { printf 'pnpm_version=%s\n' "$($PNPM_BIN --version)" printf 'hysteria_source=official-upstream\n' printf 'hysteria_target=linux-amd64\n' + printf 'hysteria_version=%s\n' "$HYSTERIA_VERSION" + printf 'hysteria_artifact_url=%s\n' "$HYSTERIA_ARTIFACT_URL" + printf 'hysteria_artifact_sha256=%s\n' "$HYSTERIA_ARTIFACT_SHA256" } >"$STAGE_DIR/metadata/package.env" + printf '%s\n' "$HYSTERIA_VERSION" >"$STAGE_DIR/metadata/hysteria.version" + printf '%s\n' "$HYSTERIA_ARTIFACT_URL" >"$STAGE_DIR/metadata/hysteria.url" + printf '%s\n' "$HYSTERIA_ARTIFACT_SHA256" >"$STAGE_DIR/metadata/hysteria.sha256" + ( cd "$STAGE_DIR" find . -type f ! -path './metadata/checksums.txt' -print0 \