diff --git a/README.md b/README.md index f1bebb5..f8ca518 100644 --- a/README.md +++ b/README.md @@ -57,9 +57,17 @@ https://git.ext.flamy.studio/flamy_dev/HY2XS_flamy.git На чистом Debian 12 target нужно распаковать архив и запустить от root: ```sh -./install.sh --config /etc/hy2xs/hy2xs.env --non-interactive +./install.sh --non-interactive ``` +Опционально можно передать внешний source-config (не путь runtime-файла на target): + +```sh +./install.sh --config /root/custom-hy2xs.env --non-interactive +``` + +`/etc/hy2xs/hy2xs.env` создаётся install-слоем автоматически и далее редактируется оператором перед `reconfigure`. + После установки применяются команды оркестратора: ```sh diff --git a/apps/controller/config.go b/apps/controller/config.go index afec475..e360748 100644 --- a/apps/controller/config.go +++ b/apps/controller/config.go @@ -19,6 +19,21 @@ import ( "time" ) +func isOrchestratorManagedConfigKey(key string) bool { + switch key { + case constant.HUIWebPort, + constant.HUIWebContext, + constant.HUICrtPath, + constant.HUIKeyPath, + constant.Hysteria2Enable, + constant.Hysteria2Config, + constant.Hysteria2TrafficStatsSecret: + return true + default: + return false + } +} + func UpdateConfigs(c *gin.Context) { configsUpdateDto, err := validateField(c, dto.ConfigsUpdateDto{}) if err != nil { @@ -37,6 +52,11 @@ func UpdateConfigs(c *gin.Context) { key := *item.Key value := *item.Value + if isOrchestratorManagedConfigKey(key) { + vo.Fail(fmt.Sprintf("%s managed by orchestrator: use hy2xs-orchestrator reconfigure", key), c) + return + } + if key == constant.HUIWebPort && strconv.FormatInt(port, 10) != value { port, err := strconv.Atoi(value) if err != nil { @@ -117,17 +137,12 @@ func GetConfig(c *gin.Context) { } running := service.Hysteria2IsRunning() - - if (*config.Value == "1") != running { - enable := "0" + if *config.Key == constant.Hysteria2Enable { if running { - enable = "1" + configVo.Value = "1" + } else { + configVo.Value = "0" } - if err := service.UpdateConfig(constant.Hysteria2Enable, enable); err != nil { - vo.Fail(err.Error(), c) - return - } - configVo.Value = enable } vo.Success(configVo, c) @@ -284,6 +299,12 @@ func ImportConfig(c *gin.Context) { vo.Fail("content Unmarshal err", c) return } + for _, cfg := range configs { + if cfg.Key != nil && isOrchestratorManagedConfigKey(*cfg.Key) { + vo.Fail(fmt.Sprintf("%s managed by orchestrator: use hy2xs-orchestrator reconfigure", *cfg.Key), c) + return + } + } if err = service.UpsertConfig(configs); err != nil { vo.Fail(err.Error(), c) return diff --git a/apps/dao/sqlite.go b/apps/dao/sqlite.go index ad64dc1..ef68f13 100644 --- a/apps/dao/sqlite.go +++ b/apps/dao/sqlite.go @@ -2,7 +2,6 @@ package dao import ( "errors" - "fmt" "github.com/glebarez/sqlite" "github.com/sirupsen/logrus" "gorm.io/gorm" @@ -120,7 +119,7 @@ func ensureAccountSchema() error { func ensureSecureBootstrapAdmin() error { adminUser := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_USER")) if adminUser == "" { - adminUser = "admin" + adminUser = "hy2xsadmin" } adminPassword := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_INITIAL_PASSWORD")) if adminPassword == "" { @@ -136,11 +135,14 @@ func ensureSecureBootstrapAdmin() error { deviceNo := int64(envInt("HY2XS_ADMIN_DEVICE_NO", 6)) role := "admin" deleted := int64(0) - conPassSecret, conErr := util.RandomString(28) - if conErr != nil { - return conErr + conPass := strings.TrimSpace(os.Getenv("HY2XS_ADMIN_CON_PASS")) + if conPass == "" { + generated, genErr := util.RandomString(28) + if genErr != nil { + return genErr + } + conPass = generated } - conPass := fmt.Sprintf("%s.%s", adminUser, conPassSecret) hash, hashErr := util.HashPassword(adminPassword) if hashErr != nil { return hashErr @@ -169,16 +171,6 @@ func ensureSecureBootstrapAdmin() error { if admin.Pass == nil { return nil } - - updates := map[string]interface{}{ - "username": adminUser, - "pass": hash, - "con_pass": conPass, - "force_password_change": forcePasswordChange, - } - if updateErr := UpdateAccount([]int64{*admin.Id}, updates); updateErr != nil { - return updateErr - } return nil } diff --git a/apps/proxy/hysteria2_api.go b/apps/proxy/hysteria2_api.go index ded58ba..34cb64e 100644 --- a/apps/proxy/hysteria2_api.go +++ b/apps/proxy/hysteria2_api.go @@ -27,9 +27,6 @@ func NewHysteria2Api(apiPort int64) *Hysteria2Api { // ListUsers Информация о трафике каждого пользователя func (h *Hysteria2Api) ListUsers(clear bool, secret string) (map[string]bo.Hysteria2UserTraffic, error) { var users map[string]bo.Hysteria2UserTraffic - if !NewHysteria2Instance().IsRunning() { - return users, nil - } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://127.0.0.1:%d/traffic", h.apiPort) @@ -66,9 +63,6 @@ func (h *Hysteria2Api) ListUsers(clear bool, secret string) (map[string]bo.Hyste // KickUsers Принудительное отключение func (h *Hysteria2Api) KickUsers(keys []string, secret string) error { - if !NewHysteria2Instance().IsRunning() { - return nil - } usernamesByte, err := json.Marshal(keys) if err != nil { logrus.Errorf("Hysteria2 KickUsers Marshal err: %v", err) @@ -101,9 +95,6 @@ func (h *Hysteria2Api) KickUsers(keys []string, secret string) error { // OnlineUsers Пользователи онлайн func (h *Hysteria2Api) OnlineUsers(secret string) (map[string]int64, error) { var onlineUsers map[string]int64 - if !NewHysteria2Instance().IsRunning() { - return onlineUsers, nil - } ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() url := fmt.Sprintf("http://127.0.0.1:%d/online", h.apiPort) diff --git a/apps/service/config.go b/apps/service/config.go index 8f4fe8c..9c656bd 100644 --- a/apps/service/config.go +++ b/apps/service/config.go @@ -15,26 +15,6 @@ import ( ) func UpdateConfig(key string, value string) error { - if key == constant.Hysteria2Enable { - if value == "1" { - hysteria2Config, err := GetHysteria2Config() - if err != nil { - return err - } - if hysteria2Config.Listen == nil || *hysteria2Config.Listen == "" { - logrus.Errorf("hysteria2 config is empty") - return errors.New("hysteria2 config is empty") - } - // Запуск Hysteria2 - if err = StartHysteria2(); err != nil { - return err - } - } else { - if err := StopHysteria2(); err != nil { - return err - } - } - } return dao.UpdateConfig([]string{key}, map[string]interface{}{"value": value}) } diff --git a/apps/service/cron.go b/apps/service/cron.go index 5aaf25b..2b5e2d2 100644 --- a/apps/service/cron.go +++ b/apps/service/cron.go @@ -17,27 +17,25 @@ var kickMutex sync.Mutex func CronHandleAccount() { go func() { - hysteriaEnable, err := dao.GetConfig("key = ?", constant.Hysteria2Enable) + if !Hysteria2IsRunning() { + return + } + + apiPort, err := GetHysteria2ApiPort() if err != nil { return } - if hysteriaEnable.Value != nil && *hysteriaEnable.Value == "1" { - apiPort, err := GetHysteria2ApiPort() - if err != nil { - return - } - trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) - if err != nil { - return - } - - // Сохранение данных трафика - go saveAccountTraffic(apiPort, *trafficSecretConfig.Value) - - // Принудительное отключение - go kickAccount(apiPort, *trafficSecretConfig.Value) + trafficSecretConfig, err := dao.GetConfig("key = ?", constant.Hysteria2TrafficStatsSecret) + if err != nil { + return } + + // Сохранение данных трафика + go saveAccountTraffic(apiPort, *trafficSecretConfig.Value) + + // Принудительное отключение + go kickAccount(apiPort, *trafficSecretConfig.Value) }() } diff --git a/apps/service/server.go b/apps/service/server.go index 7c22617..adfe3ad 100644 --- a/apps/service/server.go +++ b/apps/service/server.go @@ -27,10 +27,6 @@ func StartServer(crtPath string, keyPath string) error { } func StopServer() error { - if err := StopHysteria2(); err != nil { - return err - } - ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() if err := server.Shutdown(ctx); err != nil { diff --git a/docs/09-post-install-env.md b/docs/09-post-install-env.md index d64bf14..d216f68 100644 --- a/docs/09-post-install-env.md +++ b/docs/09-post-install-env.md @@ -63,6 +63,8 @@ - `SSH_PORT` - `HY2XS_FIREWALL_ENABLED` - `HY2XS_FIREWALL_STAGED_APPLY` +- `HY2XS_ADMIN_USER` +- `HY2XS_FORCE_PASSWORD_CHANGE` ### Hysteria - `HY2_SOURCE=official-upstream` @@ -101,6 +103,11 @@ 3. оператор запускает `reconfigure --dry-run`, затем `reconfigure --apply`; 4. оркестратор обновляет runtime и перезаписывает snapshot. +Важно: +- `HY2XS_ADMIN_INITIAL_PASSWORD` используется только для первичного bootstrap seed; +- `HY2XS_ADMIN_CON_PASS` — отдельная runtime-сущность для Hysteria auth/smoke; +- после первичного seed перезапуски `hy2xs-admin` не должны переопределять пароль admin и `con_pass`. + ## Что нельзя делать - сваливать туда временный мусор diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index 1642d85..a9ac934 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -49,7 +49,10 @@ 14. TLS mode в `config.yaml` соответствует runtime env (`acme|file|self_signed_dev`) 15. при `HY2XS_TLS_MODE=acme` в `config.yaml` выставлен `acme.type` из `HY2XS_ACME_TYPE` 16. subscribe/node URL в API/QR формируются по `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT` -15. `nft -c -f /etc/nftables.conf` проходит после apply +17. `nft -c -f /etc/nftables.conf` проходит после apply +18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin` +19. остановка/рестарт UI не останавливает `hysteria-server` +20. traffic accounting/kick ориентируются на systemd status, а не на SQLite `HYSTERIA2_ENABLE` ## D. Negative tests @@ -79,3 +82,4 @@ 9. отсутствует production path для port hopping 10. UI не запускается от root 11. клиентские endpoint не зависят от request `Host`/`hostname` +12. production build verify падает, если `config/hy2xs.env` содержит placeholder-значения diff --git a/orchestrator/src/config/env.ts b/orchestrator/src/config/env.ts index 81d0780..5617df1 100644 --- a/orchestrator/src/config/env.ts +++ b/orchestrator/src/config/env.ts @@ -119,6 +119,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig { uiPort, adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"), adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD), + adminConPass: requireValue("HY2XS_ADMIN_CON_PASS", valueOrGenerate(env.HY2XS_ADMIN_CON_PASS)), forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true), tlsMode, acmeType, @@ -202,6 +203,7 @@ export function renderRuntimeEnv(config: RuntimeConfig): string { `HY2XS_UI_PORT=${config.uiPort}`, `HY2XS_ADMIN_USER=${config.adminUser}`, `HY2XS_ADMIN_INITIAL_PASSWORD=${config.adminInitialPassword}`, + `HY2XS_ADMIN_CON_PASS=${config.adminConPass}`, `HY2XS_FORCE_PASSWORD_CHANGE=${config.forcePasswordChange}`, `HY2XS_TLS_MODE=${config.tlsMode}`, `HY2XS_ACME_TYPE=${config.acmeType}`, diff --git a/orchestrator/src/steps/env.ts b/orchestrator/src/steps/env.ts index 979b94f..58dce0a 100644 --- a/orchestrator/src/steps/env.ts +++ b/orchestrator/src/steps/env.ts @@ -30,5 +30,5 @@ export async function writePostInstallEnv(context: InstallContext): Promise { throw new Error(`unexpected auth response for invalid credentials: ${invalidAuthResponse}`); } - const validAuthResponse = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${context.config.adminUser}.${context.config.adminInitialPassword}","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`; + const adminConPass = (await runSecret`grep '^${context.config.adminUser}:' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d: -f2-`).trim(); + if (!adminConPass) { + throw new Error("admin connection password is empty in bootstrap secret file"); + } + + const validAuthResponse = await runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${adminConPass}","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`; if (!/"ok"\s*:\s*true/.test(validAuthResponse)) { throw new Error(`unexpected auth response for valid credentials`); } diff --git a/orchestrator/src/types/context.ts b/orchestrator/src/types/context.ts index 6a36bc6..f4a0502 100644 --- a/orchestrator/src/types/context.ts +++ b/orchestrator/src/types/context.ts @@ -32,6 +32,7 @@ export type RuntimeConfig = { uiPort: number; adminUser: string; adminInitialPassword: string; + adminConPass: string; forcePasswordChange: boolean; tlsMode: TlsMode; acmeType: "http" | "tls" | "dns"; diff --git a/package/config/hy2xs.env b/package/config/hy2xs.env index 6a596f8..979eb38 100644 --- a/package/config/hy2xs.env +++ b/package/config/hy2xs.env @@ -8,8 +8,9 @@ HY2XS_FIREWALL_ENABLED=true HY2XS_FIREWALL_STAGED_APPLY=true HY2XS_UI_BIND_HOST=127.0.0.1 HY2XS_UI_PORT=8080 -HY2XS_ADMIN_USER=admin +HY2XS_ADMIN_USER=hy2xsadmin HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__ +HY2XS_ADMIN_CON_PASS=__GENERATE__ HY2XS_FORCE_PASSWORD_CHANGE=true HY2XS_TLS_MODE=acme HY2XS_ACME_TYPE=http diff --git a/tools/build/lib/verify.sh b/tools/build/lib/verify.sh index 1e6b712..3cba1d8 100644 --- a/tools/build/lib/verify.sh +++ b/tools/build/lib/verify.sh @@ -7,6 +7,7 @@ require_repo_layout() { [ -f "orchestrator/bun.lock" ] || fail "missing orchestrator/bun.lock" [ -f "package/install.sh" ] || fail "missing package/install.sh" [ -d "package/templates" ] || fail "missing package/templates" + [ -f "package/config/hy2xs.env" ] || fail "missing package/config/hy2xs.env" [ -f "package/templates/env/post-install.env.tpl" ] || fail "missing package/templates/env/post-install.env.tpl" [ -d "package/systemd" ] || fail "missing package/systemd" [ -d "apps" ] || fail "missing apps HY2XS admin source" @@ -31,6 +32,12 @@ verify_archive() { printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hysteria-server.service$' || fail "archive missing hysteria systemd unit" printf '%s\n' "$listing" | grep -q '^hy2xs-install/systemd/hy2xs-admin.service$' || fail "archive missing admin systemd unit" printf '%s\n' "$listing" | grep -q '^hy2xs-install/templates/hysteria/config.yaml.tpl$' || fail "archive missing Hysteria config template" + printf '%s\n' "$listing" | grep -q '^hy2xs-install/config/hy2xs.env$' || fail "archive missing canonical runtime config" printf '%s\n' "$listing" | grep -q '^hy2xs-install/templates/env/post-install.env.tpl$' || fail "archive missing post-install env template" printf '%s\n' "$listing" | grep -q '^hy2xs-install/metadata/checksums.txt$' || fail "archive missing checksums" + + local env_content + env_content="$(tar -xOzf "$archive" hy2xs-install/config/hy2xs.env)" + printf '%s\n' "$env_content" | grep -q 'replace-with-your-domain.example' && fail "packaged hy2xs.env contains placeholder domain" + printf '%s\n' "$env_content" | grep -q 'replace-with-your-email@example.com' && fail "packaged hy2xs.env contains placeholder email" }