From b228928e99baa1d728e6e0cd5366ffdb2212c518 Mon Sep 17 00:00:00 2001 From: Crimson Date: Thu, 30 Apr 2026 23:54:15 +0500 Subject: [PATCH] =?UTF-8?q?fix12:=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20env-=D0=B0=D1=80=D1=82=D0=B5=D1=84?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BE=D0=B2=20=D0=B8=20=D1=81=D1=82=D0=B0?= =?UTF-8?q?=D0=B1=D0=B8=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20smoke?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/controller/config.go | 8 ++--- apps/router/hysteria2.go | 4 --- docs/11-testing-and-acceptance.md | 2 +- orchestrator/src/commands/install.ts | 4 ++- orchestrator/src/steps/env.ts | 3 ++ orchestrator/src/steps/smoke.ts | 50 +++++++++++++++++----------- 6 files changed, 42 insertions(+), 29 deletions(-) diff --git a/apps/controller/config.go b/apps/controller/config.go index 6d87c79..7694aa8 100644 --- a/apps/controller/config.go +++ b/apps/controller/config.go @@ -180,7 +180,7 @@ func GetHysteria2Config(c *gin.Context) { } func UpdateHysteria2Config(c *gin.Context) { - vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) + vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) } func ExportHysteria2Config(c *gin.Context) { @@ -249,7 +249,7 @@ func ExportHysteria2Config(c *gin.Context) { } func ImportHysteria2Config(c *gin.Context) { - vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) + vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) } func ExportConfig(c *gin.Context) { @@ -326,9 +326,9 @@ func Hysteria2AcmePath(c *gin.Context) { } func RestartServer(c *gin.Context) { - vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) + vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) } func UploadCertFile(c *gin.Context) { - vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) + vo.Fail("managed by orchestrator: use hy2xs-orchestrator reconfigure", c) } diff --git a/apps/router/hysteria2.go b/apps/router/hysteria2.go index 188f859..7f92dd7 100644 --- a/apps/router/hysteria2.go +++ b/apps/router/hysteria2.go @@ -12,10 +12,6 @@ func initHysteria2MachineAuthRouter(hysteria2Api *gin.RouterGroup) { } } -func initHysteria2SubscribeRouter(hysteria2Api *gin.RouterGroup) { - hysteria2Api.GET("/:conPass", controller.Hysteria2Subscribe) -} - func initHysteria2Router(hysteria2Api *gin.RouterGroup) { hysteria2 := hysteria2Api.Group("/hysteria2") { diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index cbf61d8..95d956c 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -48,7 +48,7 @@ 13. `trafficStats` API: корректный secret принимает запрос, неверный secret отклоняется 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` +16. direct `hysteria2://` node URL в API/QR формируется по `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT`; subscription delivery endpoint отключён в baseline и не входит в acceptance 17. `nft -c -f /etc/nftables.conf` проходит после apply 18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin` 19. остановка/рестарт UI не останавливает `hysteria-server` diff --git a/orchestrator/src/commands/install.ts b/orchestrator/src/commands/install.ts index 8ec6a5b..66b974e 100644 --- a/orchestrator/src/commands/install.ts +++ b/orchestrator/src/commands/install.ts @@ -12,7 +12,7 @@ import { installHysteria } from "../steps/hysteria"; import { generateConfig } from "../steps/config"; import { deploySystemd } from "../steps/systemd"; import { applyFirewall } from "../steps/firewall"; -import { writePostInstallEnv } from "../steps/env"; +import { writeBootstrapAdminSecret, writePostInstallEnv } from "../steps/env"; import { smoke } from "../steps/smoke"; export async function install(options: InstallOptions): Promise { @@ -64,6 +64,8 @@ export async function install(options: InstallOptions): Promise { await applyFirewall(context); step("post-install env"); await writePostInstallEnv(context); + step("bootstrap admin secret"); + await writeBootstrapAdminSecret(context); step("smoke checks"); await smoke(context); } diff --git a/orchestrator/src/steps/env.ts b/orchestrator/src/steps/env.ts index 7207651..c3d55b2 100644 --- a/orchestrator/src/steps/env.ts +++ b/orchestrator/src/steps/env.ts @@ -33,6 +33,9 @@ export async function writePostInstallEnv(context: RuntimeContext): Promise { await writeText( context.config.bootstrapAdminSecretPath, [ diff --git a/orchestrator/src/steps/smoke.ts b/orchestrator/src/steps/smoke.ts index 9cd7182..21d25df 100644 --- a/orchestrator/src/steps/smoke.ts +++ b/orchestrator/src/steps/smoke.ts @@ -3,24 +3,31 @@ import { info } from "../lib/log"; import { runHidden, runSecret, runVisible } from "../lib/process"; async function retry( + label: string, attempts: number, delayMs: number, action: () => Promise, validate: (value: T) => boolean, - errorFactory: (value: T) => Error, + errorFactory: (value: T | undefined, error: unknown) => Error, ): Promise { let lastValue: T | undefined; + let lastError: unknown; for (let i = 0; i < attempts; i += 1) { - const value = await action(); - lastValue = value; - if (validate(value)) { - return value; + try { + const value = await action(); + lastValue = value; + if (validate(value)) { + return value; + } + } catch (error) { + lastError = error; } if (i < attempts - 1) { + info(`${label}: retry ${i + 1}/${attempts}`); await runHidden`sleep ${Math.max(1, Math.ceil(delayMs / 1000))}`; } } - throw errorFactory(lastValue as T); + throw errorFactory(lastValue, lastError); } export async function smoke(context: RuntimeContext): Promise { @@ -62,11 +69,12 @@ export async function smoke(context: RuntimeContext): Promise { await runVisible`! ss -H -ltn | grep -q '\[::\]:${context.config.uiPort} '`; await runVisible`! ss -H -lun | grep -q '\[::\]:${context.config.hysteriaPort} '`; const invalidAuthResponse = await retry( + "auth invalid credentials", 5, 1000, async () => 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`, (response) => /"ok"\s*:\s*false/.test(response), - (response) => new Error(`unexpected auth response for invalid credentials: ${response}`), + (response, error) => new Error(`unexpected auth response for invalid credentials: ${response ?? String(error)}`), ); for (let i = 0; i < 10; i += 1) { @@ -76,25 +84,29 @@ export async function smoke(context: RuntimeContext): Promise { } } - 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"); + if (context.mode === "install") { + 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"); + } + + await retry( + "auth valid credentials", + 10, + 1000, + async () => 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`, + (response) => /"ok"\s*:\s*true/.test(response), + (response, error) => new Error(`unexpected auth response for valid credentials: ${response ?? String(error)}`), + ); } - const validAuthResponse = await retry( - 10, - 1000, - async () => 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`, - (response) => /"ok"\s*:\s*true/.test(response), - () => new Error("unexpected auth response for valid credentials"), - ); - await retry( + "trafficStats valid secret", 10, 1000, async () => runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`, (code) => /^2\d\d$/.test(code.trim()), - (code) => new Error(`unexpected trafficStats status for valid secret: ${code}`), + (code, error) => new Error(`unexpected trafficStats status for valid secret: ${code ?? String(error)}`), ); const deniedCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`; if (!/(401|403)/.test(deniedCode)) {