Довёл до прода интеграцию Hysteria2: безопасный URI, тесты, актуализация доков и hardening env
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"hy2xs-admin/model/bo"
|
"hy2xs-admin/model/bo"
|
||||||
"hy2xs-admin/model/constant"
|
"hy2xs-admin/model/constant"
|
||||||
"hy2xs-admin/proxy"
|
"hy2xs-admin/proxy"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -255,34 +256,54 @@ func Hysteria2Url(accountId int64) (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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)
|
hysteria2ConfigRemark, err := dao.GetConfig("key = ?", constant.Hysteria2ConfigRemark)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@
|
|||||||
## Что фиксировать в `post-install.env`
|
## Что фиксировать в `post-install.env`
|
||||||
|
|
||||||
Минимум:
|
Минимум:
|
||||||
- `HY2_BANDWIDTH_UP_Mbps`
|
- `HY2_BANDWIDTH_UP`
|
||||||
- `HY2_BANDWIDTH_DOWN_Mbps`
|
- `HY2_BANDWIDTH_DOWN`
|
||||||
- `HY2_IGNORE_CLIENT_BANDWIDTH`
|
- `HY2_IGNORE_CLIENT_BANDWIDTH`
|
||||||
|
|
||||||
Дополнительно фиксируется `HY2_VERSION` как фактически установленная версия Hysteria2.
|
Дополнительно фиксируется `HY2_VERSION` как фактически установленная версия Hysteria2.
|
||||||
|
|||||||
@@ -83,8 +83,8 @@
|
|||||||
- `HY2_TRAFFIC_STATS_LISTEN`
|
- `HY2_TRAFFIC_STATS_LISTEN`
|
||||||
- `HY2_OBFS_TYPE`
|
- `HY2_OBFS_TYPE`
|
||||||
- `HY2_OBFS_PASSWORD`
|
- `HY2_OBFS_PASSWORD`
|
||||||
- `HY2_BANDWIDTH_UP_Mbps`
|
- `HY2_BANDWIDTH_UP`
|
||||||
- `HY2_BANDWIDTH_DOWN_Mbps`
|
- `HY2_BANDWIDTH_DOWN`
|
||||||
- `HY2_IGNORE_CLIENT_BANDWIDTH`
|
- `HY2_IGNORE_CLIENT_BANDWIDTH`
|
||||||
- `HY2_CONFIG_PATH`
|
- `HY2_CONFIG_PATH`
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ sudo -u hy2xs-admin test -r /etc/hysteria/config.yaml
|
|||||||
|
|
||||||
curl -sS -X POST \
|
curl -sS -X POST \
|
||||||
-H 'Content-Type: application/json' \
|
-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
|
http://127.0.0.1:8080/hui/hysteria2/auth
|
||||||
|
|
||||||
curl -sS \
|
curl -sS \
|
||||||
|
|||||||
@@ -143,6 +143,14 @@ function normalizeFixedHysteriaObfsType(value: string | undefined): "salamander"
|
|||||||
return "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 {
|
function normalizeSafeAbsolutePath(name: string, value: string, options?: { disallowTmp?: boolean }): string {
|
||||||
const v = value.trim();
|
const v = value.trim();
|
||||||
if (!v.startsWith("/")) {
|
if (!v.startsWith("/")) {
|
||||||
@@ -205,9 +213,15 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
|
|||||||
env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1"
|
env.HY2XS_HYSTERIA_TRAFFIC_STATS_HOST || "127.0.0.1"
|
||||||
),
|
),
|
||||||
hysteriaTrafficStatsPort: trafficStatsPort,
|
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,
|
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",
|
hysteriaBandwidthUp: env.HY2XS_HYSTERIA_BANDWIDTH_UP || "50 mbps",
|
||||||
hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps",
|
hysteriaBandwidthDown: env.HY2XS_HYSTERIA_BANDWIDTH_DOWN || "50 mbps",
|
||||||
hysteriaIgnoreClientBandwidth: parseBool(
|
hysteriaIgnoreClientBandwidth: parseBool(
|
||||||
|
|||||||
Reference in New Issue
Block a user