links: accept unpadded and url-safe base64, legacy ss format #1281

This commit is contained in:
Alireza Ahmadi 2026-10-10 21:48:31 +02:00
parent bea9c79916
commit bbff4ea852
3 changed files with 86 additions and 3 deletions

View file

@ -1,10 +1,25 @@
package util
import "encoding/base64"
import (
"encoding/base64"
"strings"
)
func decodeBase64(str string) ([]byte, error) {
str = strings.TrimSpace(str)
var err error
for _, enc := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} {
var decoded []byte
if decoded, err = enc.DecodeString(str); err == nil {
return decoded, nil
}
}
return nil, err
}
// Function to return decoded bytes if a string is Base64 encoded
func StrOrBase64Encoded(str string) string {
decoded, err := base64.StdEncoding.DecodeString(str)
decoded, err := decodeBase64(str)
if err == nil {
return string(decoded)
}
@ -12,7 +27,7 @@ func StrOrBase64Encoded(str string) string {
}
func B64StrToByte(str string) ([]byte, error) {
return base64.StdEncoding.DecodeString(str)
return decodeBase64(str)
}
func ByteToB64Str(b []byte) string {

View file

@ -351,6 +351,21 @@ func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) {
}
func ss(u *url.URL, i int) (*map[string]interface{}, string, error) {
if u.User == nil {
decoded, err := decodeBase64(u.Host + strings.TrimSuffix(u.Path, "/"))
if err != nil {
return nil, "", common.NewError("Unsupported shadowsocks")
}
idx := strings.LastIndex(string(decoded), "@")
if idx == -1 {
return nil, "", common.NewError("Unsupported shadowsocks")
}
legacy := *u
legacy.User = url.UserPassword(splitMethod(string(decoded[:idx])))
legacy.Host = string(decoded[idx+1:])
legacy.Path = ""
u = &legacy
}
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
@ -410,6 +425,11 @@ func ss(u *url.URL, i int) (*map[string]interface{}, string, error) {
return &ss, tag, nil
}
func splitMethod(userInfo string) (string, string) {
method, password, _ := strings.Cut(userInfo, ":")
return method, password
}
func parseNaiveLink(u *url.URL, i int) (*map[string]interface{}, string, error) {
var host, portStr, username, password string
var port int

View file

@ -0,0 +1,48 @@
package util
import (
"encoding/base64"
"testing"
)
// Shadowsocks links come base64-encoded in several flavours; strict standard
// decoding rejected most of them and the node vanished from Clash output,
// while the link subscription passed the line through untouched (#1281).
func TestShadowsocksLinkFormats(t *testing.T) {
const method = "2022-blake3-aes-256-gcm"
const password = "bWFza2VkLXZhbHVlMDAwMDAwMDAwMDAwMDAwMDAwMDA=:bWFza2VkLXZhbHVlMDAwMDAwMDAwMDAwMDAwMDAwMDA="
userInfo := method + ":" + password
cases := map[string]string{
"padded standard": base64.StdEncoding.EncodeToString([]byte(userInfo)) + "@[240e:1:2::1234]:22851",
"unpadded url-safe": base64.RawURLEncoding.EncodeToString([]byte(userInfo)) + "@[240e:1:2::1234]:22851",
"plugin path": base64.RawURLEncoding.EncodeToString([]byte(userInfo)) + "@[240e:1:2::1234]:22851/?plugin=none",
"legacy whole": base64.StdEncoding.EncodeToString([]byte(userInfo + "@[240e:1:2::1234]:22851")),
"legacy unpadded": base64.RawURLEncoding.EncodeToString([]byte(userInfo + "@[240e:1:2::1234]:22851")),
}
for name, body := range cases {
t.Run(name, func(t *testing.T) {
out, tag, err := GetOutbound("ss://"+body+"#node", 0)
if err != nil {
t.Fatal(err)
}
o := *out
if tag != "node" || o["method"] != method || o["password"] != password ||
o["server"] != "240e:1:2::1234" || o["server_port"] != 22851 {
t.Errorf("got %v", o)
}
})
}
}
// vmess payloads are often unpadded too.
func TestVmessUnpaddedLink(t *testing.T) {
payload := base64.RawStdEncoding.EncodeToString([]byte(`{"v":"2","ps":"vm","add":"1.2.3.4","port":"443","id":"b831381d-6324-4d53-ad4f-8cda48b30811","net":"tcp"}`))
out, _, err := GetOutbound("vmess://"+payload, 0)
if err != nil {
t.Fatal(err)
}
if (*out)["server"] != "1.2.3.4" {
t.Errorf("got %v", *out)
}
}