fix(link): preserve Shadowsocks TLS query params on import (#6467)

* fix(link): preserve Shadowsocks TLS query params on import

Mirror trojan/vless stream parsing so Xray-native type/security/sni/alpn/fp
query params on ss:// links survive into streamSettings on both Go and TS importers.

Fixes #6094

* fix(link): drop extra blank line so oxfmt passes

---------

Co-authored-by: mrchatam <287639636+mrchatam@users.noreply.github.com>
This commit is contained in:
mrchatam
2026-09-12 13:22:38 +03:30
committed by GitHub
parent bdd351bd15
commit 503b5df4b9
4 changed files with 122 additions and 35 deletions
+44 -34
View File
@@ -337,16 +337,23 @@ func parseShadowsocks(link string) (*ParseResult, error) {
// Two shapes:
// ss://base64(method:pass)@host:port#remark
// ss://base64(method:pass@host:port)#remark
// Query may carry Xray-native stream params (type/security/sni/alpn/fp)
// emitted by genShadowsocksLink — preserve them like trojan/vless.
remark := ""
if i := strings.Index(link, "#"); i >= 0 {
remark, _ = url.QueryUnescape(link[i+1:])
link = link[:i]
}
rawQuery := ""
if i := strings.Index(link, "?"); i >= 0 {
rawQuery = link[i+1:]
link = link[:i]
}
params, _ := url.ParseQuery(rawQuery)
core := strings.TrimPrefix(link, "ss://")
at := strings.Index(core, "@")
var host, method, pass string
var port int
if at >= 0 {
// modern
userB64 := core[:at]
@@ -364,46 +371,48 @@ func parseShadowsocks(link string) (*ParseResult, error) {
if colon < 0 {
return nil, fmt.Errorf("bad ss host:port")
}
host := hp[:colon]
port, err := strconv.Atoi(hp[colon+1:])
host = hp[:colon]
port, err = strconv.Atoi(hp[colon+1:])
if err != nil {
return nil, fmt.Errorf("bad ss port %q: %w", hp[colon+1:], err)
}
method, pass := splitMethodPass(userInfo)
identity := "ss:" + method + ":" + pass + "@" + host + ":" + strconv.Itoa(port)
ob := Outbound{
"protocol": "shadowsocks",
"tag": remark,
"settings": map[string]any{
"servers": []any{
map[string]any{"address": host, "port": port, "password": pass, "method": method},
},
},
method, pass = splitMethodPass(userInfo)
} else {
// legacy: whole thing b64
dec, err := base64DecodeFlexible(core)
if err != nil {
return nil, err
}
return &ParseResult{Outbound: ob, Identity: identity}, nil
at = strings.Index(dec, "@")
if at < 0 {
return nil, fmt.Errorf("bad legacy ss")
}
userInfo := dec[:at]
hp := dec[at+1:]
colon := strings.LastIndex(hp, ":")
if colon < 0 {
return nil, fmt.Errorf("bad legacy ss hp")
}
host = hp[:colon]
port, err = strconv.Atoi(hp[colon+1:])
if err != nil {
return nil, fmt.Errorf("bad legacy ss port %q: %w", hp[colon+1:], err)
}
method, pass = splitMethodPass(userInfo)
}
// legacy: whole thing b64
dec, err := base64DecodeFlexible(core)
if err != nil {
return nil, err
}
at = strings.Index(dec, "@")
if at < 0 {
return nil, fmt.Errorf("bad legacy ss")
}
userInfo := dec[:at]
hp := dec[at+1:]
colon := strings.LastIndex(hp, ":")
if colon < 0 {
return nil, fmt.Errorf("bad legacy ss hp")
}
host := hp[:colon]
port, err := strconv.Atoi(hp[colon+1:])
if err != nil {
return nil, fmt.Errorf("bad legacy ss port %q: %w", hp[colon+1:], err)
}
method, pass := splitMethodPass(userInfo)
identity := "ss:" + method + ":" + pass + "@" + host + ":" + strconv.Itoa(port)
network := params.Get("type")
if network == "" {
network = "tcp"
}
security := params.Get("security")
if security == "" {
security = "none"
}
stream := buildStream(network, security)
applyTransport(stream, params)
applySecurity(stream, params)
applyFinalMask(stream, params)
ob := Outbound{
"protocol": "shadowsocks",
"tag": remark,
@@ -412,6 +421,7 @@ func parseShadowsocks(link string) (*ParseResult, error) {
map[string]any{"address": host, "port": port, "password": pass, "method": method},
},
},
"streamSettings": stream,
}
return &ParseResult{Outbound: ob, Identity: identity}, nil
}
+40
View File
@@ -416,6 +416,46 @@ func TestParseShadowsocks(t *testing.T) {
}
}
func TestParseShadowsocksTLSQueryRoundTrip(t *testing.T) {
user := base64.RawURLEncoding.EncodeToString([]byte("chacha20-ietf-poly1305:secretpass"))
link := "ss://" + user + "@example.com:443?alpn=h2%2Chttp%2F1.1&fp=firefox&security=tls&sni=example.com&type=tcp#user"
res, err := ParseLink(link)
if err != nil {
t.Fatalf("parse ss tls: %v", err)
}
srv := res.Outbound["settings"].(map[string]any)["servers"].([]any)[0].(map[string]any)
if srv["address"] != "example.com" || srv["port"] != 443 {
t.Fatalf("server = %v", srv)
}
if srv["method"] != "chacha20-ietf-poly1305" || srv["password"] != "secretpass" {
t.Fatalf("creds = %v", srv)
}
stream, ok := res.Outbound["streamSettings"].(map[string]any)
if !ok {
t.Fatalf("missing streamSettings: %v", res.Outbound)
}
if stream["network"] != "tcp" {
t.Errorf("network = %v, want tcp", stream["network"])
}
if stream["security"] != "tls" {
t.Errorf("security = %v, want tls", stream["security"])
}
tls, ok := stream["tlsSettings"].(map[string]any)
if !ok {
t.Fatalf("missing tlsSettings: %v", stream)
}
if tls["serverName"] != "example.com" {
t.Errorf("sni = %v, want example.com", tls["serverName"])
}
if tls["fingerprint"] != "firefox" {
t.Errorf("fp = %v, want firefox", tls["fingerprint"])
}
alpn, _ := tls["alpn"].([]string)
if len(alpn) != 2 || alpn[0] != "h2" || alpn[1] != "http/1.1" {
t.Errorf("alpn = %v, want [h2 http/1.1]", alpn)
}
}
func TestParseShadowsocksBadPort(t *testing.T) {
user := base64.StdEncoding.EncodeToString([]byte("aes-256-gcm:secretpass"))
cases := map[string]string{