From 4180899f152951c13243753a0a4c92d476d64ea0 Mon Sep 17 00:00:00 2001 From: Jay Hemnani <193022578+jayhemnani9910@users.noreply.github.com> Date: Mon, 2 Nov 2026 19:43:54 +0530 Subject: [PATCH] fix(link): decode share link remarks once (#6977) url.Parse already unescapes the fragment, and decodeHash unescaped it again, so a + in a vless, trojan, hysteria2 or wireguard remark became a space and an escaped % was decoded twice. These parsers now pass the escaped fragment, decoding it once like the shadowsocks parser and the frontend. --- internal/util/link/outbound.go | 8 +++--- internal/util/link/outbound_helpers_test.go | 31 +++++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) 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) + } + } + } +}