From d2898ac10cc74aee616fd43ccc80ca9a68975517 Mon Sep 17 00:00:00 2001 From: Crimson Date: Fri, 8 May 2026 05:06:29 +0500 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=BE=D0=BB=D0=BD=D1=8B=D0=B9=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B4=D0=BE=D0=B2=D1=8B=D0=B9=20=D1=84=D0=B8=D0=BA?= =?UTF-8?q?=D1=81=20HY2XS/Hysteria2:=20auth=20DTO,=20=D0=B2=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F=20bind,=20smoke,=20URI,=20?= =?UTF-8?q?preflight=20=D0=B8=20env-=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0?= =?UTF-8?q?=D0=BA=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/controller/validator.go | 9 +++++++-- apps/model/dto/hysteria2.go | 2 +- apps/service/hysteria2_api.go | 9 --------- docs/03-server-hysteria2.md | 3 +++ orchestrator/src/config/env.ts | 22 ++++++++++++++++++++-- orchestrator/src/steps/env.ts | 3 +++ orchestrator/src/steps/preflight.ts | 2 +- orchestrator/src/steps/smoke.ts | 11 ++++++++--- package/config/hy2xs.env | 5 +++-- package/templates/env/post-install.env.tpl | 6 +++--- 10 files changed, 49 insertions(+), 23 deletions(-) diff --git a/apps/controller/validator.go b/apps/controller/validator.go index ce36e56..05eb9b5 100644 --- a/apps/controller/validator.go +++ b/apps/controller/validator.go @@ -26,12 +26,17 @@ func validateStr(f validator.FieldLevel) bool { } func validateField[T interface{}](c *gin.Context, field T) (T, error) { + var bindErr error if c.Request.Method == http.MethodGet { - _ = c.ShouldBindQuery(&field) + bindErr = c.ShouldBindQuery(&field) } else if c.Request.Method == http.MethodPost || c.Request.Method == http.MethodPut || c.Request.Method == http.MethodDelete { - _ = c.ShouldBindJSON(&field) + bindErr = c.ShouldBindJSON(&field) + } + if bindErr != nil { + vo.Fail(constant.InvalidError, c) + return field, fmt.Errorf(constant.InvalidError) } if err := validate.Struct(&field); err != nil { vo.Fail(constant.InvalidError, c) diff --git a/apps/model/dto/hysteria2.go b/apps/model/dto/hysteria2.go index e4e27ce..8060b84 100644 --- a/apps/model/dto/hysteria2.go +++ b/apps/model/dto/hysteria2.go @@ -3,7 +3,7 @@ package dto type Hysteria2AuthDto struct { Addr *string `json:"addr" form:"addr" validate:"required"` Auth *string `json:"auth" form:"auth" validate:"required"` - Tx *string `json:"tx" form:"tx" validate:"required"` + Tx *int64 `json:"tx" form:"tx" validate:"required"` } type Hysteria2KickDto struct { diff --git a/apps/service/hysteria2_api.go b/apps/service/hysteria2_api.go index 3c37322..1354c13 100644 --- a/apps/service/hysteria2_api.go +++ b/apps/service/hysteria2_api.go @@ -270,19 +270,10 @@ func Hysteria2Url(accountId int64) (string, error) { hysteria2Config.ACME.Domains != nil && len(hysteria2Config.ACME.Domains) > 0 { urlConfig += fmt.Sprintf("&sni=%s", hysteria2Config.ACME.Domains[0]) - // shadowrocket - urlConfig += fmt.Sprintf("&peer=%s", hysteria2Config.ACME.Domains[0]) } urlConfig += "&insecure=0" - if hysteria2Config.Bandwidth != nil && - hysteria2Config.Bandwidth.Down != nil && - *hysteria2Config.Bandwidth.Down != "" { - // shadowrocket - urlConfig += fmt.Sprintf("&downmbps=%s", url.PathEscape(*hysteria2Config.Bandwidth.Down)) - } - hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark) if err != nil { return "", err diff --git a/docs/03-server-hysteria2.md b/docs/03-server-hysteria2.md index 3609194..9ffacbc 100644 --- a/docs/03-server-hysteria2.md +++ b/docs/03-server-hysteria2.md @@ -71,6 +71,9 @@ Hysteria2 — основной транспортный компонент се - `acme` -> только `acme` block в конфиге; - `acme` block обязан содержать `type: http|tls` из runtime env (`HY2XS_ACME_TYPE`); - `HY2XS_ACME_TYPE=dns` в production-профиле запрещён до отдельной реализации; +- `HY2XS_HYSTERIA_AUTH_MODE` зафиксирован в `http` и валидируется fail-fast; +- `HY2XS_HYSTERIA_OBFS_TYPE` зафиксирован в `salamander` и валидируется fail-fast; +- блок `masquerade` в baseline не задаётся (допустимо, но приводит к `404 Not Found` на обычный HTTP трафик); - `file` -> только `tls.cert`/`tls.key` block; - `self_signed_dev` -> только dev сценарии. diff --git a/orchestrator/src/config/env.ts b/orchestrator/src/config/env.ts index 99619fc..4c8ddcc 100644 --- a/orchestrator/src/config/env.ts +++ b/orchestrator/src/config/env.ts @@ -127,6 +127,22 @@ function normalizeAcmeType(value: string): "http" | "tls" | "dns" { throw new Error(`invalid HY2XS_ACME_TYPE: ${value}`); } +function normalizeFixedHysteriaAuthMode(value: string | undefined): "http" { + const mode = value || "http"; + if (mode !== "http") { + throw new Error("HY2XS_HYSTERIA_AUTH_MODE is fixed in HY2XS production profile: http"); + } + return "http"; +} + +function normalizeFixedHysteriaObfsType(value: string | undefined): "salamander" { + const obfsType = value || "salamander"; + if (obfsType !== "salamander") { + throw new Error("HY2XS_HYSTERIA_OBFS_TYPE is fixed in HY2XS production profile: salamander"); + } + return "salamander"; +} + function normalizeSafeAbsolutePath(name: string, value: string, options?: { disallowTmp?: boolean }): string { const v = value.trim(); if (!v.startsWith("/")) { @@ -157,6 +173,8 @@ export function parseRuntimeEnv(content: string): RuntimeConfig { const tlsMode = normalizeTlsMode(env.HY2XS_TLS_MODE || "acme"); const acmeType = normalizeAcmeType(env.HY2XS_ACME_TYPE || "http"); const firewallMode = normalizeFirewallMode(env); + const hysteriaAuthMode = normalizeFixedHysteriaAuthMode(env.HY2XS_HYSTERIA_AUTH_MODE); + const hysteriaObfsType = normalizeFixedHysteriaObfsType(env.HY2XS_HYSTERIA_OBFS_TYPE); const config: RuntimeConfig = { domain: env.HY2XS_DOMAIN || "", @@ -181,14 +199,14 @@ export function parseRuntimeEnv(content: string): RuntimeConfig { tlsKeyPath: normalizeSafeAbsolutePath("HY2XS_TLS_KEY_PATH", env.HY2XS_TLS_KEY_PATH || "/etc/hysteria/server.key"), hysteriaBindHost: normalizeIpv4Host("HY2XS_HYSTERIA_BIND_HOST", env.HY2XS_HYSTERIA_BIND_HOST || "0.0.0.0"), hysteriaPort, - hysteriaAuthMode: "http", + hysteriaAuthMode, hysteriaTrafficStatsHost: normalizeIpv4Host( "HY2XS_HYSTERIA_TRAFFIC_STATS_HOST", env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1" ), hysteriaTrafficStatsPort: trafficStatsPort, hysteriaTrafficStatsSecret: valueOrGenerate(env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET), - hysteriaObfsType: "salamander", + hysteriaObfsType, hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", valueOrGenerate(env.HY2XS_HYSTERIA_OBFS_PASSWORD)), hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps", hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps", diff --git a/orchestrator/src/steps/env.ts b/orchestrator/src/steps/env.ts index a294cfd..86772c9 100644 --- a/orchestrator/src/steps/env.ts +++ b/orchestrator/src/steps/env.ts @@ -20,6 +20,9 @@ export async function writePostInstallEnv(context: RuntimeContext): Promise { const aaaa = await run`getent ahostsv6 ${context.config.domain}`; if (aaaa.trim()) { fail( - `domain ${context.config.domain} has AAAA record while HY2XS is IPv4-only; remove AAAA or set HY2XS_ALLOW_AAAA_WITH_IPV4_ONLY=true` + `domain ${context.config.domain} has AAAA record while HY2XS profile is IPv4-only; remove AAAA record before install` ); } } catch { diff --git a/orchestrator/src/steps/smoke.ts b/orchestrator/src/steps/smoke.ts index cd90095..768464f 100644 --- a/orchestrator/src/steps/smoke.ts +++ b/orchestrator/src/steps/smoke.ts @@ -148,18 +148,23 @@ export async function smoke(context: RuntimeContext): Promise { "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`, + 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, error) => new Error(`unexpected auth response for invalid credentials: ${response ?? String(error)}`), ); 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`; + 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 invalidTypeAuthCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -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 (invalidTypeAuthCode.trim() !== "400") { + throw new Error(`unexpected auth status for tx as string: ${invalidTypeAuthCode}`); + } + if (context.mode === "install") { const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim(); if (!adminConPass) { @@ -170,7 +175,7 @@ export async function smoke(context: RuntimeContext): Promise { "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`, + 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)}`), ); diff --git a/package/config/hy2xs.env b/package/config/hy2xs.env index cddff0b..1c93bbb 100644 --- a/package/config/hy2xs.env +++ b/package/config/hy2xs.env @@ -4,9 +4,8 @@ HY2XS_DOMAIN=uk.api.withen.pro HY2XS_PUBLIC_HOST=uk.api.withen.pro HY2XS_PUBLIC_PORT=443 HY2XS_SSH_PORT=22 -HY2XS_FIREWALL_ENABLED=true +HY2XS_FIREWALL_MODE=managed HY2XS_FIREWALL_STAGED_APPLY=true -HY2XS_FIREWALL_ALLOW_TAKEOVER=false HY2XS_UI_BIND_HOST=127.0.0.1 HY2XS_UI_PUBLIC_ACCESS=false HY2XS_UI_PORT=8080 @@ -22,10 +21,12 @@ HY2XS_TLS_CERT_PATH=/etc/hysteria/server.crt HY2XS_TLS_KEY_PATH=/etc/hysteria/server.key HY2XS_HYSTERIA_BIND_HOST=0.0.0.0 HY2XS_HYSTERIA_PORT=443 +# fixed in production profile: must remain http HY2XS_HYSTERIA_AUTH_MODE=http HY2XS_HYSTERIA_TRAFFIC_STATS_HOST=127.0.0.1 HY2XS_HYSTERIA_TRAFFIC_STATS_PORT=36712 HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET=__GENERATE__ +# fixed in production profile: must remain salamander HY2XS_HYSTERIA_OBFS_TYPE=salamander HY2XS_HYSTERIA_OBFS_PASSWORD=__GENERATE__ HY2XS_HYSTERIA_BANDWIDTH_UP=50 mbps diff --git a/package/templates/env/post-install.env.tpl b/package/templates/env/post-install.env.tpl index bfeb2c6..3eb9dc1 100644 --- a/package/templates/env/post-install.env.tpl +++ b/package/templates/env/post-install.env.tpl @@ -32,9 +32,9 @@ HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/hui/hysteria2/auth HY2_TRAFFIC_STATS_LISTEN={{HYSTERIA_API_HOST}}:{{HYSTERIA_API_PORT}} HY2_OBFS_TYPE=salamander HY2_OBFS_PASSWORD={{HYSTERIA_OBFS_PASSWORD}} -HY2_BANDWIDTH_UP_Mbps=50 -HY2_BANDWIDTH_DOWN_Mbps=50 -HY2_IGNORE_CLIENT_BANDWIDTH=false +HY2_BANDWIDTH_UP={{BANDWIDTH_UP}} +HY2_BANDWIDTH_DOWN={{BANDWIDTH_DOWN}} +HY2_IGNORE_CLIENT_BANDWIDTH={{IGNORE_CLIENT_BANDWIDTH}} HY2_CONFIG_PATH=/etc/hysteria/config.yaml HUI_ENABLED=true