From 06540087fda40f603b54504fd316563803cd6e74 Mon Sep 17 00:00:00 2001 From: Crimson Date: Fri, 8 May 2026 07:14:41 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B2=D1=91=D0=BB=20=D0=B4=D0=BE?= =?UTF-8?q?=20=D0=BF=D1=80=D0=BE=D0=B4=D0=B0=20=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D1=8E=20Hysteria2:=20=D0=B1?= =?UTF-8?q?=D0=B5=D0=B7=D0=BE=D0=BF=D0=B0=D1=81=D0=BD=D1=8B=D0=B9=20URI,?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D1=8B,=20=D0=B0=D0=BA=D1=82=D1=83?= =?UTF-8?q?=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=BE?= =?UTF-8?q?=D0=BA=D0=BE=D0=B2=20=D0=B8=20hardening=20env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/service/hysteria2_api.go | 69 +++++++++++++-------- apps/service/hysteria2_api_test.go | 74 +++++++++++++++++++++++ docs/06-speed-limits-and-congestion.md | 4 +- docs/09-post-install-env.md | 4 +- docs/12-operations-and-troubleshooting.md | 2 +- orchestrator/src/config/env.ts | 18 +++++- 6 files changed, 140 insertions(+), 31 deletions(-) create mode 100644 apps/service/hysteria2_api_test.go diff --git a/apps/service/hysteria2_api.go b/apps/service/hysteria2_api.go index 1354c13..3992980 100644 --- a/apps/service/hysteria2_api.go +++ b/apps/service/hysteria2_api.go @@ -8,6 +8,7 @@ import ( "hy2xs-admin/model/bo" "hy2xs-admin/model/constant" "hy2xs-admin/proxy" + "net" "net/url" "os" "strconv" @@ -255,34 +256,54 @@ func Hysteria2Url(accountId int64) (string, error) { if err != nil { return "", err } - - urlConfig := "" - if hysteria2Config.Obfs != nil && - hysteria2Config.Obfs.Type != nil && - *hysteria2Config.Obfs.Type == "salamander" && - hysteria2Config.Obfs.Salamander != nil && - hysteria2Config.Obfs.Salamander.Password != nil && - *hysteria2Config.Obfs.Salamander.Password != "" { - urlConfig += fmt.Sprintf("&obfs=salamander&obfs-password=%s", *hysteria2Config.Obfs.Salamander.Password) - } - - if hysteria2Config.ACME != nil && - hysteria2Config.ACME.Domains != nil && - len(hysteria2Config.ACME.Domains) > 0 { - urlConfig += fmt.Sprintf("&sni=%s", hysteria2Config.ACME.Domains[0]) - } - - urlConfig += "&insecure=0" - hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark) if err != nil { return "", err } - if *hysteria2ConfigRemark.Value != "" { - urlConfig += fmt.Sprintf("#%s", *hysteria2ConfigRemark.Value) + + remark := "" + if hysteria2ConfigRemark.Value != nil { + remark = *hysteria2ConfigRemark.Value } - if urlConfig != "" { - urlConfig = "/?" + strings.TrimPrefix(urlConfig, "&") + + obfsType := "" + obfsPassword := "" + if hysteria2Config.Obfs != nil && + hysteria2Config.Obfs.Type != nil && + hysteria2Config.Obfs.Salamander != nil && + hysteria2Config.Obfs.Salamander.Password != nil { + obfsType = *hysteria2Config.Obfs.Type + obfsPassword = *hysteria2Config.Obfs.Salamander.Password } - return fmt.Sprintf("hysteria2://%s@%s:%d", *account.ConPass, hostname, port) + urlConfig, nil + + sni := "" + if hysteria2Config.ACME != nil && len(hysteria2Config.ACME.Domains) > 0 { + sni = hysteria2Config.ACME.Domains[0] + } + + return buildHysteria2Url(*account.ConPass, hostname, port, obfsType, obfsPassword, sni, remark), nil +} + +func buildHysteria2Url(conPass string, hostname string, port int, obfsType string, obfsPassword string, sni string, remark string) string { + query := url.Values{} + if obfsType == "salamander" && obfsPassword != "" { + query.Set("obfs", "salamander") + query.Set("obfs-password", obfsPassword) + } + if sni != "" { + query.Set("sni", sni) + } + query.Set("insecure", "0") + + u := url.URL{ + Scheme: "hysteria2", + User: url.User(conPass), + Host: net.JoinHostPort(hostname, strconv.Itoa(port)), + Path: "/", + RawQuery: query.Encode(), + } + if strings.TrimSpace(remark) != "" { + u.Fragment = remark + } + return u.String() } diff --git a/apps/service/hysteria2_api_test.go b/apps/service/hysteria2_api_test.go new file mode 100644 index 0000000..7b8e3b3 --- /dev/null +++ b/apps/service/hysteria2_api_test.go @@ -0,0 +1,74 @@ +package service + +import ( + "net/url" + "strings" + "testing" +) + +func TestBuildHysteria2Url_EncodesUserInfoQueryAndFragment(t *testing.T) { + raw := buildHysteria2Url( + "u@ser:#&=+ pass", + "example.com", + 443, + "salamander", + "obf+s&pass=@x", + "exa mple.com", + "my remark #1", + ) + + parsed, err := url.Parse(raw) + if err != nil { + t.Fatalf("expected valid url, got error: %v", err) + } + if parsed.Scheme != "hysteria2" { + t.Fatalf("expected hysteria2 scheme, got %s", parsed.Scheme) + } + if parsed.User == nil { + t.Fatal("expected userinfo to be present") + } + if parsed.User.Username() != "u@ser:#&=+ pass" { + t.Fatalf("expected decoded userinfo to match source, got %q", parsed.User.Username()) + } + + q := parsed.Query() + if q.Get("obfs") != "salamander" { + t.Fatalf("expected obfs=salamander, got %q", q.Get("obfs")) + } + if q.Get("obfs-password") != "obf+s&pass=@x" { + t.Fatalf("expected decoded obfs-password, got %q", q.Get("obfs-password")) + } + if q.Get("sni") != "exa mple.com" { + t.Fatalf("expected decoded sni, got %q", q.Get("sni")) + } + if q.Get("insecure") != "0" { + t.Fatalf("expected insecure=0, got %q", q.Get("insecure")) + } + if parsed.Fragment != "my remark #1" { + t.Fatalf("expected decoded fragment, got %q", parsed.Fragment) + } + + if strings.Contains(raw, "u@ser:#&=+ pass") { + t.Fatalf("raw uri must not contain unescaped userinfo: %s", raw) + } +} + +func TestBuildHysteria2Url_MinimalConfig(t *testing.T) { + raw := buildHysteria2Url("pass", "example.com", 8443, "", "", "", "") + + parsed, err := url.Parse(raw) + if err != nil { + t.Fatalf("expected valid url, got error: %v", err) + } + if parsed.Host != "example.com:8443" { + t.Fatalf("unexpected host: %s", parsed.Host) + } + q := parsed.Query() + if q.Get("insecure") != "0" { + t.Fatalf("expected insecure=0, got %q", q.Get("insecure")) + } + if q.Get("obfs") != "" || q.Get("obfs-password") != "" || q.Get("sni") != "" { + t.Fatalf("unexpected optional query params in minimal config: %s", parsed.RawQuery) + } +} + diff --git a/docs/06-speed-limits-and-congestion.md b/docs/06-speed-limits-and-congestion.md index e1afc3c..63bfd07 100644 --- a/docs/06-speed-limits-and-congestion.md +++ b/docs/06-speed-limits-and-congestion.md @@ -40,8 +40,8 @@ ## Что фиксировать в `post-install.env` Минимум: -- `HY2_BANDWIDTH_UP_Mbps` -- `HY2_BANDWIDTH_DOWN_Mbps` +- `HY2_BANDWIDTH_UP` +- `HY2_BANDWIDTH_DOWN` - `HY2_IGNORE_CLIENT_BANDWIDTH` Дополнительно фиксируется `HY2_VERSION` как фактически установленная версия Hysteria2. diff --git a/docs/09-post-install-env.md b/docs/09-post-install-env.md index 412ec37..7043261 100644 --- a/docs/09-post-install-env.md +++ b/docs/09-post-install-env.md @@ -83,8 +83,8 @@ - `HY2_TRAFFIC_STATS_LISTEN` - `HY2_OBFS_TYPE` - `HY2_OBFS_PASSWORD` -- `HY2_BANDWIDTH_UP_Mbps` -- `HY2_BANDWIDTH_DOWN_Mbps` +- `HY2_BANDWIDTH_UP` +- `HY2_BANDWIDTH_DOWN` - `HY2_IGNORE_CLIENT_BANDWIDTH` - `HY2_CONFIG_PATH` diff --git a/docs/12-operations-and-troubleshooting.md b/docs/12-operations-and-troubleshooting.md index 6ec16e3..22e353e 100644 --- a/docs/12-operations-and-troubleshooting.md +++ b/docs/12-operations-and-troubleshooting.md @@ -88,7 +88,7 @@ sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml curl -sS -X POST \ -H 'Content-Type: application/json' \ - --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' \ + --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":0}' \ http://127.0.0.1:8080/hui/hysteria2/auth curl -sS \ diff --git a/orchestrator/src/config/env.ts b/orchestrator/src/config/env.ts index 4c8ddcc..b0828f2 100644 --- a/orchestrator/src/config/env.ts +++ b/orchestrator/src/config/env.ts @@ -143,6 +143,14 @@ function normalizeFixedHysteriaObfsType(value: string | undefined): "salamander" return "salamander"; } +function normalizeYamlSafeSecret(name: string, value: string): string { + const v = requireValue(name, value); + if (/["\n\r]/.test(v)) { + throw new Error(`${name} contains forbidden characters for HY2XS YAML profile`); + } + return v; +} + function normalizeSafeAbsolutePath(name: string, value: string, options?: { disallowTmp?: boolean }): string { const v = value.trim(); if (!v.startsWith("/")) { @@ -205,9 +213,15 @@ export function parseRuntimeEnv(content: string): RuntimeConfig { env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1" ), hysteriaTrafficStatsPort: trafficStatsPort, - hysteriaTrafficStatsSecret: valueOrGenerate(env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET), + hysteriaTrafficStatsSecret: normalizeYamlSafeSecret( + "HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET", + valueOrGenerate(env.HY2XS_HYSTERIA_TRAFFIC_STATS_SECRET) + ), hysteriaObfsType, - hysteriaObfsPassword: requireValue("HY2XS_HYSTERIA_OBFS_PASSWORD", valueOrGenerate(env.HY2XS_HYSTERIA_OBFS_PASSWORD)), + hysteriaObfsPassword: normalizeYamlSafeSecret( + "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", hysteriaIgnoreClientBandwidth: parseBool(