Продакшн рефактор без легаси

This commit is contained in:
2026-04-30 20:54:38 +05:00
parent c10ab1fafd
commit 4a0d9569d1
41 changed files with 193 additions and 888 deletions
+1 -2
View File
@@ -7,7 +7,7 @@ HY2XS — production installer/runtime-manager для развёртывания
- production builder на Debian 12 amd64 создаёт один переносимый архив; - production builder на Debian 12 amd64 создаёт один переносимый архив;
- install-only оркестратор на Bun + TypeScript поставляется на target как готовый compiled artifact; - install-only оркестратор на Bun + TypeScript поставляется на target как готовый compiled artifact;
- HY2XS admin поставляется в составе пакета как bundled fork; - HY2XS admin поставляется в составе пакета как bundled fork;
- vanilla Hysteria2 скачивается с official upstream во время установки; - vanilla Hysteria2 устанавливается как pinned binary (version/url/sha256 из metadata install package);
- runtime-конфиг управляется через `/etc/hy2xs/hy2xs.env` и команду `reconfigure`. - runtime-конфиг управляется через `/etc/hy2xs/hy2xs.env` и команду `reconfigure`.
В baseline намеренно не входят target-side build, rollback/uninstall orchestration, Telegram bot delivery, port hopping и generic server-manager функции. В 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. - [`package`](package) — skeleton итогового install package: entrypoint, templates, systemd units, package docs.
- [`apps`](apps) — исходники HY2XS admin fork. - [`apps`](apps) — исходники HY2XS admin fork.
- [`docs`](docs) — архитектурная документация и acceptance. - [`docs`](docs) — архитектурная документация и acceptance.
- [`hy2xs_implementation_plan-no_git`](hy2xs_implementation_plan-no_git) — roadmap и task breakdown.
- [`dist`](dist) — итоговые install archives, создаются builder'ом и не хранятся в git. - [`dist`](dist) — итоговые install archives, создаются builder'ом и не хранятся в git.
## Сборка ## Сборка
+2 -1
View File
@@ -11,6 +11,7 @@ import (
"hy2xs-admin/service" "hy2xs-admin/service"
"hy2xs-admin/util" "hy2xs-admin/util"
"io" "io"
"path/filepath"
"strings" "strings"
"time" "time"
) )
@@ -299,7 +300,7 @@ func ExportAccount(c *gin.Context) {
} }
fileName := fmt.Sprintf("AccountExport-%s.json", time.Now().Format("20060102150405")) 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 { if err = util.ExportFile(filePath, accountExports, 0); err != nil {
vo.Fail(err.Error(), c) vo.Fail(err.Error(), c)
+3 -2
View File
@@ -14,6 +14,7 @@ import (
"hy2xs-admin/service" "hy2xs-admin/service"
"hy2xs-admin/util" "hy2xs-admin/util"
"io" "io"
"path/filepath"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -230,7 +231,7 @@ func ExportHysteria2Config(c *gin.Context) {
hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret hysteria2ServerConfig.TrafficStats.Secret = &trafficStatsSecret
fileName := fmt.Sprintf("Hysteria2Config-%s.yaml", time.Now().Format("20060102150405")) 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 { if err = util.ExportFile(filePath, hysteria2ServerConfig, 1); err != nil {
vo.Fail(err.Error(), c) vo.Fail(err.Error(), c)
@@ -258,7 +259,7 @@ func ExportConfig(c *gin.Context) {
return return
} }
fileName := fmt.Sprintf("SystemConfig-%s.json", time.Now().Format("20060102150405")) 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 { if err = util.ExportFile(filePath, configs, 0); err != nil {
vo.Fail(err.Error(), c) vo.Fail(err.Error(), c)
+26
View File
@@ -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()
}
}
+4 -2
View File
@@ -5,12 +5,14 @@ import (
"hy2xs-admin/controller" "hy2xs-admin/controller"
) )
func initHysteria2AuthRouter(hysteria2Api *gin.RouterGroup) { func initHysteria2MachineAuthRouter(hysteria2Api *gin.RouterGroup) {
hysteria2 := hysteria2Api.Group("/hysteria2") hysteria2 := hysteria2Api.Group("/hysteria2")
{ {
hysteria2.POST("/auth", controller.Hysteria2Auth) hysteria2.POST("/auth", controller.Hysteria2Auth)
} }
}
func initHysteria2SubscribeRouter(hysteria2Api *gin.RouterGroup) {
hysteria2Api.GET("/:conPass", controller.Hysteria2Subscribe) hysteria2Api.GET("/:conPass", controller.Hysteria2Subscribe)
} }
+15 -14
View File
@@ -8,34 +8,35 @@ import (
) )
func Router(router *gin.Engine, huiWebContext *string) { func Router(router *gin.Engine, huiWebContext *string) {
// global context
relativePath := "/" relativePath := "/"
if huiWebContext != nil && strings.HasPrefix(*huiWebContext, "/") { if huiWebContext != nil && strings.HasPrefix(*huiWebContext, "/") {
relativePath = *huiWebContext relativePath = *huiWebContext
} }
globalGroup := router.Group(relativePath)
{
globalGroup.Use(middleware.FilterHandler(), middleware.LogHandler(), middleware.RateLimiterHandler())
frontend.InitFrontend(router, relativePath) frontend.InitFrontend(router, relativePath)
globalGroup := router.Group(relativePath)
machineApi := globalGroup.Group("/hui")
machineApi.Use(middleware.LocalOnlyHandler(), middleware.LogHandler())
initHysteria2MachineAuthRouter(machineApi)
authApi := globalGroup.Group("/hui") authApi := globalGroup.Group("/hui")
{ authApi.Use(middleware.FilterHandler(), middleware.LogHandler(), middleware.RateLimiterHandler())
initAuthRouter(authApi) initAuthRouter(authApi)
initHysteria2AuthRouter(authApi) initHysteria2SubscribeRouter(authApi)
}
globalGroup.Use(middleware.JWTHandler())
globalGroup.Use(middleware.AdminHandler())
huiAdminApi := globalGroup.Group("/hui") huiAdminApi := globalGroup.Group("/hui")
{ huiAdminApi.Use(
middleware.FilterHandler(),
middleware.LogHandler(),
middleware.RateLimiterHandler(),
middleware.JWTHandler(),
middleware.AdminHandler(),
)
initAccountAdminRouter(huiAdminApi) initAccountAdminRouter(huiAdminApi)
initConfigRouter(huiAdminApi) initConfigRouter(huiAdminApi)
initHysteria2Router(huiAdminApi) initHysteria2Router(huiAdminApi)
initLogRouter(huiAdminApi) initLogRouter(huiAdminApi)
initMonitorRouter(huiAdminApi) initMonitorRouter(huiAdminApi)
} }
}
}
+4 -3
View File
@@ -123,9 +123,10 @@ project/
- целевая установка должна брать его с official upstream на момент развёртывания - целевая установка должна брать его с official upstream на момент развёртывания
Дополнительно: Дополнительно:
- `HY2XS_HYSTERIA_VERSION=latest|vX.Y.Z` задаётся через runtime env; - версия Hysteria2 фиксируется в metadata install package (`metadata/hysteria.version`);
- при `latest` оркестратор записывает **фактически установленную** версию в `post-install.env`; - URL артефакта и SHA256 фиксируются в metadata install package (`metadata/hysteria.url`, `metadata/hysteria.sha256`);
- install flow использует download-to-temp + explicit execute + post-install verification binary/version. - runtime `reconfigure` не обновляет и не откатывает бинарник Hysteria2;
- install flow валидирует SHA256 и фактическую версию установленного бинарника.
## Инварианты ## Инварианты
+3 -2
View File
@@ -22,8 +22,9 @@ Hysteria2 — основной транспортный компонент се
- по умолчанию install layer тянет **свежий upstream release / install source** - по умолчанию install layer тянет **свежий upstream release / install source**
- фактически установленная версия обязательно записывается в `post-install.env` - фактически установленная версия обязательно записывается в `post-install.env`
- поддерживаются политики `latest | vX.Y.Z` через `HY2XS_HYSTERIA_VERSION` - бинарник Hysteria2 устанавливается только на этапе `install`
- при `vX.Y.Z` install обязан валидировать соответствие фактически установленной версии - версия, URL и SHA256 фиксируются в metadata install package
- `reconfigure` не обновляет и не откатывает бинарник Hysteria2
## Платформа ## Платформа
+3 -1
View File
@@ -78,7 +78,7 @@
2. Проверяет базовые зависимости и install context. 2. Проверяет базовые зависимости и install context.
3. Создаёт каталоги установки. 3. Создаёт каталоги установки.
4. Разворачивает bundled HY2XS admin. 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. 6. Генерирует Hysteria config.
7. Создаёт systemd unit для Hysteria. 7. Создаёт systemd unit для Hysteria.
8. Создаёт systemd unit для HY2XS admin. 8. Создаёт systemd unit для HY2XS admin.
@@ -123,6 +123,8 @@
- TLS modes: `acme | file | self_signed_dev`; - TLS modes: `acme | file | self_signed_dev`;
- `trafficStats.secret` отдельный от `JWT_SECRET`; - `trafficStats.secret` отдельный от `JWT_SECRET`;
- install flow фиксирует фактически установленную версию Hysteria в snapshot; - install flow фиксирует фактически установленную версию Hysteria в snapshot;
- версия/URL/SHA256 Hysteria берутся из metadata install package;
- `reconfigure` не обновляет бинарник Hysteria, только runtime-слой.
- при `reconfigure --apply`: backup -> staged apply -> smoke -> rollback on fail. - при `reconfigure --apply`: backup -> staged apply -> smoke -> rollback on fail.
## Что не реализовывать ## Что не реализовывать
+4 -1
View File
@@ -20,7 +20,10 @@
- какую фактическую версию оркестратор установил - какую фактическую версию оркестратор установил
- что записано в `HY2_VERSION` - что записано в `HY2_VERSION`
- не связано ли поведение со свежим upstream release - не связано ли поведение со свежим upstream release
- какая policy была в `HY2XS_HYSTERIA_VERSION` (`latest|vX.Y.Z`) - какая версия Hysteria зафиксирована в metadata установленного пакета
Для обновления бинарника Hysteria2 используйте новый release install package.
Изменение runtime env не обновляет бинарник Hysteria2.
### 4. Оркестратор — Bun/TypeScript, но target не билдит его ### 4. Оркестратор — Bun/TypeScript, но target не билдит его
Если проблема в install flow, сначала смотреть: Если проблема в install flow, сначала смотреть:
@@ -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.
@@ -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.
@@ -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-<version>.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/
Но логическое разделение всё равно должно сохраниться.
@@ -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-<version>.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 можно передать на сервер без дополнительной сборки.
@@ -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
- вызывать собранный артефакт оркестратора
- логировать запуск
- завершаться с понятным кодом ошибки
@@ -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
@@ -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:<PORT>
- 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 соответствует согласованной серверной и клиентской конфигурации.
@@ -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.
@@ -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.
@@ -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.
@@ -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-изменений.
- Любые ручные правки оператора должны потом осознанно переноситься в реальные рабочие конфиги и применяться документированным способом.
@@ -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.
@@ -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 соответствуют фактической реализации.
@@ -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.
[ ] Зафиксировать финальные инварианты.
@@ -1,8 +0,0 @@
### 💎
"Проблема" с типами в Bun вообще не ваша забота на этапе создания бинарника. При сборке они просто вырезаются. Ваш план:
1. Пишете логику оркестратора на TS, как мы обсуждали.
2. Перед финальной сборкой прогоняете `bunx tsc --noEmit` для проверки типов, если хотите перестраховаться.
3. Собираете командой `bun build ./src/main.ts --compile --outfile orchestrator`.
4. Запускаете на любом сервере: `./orchestrator`.
+11 -7
View File
@@ -1,4 +1,3 @@
import { randomBytes } from "node:crypto";
import type { InstallContext, InstallOptions } from "../types/context"; import type { InstallContext, InstallOptions } from "../types/context";
import { exists, readText, writeText } from "../lib/fs"; import { exists, readText, writeText } from "../lib/fs";
import { runVisible } from "../lib/process"; import { runVisible } from "../lib/process";
@@ -16,10 +15,6 @@ import { applyFirewall } from "../steps/firewall";
import { writePostInstallEnv } from "../steps/env"; import { writePostInstallEnv } from "../steps/env";
import { smoke } from "../steps/smoke"; import { smoke } from "../steps/smoke";
function secret(): string {
return randomBytes(24).toString("base64url");
}
export async function install(options: InstallOptions): Promise<void> { export async function install(options: InstallOptions): Promise<void> {
const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false; const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false;
if (options.sourceConfigPath && !hasSourceConfig) { if (options.sourceConfigPath && !hasSourceConfig) {
@@ -30,15 +25,22 @@ export async function install(options: InstallOptions): Promise<void> {
const config = parseRuntimeEnv(sourceConfigRaw); const config = parseRuntimeEnv(sourceConfigRaw);
const context: InstallContext = { const context: InstallContext = {
mode: "install",
options, options,
config, config,
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
installDate: new Date().toISOString(), 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"); step("preflight");
await preflight(context); await preflight(context);
step("system dependencies"); step("system dependencies");
@@ -48,6 +50,8 @@ export async function install(options: InstallOptions): Promise<void> {
step("write runtime env"); step("write runtime env");
await runVisible`mkdir -p /etc/hy2xs`; await runVisible`mkdir -p /etc/hy2xs`;
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600); await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
await runVisible`chown root:root ${options.runtimeConfigPath}`;
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
step("bundled UI"); step("bundled UI");
await deployUi(context); await deployUi(context);
step("Hysteria2 upstream install"); step("Hysteria2 upstream install");
+4 -2
View File
@@ -46,13 +46,13 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
const configRaw = await readText(options.sourceConfigPath); const configRaw = await readText(options.sourceConfigPath);
const config = parseRuntimeEnv(configRaw); const config = parseRuntimeEnv(configRaw);
const context: ReconfigureContext & { packageVersion: string; packageBuildId: string; installDate: string; hysteriaAuthPassword: string; hysteriaVersion: string } = { const context: ReconfigureContext = {
mode: "reconfigure",
options, options,
config, config,
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"), packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"), packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
installDate: new Date().toISOString(), installDate: new Date().toISOString(),
hysteriaAuthPassword: "managed-by-ui-auth",
hysteriaVersion: await readInstalledHysteriaVersion() hysteriaVersion: await readInstalledHysteriaVersion()
}; };
@@ -81,6 +81,8 @@ export async function reconfigure(options: ReconfigureOptions): Promise<void> {
await applyFirewall(context); await applyFirewall(context);
step("write env artifacts"); step("write env artifacts");
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600); await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
await runVisible`chown root:root ${options.runtimeConfigPath}`;
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
await writePostInstallEnv(context); await writePostInstallEnv(context);
step("smoke checks"); step("smoke checks");
await smoke(context); await smoke(context);
-4
View File
@@ -148,8 +148,6 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
false false
), ),
hysteriaConfigPath: env.HY2XS_HYSTERIA_CONFIG_PATH || "/etc/hysteria/config.yaml", 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", installDir: env.HY2XS_INSTALL_DIR || "/opt/hy2xs-admin",
dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin", dataDir: env.HY2XS_DATA_DIR || "/var/lib/hy2xs-admin",
logDir: env.HY2XS_LOG_DIR || "/var/log/hy2xs", 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_BANDWIDTH_DOWN=${config.hysteriaBandwidthDown}`,
`HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`, `HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=${config.hysteriaIgnoreClientBandwidth}`,
`HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`, `HY2XS_HYSTERIA_CONFIG_PATH=${config.hysteriaConfigPath}`,
`HY2XS_ALLOW_LATEST_HYSTERIA=${config.allowLatestHysteria}`,
`HY2XS_HYSTERIA_VERSION=${config.hysteriaVersionPolicy}`,
`HY2XS_INSTALL_DIR=${config.installDir}`, `HY2XS_INSTALL_DIR=${config.installDir}`,
`HY2XS_DATA_DIR=${config.dataDir}`, `HY2XS_DATA_DIR=${config.dataDir}`,
`HY2XS_LOG_DIR=${config.logDir}` `HY2XS_LOG_DIR=${config.logDir}`
+2 -3
View File
@@ -1,8 +1,8 @@
import type { InstallContext } from "../types/context"; import type { RuntimeContext } from "../types/context";
import { readText, renderTemplate, writeText } from "../lib/fs"; import { readText, renderTemplate, writeText } from "../lib/fs";
import { runVisible } from "../lib/process"; import { runVisible } from "../lib/process";
export async function generateConfig(context: InstallContext): Promise<void> { export async function generateConfig(context: RuntimeContext): Promise<void> {
const tlsAcmeBlock = context.config.tlsMode === "acme" 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}` ? `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<void> {
const rendered = renderTemplate(template, { const rendered = renderTemplate(template, {
HYSTERIA_BIND_HOST: context.config.hysteriaBindHost, HYSTERIA_BIND_HOST: context.config.hysteriaBindHost,
HYSTERIA_PORT: context.config.hysteriaPort, HYSTERIA_PORT: context.config.hysteriaPort,
HYSTERIA_AUTH_PASSWORD: context.hysteriaAuthPassword,
HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword, HYSTERIA_OBFS_PASSWORD: context.config.hysteriaObfsPassword,
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost, HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort, HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
+7 -2
View File
@@ -1,7 +1,8 @@
import type { InstallContext } from "../types/context"; import type { RuntimeContext } from "../types/context";
import { readText, renderTemplate, writeText } from "../lib/fs"; import { readText, renderTemplate, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
export async function writePostInstallEnv(context: InstallContext): Promise<void> { export async function writePostInstallEnv(context: RuntimeContext): Promise<void> {
const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), { const rendered = renderTemplate(await readText(`${context.options.packageDir}/templates/env/post-install.env.tpl`), {
PACKAGE_VERSION: context.packageVersion, PACKAGE_VERSION: context.packageVersion,
PACKAGE_BUILD_ID: context.packageBuildId, PACKAGE_BUILD_ID: context.packageBuildId,
@@ -30,6 +31,8 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
}); });
await writeText("/etc/hysteria/post-install.env", rendered, 0o600); await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
await runVisible`chown root:root /etc/hysteria/post-install.env`;
await runVisible`chmod 0600 /etc/hysteria/post-install.env`;
await writeText( await writeText(
context.config.bootstrapAdminSecretPath, context.config.bootstrapAdminSecretPath,
[ [
@@ -39,4 +42,6 @@ export async function writePostInstallEnv(context: InstallContext): Promise<void
].join("\n") + "\n", ].join("\n") + "\n",
0o600 0o600
); );
await runVisible`chown root:root ${context.config.bootstrapAdminSecretPath}`;
await runVisible`chmod 0600 ${context.config.bootstrapAdminSecretPath}`;
} }
+8 -5
View File
@@ -4,10 +4,13 @@ import { runVisible } from "../lib/process";
export async function prepareFilesystem(context: InstallContext): Promise<void> { export async function prepareFilesystem(context: InstallContext): Promise<void> {
await runVisible`id -u hysteria >/dev/null 2>&1 || useradd --system --home /var/lib/hysteria --shell /usr/sbin/nologin hysteria`; 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`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`install -d -m 0700 -o root -g root /etc/hy2xs`;
await runVisible`chown -R hysteria:hysteria /etc/hysteria /var/lib/hysteria`; await runVisible`install -d -m 0755 -o root -g root /etc/hysteria`;
await runVisible`chown -R root:root ${context.config.installDir}`; 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`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`;
} }
+2 -2
View File
@@ -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 { exists, readText, renderTemplate, writeText } from "../lib/fs";
import { fail, info } from "../lib/log"; import { fail, info } from "../lib/log";
import { runVisible } from "../lib/process"; import { runVisible } from "../lib/process";
@@ -27,7 +27,7 @@ function isSafeNftablesEntrypoint(content: string): boolean {
return effective === "flush ruleset"; return effective === "flush ruleset";
} }
export async function applyFirewall(context: InstallContext): Promise<void> { export async function applyFirewall(context: RuntimeContext): Promise<void> {
if (context.options.skipFirewall || !context.config.firewallEnabled) { if (context.options.skipFirewall || !context.config.firewallEnabled) {
info("firewall skipped by flag"); info("firewall skipped by flag");
return; return;
+11 -20
View File
@@ -9,41 +9,32 @@ function normalizeInstalledVersion(raw: string): string {
return raw.trim(); return raw.trim();
} }
function validateVersionPolicy(value: string): void { function validatePinnedVersion(value: string): void {
if (/^v\d+\.\d+\.\d+$/.test(value)) { if (/^v\d+\.\d+\.\d+$/.test(value)) {
return; 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<void> { export async function installHysteria(context: InstallContext): Promise<void> {
const policy = context.config.hysteriaVersionPolicy; validatePinnedVersion(context.hysteriaTargetVersion);
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);
const scriptPath = "/tmp/hy2xs-install-hysteria.sh"; const tmp = "/tmp/hy2xs-hysteria-linux-amd64";
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}`;
if (policy === "latest") { await runVisible`curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location ${context.hysteriaArtifactUrl} -o ${tmp}`;
await runVisible`bash ${scriptPath}`; await runVisible`test -s ${tmp}`;
} else { await runVisible`printf '%s %s\n' ${context.hysteriaArtifactSha256} ${tmp} | sha256sum -c -`;
await runVisible`bash ${scriptPath} --version ${policy}`; await runVisible`install -m 0755 ${tmp} /usr/local/bin/hysteria`;
} await runVisible`rm -f ${tmp}`;
await runVisible`test -x /usr/local/bin/hysteria`; await runVisible`test -x /usr/local/bin/hysteria`;
const versionOutput = await run`/usr/local/bin/hysteria version`; const versionOutput = await run`/usr/local/bin/hysteria version`;
const installedVersion = normalizeInstalledVersion(versionOutput); const installedVersion = normalizeInstalledVersion(versionOutput);
context.hysteriaVersion = installedVersion; context.hysteriaVersion = installedVersion;
if (policy !== "latest" && installedVersion !== policy) { if (installedVersion !== context.hysteriaTargetVersion) {
throw new Error( 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}`;
} }
+13 -4
View File
@@ -1,4 +1,4 @@
import type { InstallContext } from "../types/context"; import type { RuntimeContext } from "../types/context";
import { exists, readText } from "../lib/fs"; import { exists, readText } from "../lib/fs";
import { fail, info } from "../lib/log"; import { fail, info } from "../lib/log";
import { run } from "../lib/process"; import { run } from "../lib/process";
@@ -21,8 +21,8 @@ async function isUnitActive(unit: string): Promise<boolean> {
} }
} }
export async function preflight(context: InstallContext): Promise<void> { export async function preflight(context: RuntimeContext): Promise<void> {
const isReconfigure = context.packageVersion === "reconfigure"; const isReconfigure = context.mode === "reconfigure";
if (process.getuid?.() !== 0) { if (process.getuid?.() !== 0) {
fail("installer must run as root"); fail("installer must run as root");
@@ -33,7 +33,16 @@ export async function preflight(context: InstallContext): Promise<void> {
fail("HY2XS baseline supports only clean Debian 12"); 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"); fail("bundled HY2XS admin is missing from install package");
} }
+12 -2
View File
@@ -1,8 +1,8 @@
import type { InstallContext } from "../types/context"; import type { RuntimeContext } from "../types/context";
import { info } from "../lib/log"; import { info } from "../lib/log";
import { runHidden, runSecret, runVisible } from "../lib/process"; import { runHidden, runSecret, runVisible } from "../lib/process";
export async function smoke(context: InstallContext): Promise<void> { export async function smoke(context: RuntimeContext): Promise<void> {
if (context.options.skipStart) { if (context.options.skipStart) {
info("service start and smoke checks skipped by flag"); info("service start and smoke checks skipped by flag");
return; return;
@@ -30,6 +30,9 @@ export async function smoke(context: InstallContext): Promise<void> {
await runVisible`sudo -u hysteria test -r /etc/hysteria/config.yaml`; 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 -r /etc/hysteria/config.yaml`;
await runVisible`sudo -u hy2xs-admin test ! -w /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} '`; await runVisible`ss -H -ltn | grep -q '${context.config.uiBindHost}:${context.config.uiPort} '`;
if (context.config.uiBindHost === "127.0.0.1") { if (context.config.uiBindHost === "127.0.0.1") {
await runVisible`! ss -H -ltn | grep -q '0.0.0.0:${context.config.uiPort} '`; 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<void> {
throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`); 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(); const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
if (!adminConPass) { if (!adminConPass) {
throw new Error("admin connection password is empty in bootstrap secret file"); throw new Error("admin connection password is empty in bootstrap secret file");
+2 -2
View File
@@ -1,8 +1,8 @@
import type { InstallContext } from "../types/context"; import type { RuntimeContext } from "../types/context";
import { readText, renderTemplate, writeText } from "../lib/fs"; import { readText, renderTemplate, writeText } from "../lib/fs";
import { runVisible } from "../lib/process"; import { runVisible } from "../lib/process";
export async function deploySystemd(context: InstallContext): Promise<void> { export async function deploySystemd(context: RuntimeContext): Promise<void> {
const values = { const values = {
UI_BIND_HOST: context.config.uiBindHost, UI_BIND_HOST: context.config.uiBindHost,
UI_PORT: context.config.uiPort, UI_PORT: context.config.uiPort,
+20 -16
View File
@@ -1,4 +1,4 @@
export type InstallOptions = { export type CommonOptions = {
packageDir: string; packageDir: string;
sourceConfigPath: string; sourceConfigPath: string;
runtimeConfigPath: string; runtimeConfigPath: string;
@@ -7,17 +7,15 @@ export type InstallOptions = {
skipStart: boolean; skipStart: boolean;
}; };
export type ReconfigureOptions = { export type InstallOptions = CommonOptions;
packageDir: string;
sourceConfigPath: string; export type ReconfigureOptions = CommonOptions & {
runtimeConfigPath: string;
nonInteractive: boolean;
dryRun: boolean; dryRun: boolean;
apply: boolean; apply: boolean;
skipFirewall: boolean;
skipStart: boolean;
}; };
export type RunMode = "install" | "reconfigure";
export type TlsMode = "acme" | "file" | "self_signed_dev"; export type TlsMode = "acme" | "file" | "self_signed_dev";
export type RuntimeConfig = { export type RuntimeConfig = {
@@ -54,25 +52,31 @@ export type RuntimeConfig = {
hysteriaBandwidthDown: string; hysteriaBandwidthDown: string;
hysteriaIgnoreClientBandwidth: boolean; hysteriaIgnoreClientBandwidth: boolean;
hysteriaConfigPath: string; hysteriaConfigPath: string;
allowLatestHysteria: boolean;
hysteriaVersionPolicy: string;
installDir: string; installDir: string;
dataDir: string; dataDir: string;
logDir: string; logDir: string;
bootstrapAdminSecretPath: string; bootstrapAdminSecretPath: string;
}; };
export type InstallContext = { export type RuntimeContext = {
options: InstallOptions; mode: RunMode;
options: CommonOptions;
config: RuntimeConfig; config: RuntimeConfig;
packageVersion: string; packageVersion: string;
packageBuildId: string; packageBuildId: string;
installDate: string; installDate: string;
hysteriaAuthPassword: string;
hysteriaVersion: string; hysteriaVersion: string;
}; };
export type ReconfigureContext = { export type InstallContext = RuntimeContext & {
options: ReconfigureOptions; mode: "install";
config: RuntimeConfig; options: InstallOptions;
hysteriaTargetVersion: string;
hysteriaArtifactUrl: string;
hysteriaArtifactSha256: string;
};
export type ReconfigureContext = RuntimeContext & {
mode: "reconfigure";
options: ReconfigureOptions;
}; };
-2
View File
@@ -32,8 +32,6 @@ HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps
HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps HY2XS_HYSTERIA_BANDWIDTH_DOWN=50 mbps
HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false HY2XS_HYSTERIA_IGNORE_CLIENT_BANDWIDTH=false
HY2XS_HYSTERIA_CONFIG_PATH=/etc/hysteria/config.yaml 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_INSTALL_DIR=/opt/hy2xs-admin
HY2XS_DATA_DIR=/var/lib/hy2xs-admin HY2XS_DATA_DIR=/var/lib/hy2xs-admin
HY2XS_LOG_DIR=/var/log/hy2xs HY2XS_LOG_DIR=/var/log/hy2xs
+10
View File
@@ -24,6 +24,16 @@ if [ ! -x "$ORCHESTRATOR" ]; then
fail "Missing executable orchestrator artifact: $ORCHESTRATOR" fail "Missing executable orchestrator artifact: $ORCHESTRATOR"
fi 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 -d -m 0755 /usr/local/lib/hy2xs
install -m 0755 "$ORCHESTRATOR" "$ORCHESTRATOR_INSTALL_PATH" install -m 0755 "$ORCHESTRATOR" "$ORCHESTRATOR_INSTALL_PATH"
ln -sf "$ORCHESTRATOR_INSTALL_PATH" "$ORCHESTRATOR_SYMLINK" ln -sf "$ORCHESTRATOR_INSTALL_PATH" "$ORCHESTRATOR_SYMLINK"
+3
View File
@@ -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
+15
View File
@@ -3,6 +3,7 @@ set -euo pipefail
STAGE_DIR="tools/build/output/hy2xs-install" STAGE_DIR="tools/build/output/hy2xs-install"
ADMIN_BUILD_DIR="tools/build/output/hy2xs-admin-build" ADMIN_BUILD_DIR="tools/build/output/hy2xs-admin-build"
HYSTERIA_LOCK_FILE="tools/build/hysteria-lock.env"
prepare_stage() { prepare_stage() {
local version="$1" local version="$1"
@@ -62,6 +63,13 @@ write_metadata() {
local version="$1" local version="$1"
local build_id="$2" 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 'name=HY2XS\n'
printf 'version=%s\n' "$version" printf 'version=%s\n' "$version"
@@ -77,8 +85,15 @@ write_metadata() {
printf 'pnpm_version=%s\n' "$($PNPM_BIN --version)" printf 'pnpm_version=%s\n' "$($PNPM_BIN --version)"
printf 'hysteria_source=official-upstream\n' printf 'hysteria_source=official-upstream\n'
printf 'hysteria_target=linux-amd64\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" } >"$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" cd "$STAGE_DIR"
find . -type f ! -path './metadata/checksums.txt' -print0 \ find . -type f ! -path './metadata/checksums.txt' -print0 \