From 16b7c57137c52159b037331f222615396ca3217d Mon Sep 17 00:00:00 2001 From: ant-black <1016930479@qq.com> Date: Wed, 10 Jun 2026 22:55:24 +0800 Subject: [PATCH] fix: support ss uri speed tests with ipv6 endpoints --- backend/internal/proxy/parser_uri.go | 8 ++-- backend/internal/proxy/speedtest_mapping.go | 23 ++++++++++ backend/internal/proxy/speedtest_test.go | 39 ++++++++++++++++ backend/internal/proxy/utils_parse.go | 49 ++++++++++++++++++++- 4 files changed, 114 insertions(+), 5 deletions(-) diff --git a/backend/internal/proxy/parser_uri.go b/backend/internal/proxy/parser_uri.go index b1a12a34..20887f86 100644 --- a/backend/internal/proxy/parser_uri.go +++ b/backend/internal/proxy/parser_uri.go @@ -243,10 +243,10 @@ func buildOutboundSS(node string) (map[string]interface{}, error) { method = parts[0] password = parts[1] } - hostPort := strings.Split(hostPart, ":") - if len(hostPort) == 2 { - host = hostPort[0] - port, _ = strconv.Atoi(hostPort[1]) + parsedHost, parsedPort, splitErr := splitHostPortLenient(hostPart) + if splitErr == nil { + host = parsedHost + port = parsedPort } } diff --git a/backend/internal/proxy/speedtest_mapping.go b/backend/internal/proxy/speedtest_mapping.go index 215df2b2..6546fc41 100644 --- a/backend/internal/proxy/speedtest_mapping.go +++ b/backend/internal/proxy/speedtest_mapping.go @@ -19,6 +19,9 @@ func proxyConfigToMapping(src string) (map[string]any, error) { if strings.HasPrefix(l, "socks5://") { return parseStandardProxy(src, "socks5") } + if strings.HasPrefix(l, "ss://") { + return parseSSURIToMapping(src) + } if strings.Contains(l, "://") && !strings.Contains(l, "type:") { return nil, fmt.Errorf("URI 格式暂不支持: %s", l[:min(30, len(l))]) @@ -60,6 +63,26 @@ func parseStandardProxy(src string, proxyType string) (map[string]any, error) { return mapping, nil } +func parseSSURIToMapping(src string) (map[string]any, error) { + outbound, err := buildOutboundSS(src) + if err != nil { + return nil, err + } + settings, ok := outbound["settings"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("ss 节点缺少 settings") + } + mapping := map[string]any{ + "name": "speedtest-proxy", + "type": "ss", + "server": settings["address"], + "port": settings["port"], + "cipher": settings["method"], + "password": settings["password"], + } + return mapping, nil +} + func parseClashYAMLToMapping(src string) (map[string]any, error) { var payload interface{} if err := yaml.Unmarshal([]byte(src), &payload); err != nil { diff --git a/backend/internal/proxy/speedtest_test.go b/backend/internal/proxy/speedtest_test.go index 58535f44..3cd801a1 100644 --- a/backend/internal/proxy/speedtest_test.go +++ b/backend/internal/proxy/speedtest_test.go @@ -1,6 +1,7 @@ package proxy import ( + "encoding/base64" "strings" "testing" ) @@ -80,6 +81,44 @@ func TestProxyConfigToMappingClashYAML(t *testing.T) { } } +func TestProxyConfigToMappingSSURI(t *testing.T) { + t.Parallel() + + userinfo := base64.RawURLEncoding.EncodeToString([]byte("aes-128-gcm:secret")) + mapping, err := proxyConfigToMapping("ss://" + userinfo + "@ptxlv6-1.hxx.top:43001#node") + if err != nil { + t.Fatalf("proxyConfigToMapping returned error: %v", err) + } + if got := mapping["type"]; got != "ss" { + t.Fatalf("type = %v, want ss", got) + } + if got := mapping["server"]; got != "ptxlv6-1.hxx.top" { + t.Fatalf("server = %v, want ptxlv6-1.hxx.top", got) + } + if got := mapping["port"]; got != 43001 { + t.Fatalf("port = %v, want 43001", got) + } + if got := mapping["cipher"]; got != "aes-128-gcm" { + t.Fatalf("cipher = %v, want aes-128-gcm", got) + } + if got := mapping["password"]; got != "secret" { + t.Fatalf("password = %v, want secret", got) + } +} + +func TestProxyEndpointSSURIIPv6(t *testing.T) { + t.Parallel() + + raw := base64.RawURLEncoding.EncodeToString([]byte("aes-128-gcm:secret@[2001:db8::1]:43001")) + endpoint, err := proxyEndpoint("ss://" + raw) + if err != nil { + t.Fatalf("proxyEndpoint returned error: %v", err) + } + if endpoint != "[2001:db8::1]:43001" { + t.Fatalf("endpoint = %q, want [2001:db8::1]:43001", endpoint) + } +} + func TestProxyConfigToMappingUnsupportedURI(t *testing.T) { t.Parallel() diff --git a/backend/internal/proxy/utils_parse.go b/backend/internal/proxy/utils_parse.go index fcd0ced1..ab04c2e6 100644 --- a/backend/internal/proxy/utils_parse.go +++ b/backend/internal/proxy/utils_parse.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "net" "net/url" "strconv" "strings" @@ -46,10 +47,31 @@ func proxyEndpoint(src string) (string, error) { if at := strings.LastIndex(rest, "@"); at >= 0 { hostport := strings.SplitN(rest[at+1:], "?", 2)[0] hostport = strings.SplitN(hostport, "#", 2)[0] - return hostport, nil + host, port, err := splitHostPortLenient(hostport) + if err != nil { + return "", err + } + return net.JoinHostPort(host, strconv.Itoa(port)), nil } } + if strings.HasPrefix(l, "ss://") { + outbound, err := buildOutboundSS(src) + if err != nil { + return "", err + } + settings, ok := outbound["settings"].(map[string]interface{}) + if !ok { + return "", fmt.Errorf("ss 节点缺少 settings") + } + server := getMapString(settings, "address") + port := getMapInt(settings, "port") + if server == "" || port == 0 { + return "", fmt.Errorf("ss 节点信息不完整") + } + return net.JoinHostPort(server, strconv.Itoa(port)), nil + } + var payload interface{} if err := yaml.Unmarshal([]byte(src), &payload); err == nil { node := pickClashNode(payload) @@ -65,6 +87,31 @@ func proxyEndpoint(src string) (string, error) { return "", fmt.Errorf("无法解析代理地址") } +func splitHostPortLenient(hostport string) (string, int, error) { + hostport = strings.TrimSpace(hostport) + if hostport == "" { + return "", 0, fmt.Errorf("缺少代理地址") + } + if host, portText, err := net.SplitHostPort(hostport); err == nil { + port, convErr := strconv.Atoi(portText) + if convErr != nil || port <= 0 || port > 65535 { + return "", 0, fmt.Errorf("代理端口无效: %s", portText) + } + return strings.Trim(host, "[]"), port, nil + } + idx := strings.LastIndex(hostport, ":") + if idx <= 0 || idx == len(hostport)-1 { + return "", 0, fmt.Errorf("无法解析代理地址: %s", hostport) + } + host := strings.Trim(strings.TrimSpace(hostport[:idx]), "[]") + portText := strings.TrimSpace(hostport[idx+1:]) + port, err := strconv.Atoi(portText) + if host == "" || err != nil || port <= 0 || port > 65535 { + return "", 0, fmt.Errorf("无法解析代理地址: %s", hostport) + } + return host, port, nil +} + func toStringMap(input interface{}) map[string]interface{} { switch v := input.(type) { case map[string]interface{}: