diff --git a/internal/util/link/outbound.go b/internal/util/link/outbound.go index 7980fb7ff..5a64164b5 100644 --- a/internal/util/link/outbound.go +++ b/internal/util/link/outbound.go @@ -387,7 +387,7 @@ func parseVless(link string) (*ParseResult, error) { ob := Outbound{ "protocol": "vless", - "tag": decodeHash(u.Fragment), + "tag": decodeHash(u.EscapedFragment()), "settings": map[string]any{ "address": host, "port": port, @@ -429,7 +429,7 @@ func parseTrojan(link string) (*ParseResult, error) { ob := Outbound{ "protocol": "trojan", - "tag": decodeHash(u.Fragment), + "tag": decodeHash(u.EscapedFragment()), "settings": map[string]any{ "servers": []any{ map[string]any{"address": host, "port": port, "password": pw}, @@ -634,7 +634,7 @@ func parseHysteria2(link string) (*ParseResult, error) { ob := Outbound{ "protocol": "hysteria", - "tag": decodeHash(u.Fragment), + "tag": decodeHash(u.EscapedFragment()), "settings": map[string]any{"address": host, "port": port, "version": 2}, "streamSettings": stream, } @@ -712,7 +712,7 @@ func parseWireguard(link string) (*ParseResult, error) { ob := Outbound{ "protocol": "wireguard", - "tag": decodeHash(u.Fragment), + "tag": decodeHash(u.EscapedFragment()), "settings": settings, } return &ParseResult{Outbound: ob, Identity: identity}, nil diff --git a/internal/util/link/outbound_helpers_test.go b/internal/util/link/outbound_helpers_test.go index 4ecf390c5..254c891f8 100644 --- a/internal/util/link/outbound_helpers_test.go +++ b/internal/util/link/outbound_helpers_test.go @@ -430,3 +430,34 @@ func TestParse_VmessFinalMask(t *testing.T) { t.Fatalf("finalmask.udp = %#v, want %#v", udp, want) } } + +func TestParse_RemarkDecodedOnce(t *testing.T) { + // Remarks as the subscription server writes them (encodeURIComponent). + cases := []struct { + fragment string + want string + }{ + {"VIP%2B", "VIP+"}, + {"US%2BEU%201", "US+EU 1"}, + {"a%2520b", "a%20b"}, + {"%D0%A2%D0%B5%D1%81%D1%82", "Тест"}, + } + prefixes := []string{ + "vless://uuid@1.2.3.4:443?type=tcp#", + "trojan://pw@1.2.3.4:443#", + "hysteria2://auth@1.2.3.4:443#", + "wireguard://key@1.2.3.4:51820?publickey=pub#", + "ss://YWVzLTI1Ni1nY206cHc@1.2.3.4:8388#", + } + for _, p := range prefixes { + for _, c := range cases { + res, err := ParseLink(p + c.fragment) + if err != nil { + t.Fatalf("parse %s%s: %v", p, c.fragment, err) + } + if res.Outbound["tag"] != c.want { + t.Errorf("%s%s: tag = %q, want %q", p, c.fragment, res.Outbound["tag"], c.want) + } + } + } +}