mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 03:05:02 +03:00
fix(sub): encode the link host and the VLESS id in share links
joinHostPort wrote an endpoint address into the authority of every URL share link unencoded, and genVlessLink alone skipped encodeUserinfo for the client id. Both can come from a node's sync (an externalProxy dest, an adopted host row, the client id), the finished link is never re-parsed, and the raw subscription is one link per line: a line break in either added lines of the node's choosing to the master's subscription, and an id carrying "@" moved the client's VLESS links on other inbounds to another server. Whitespace, control characters and URI delimiters in the host are now percent-encoded, which leaves host names, IP literals and zone ids unchanged, and the VLESS id is encoded as userinfo like every other protocol's credential.
This commit is contained in:
@@ -181,3 +181,53 @@ func TestBuildEndpointVmessLinks_HostFinalMask(t *testing.T) {
|
|||||||
t.Fatalf("vmess fm = %q, want the host mask", obj["fm"])
|
t.Fatalf("vmess fm = %q, want the host mask", obj["fm"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// An endpoint address is mirrored from the node as reported: whatever it holds, one
|
||||||
|
// endpoint stays one line of the subscription body, inside its own link's authority.
|
||||||
|
func TestSub_EndpointAddressCannotAddLines(t *testing.T) {
|
||||||
|
seedSubDB(t)
|
||||||
|
dest, _ := json.Marshal("::1]:443#a\nincy://routing/onadd/e30=\r\nvless://x@[::2")
|
||||||
|
seedSubInbound(t, "s1", "n", 4490, 1, fmt.Sprintf(
|
||||||
|
`{"network":"tcp","security":"none","externalProxy":[{"forceTls":"same","dest":%s,"port":443,"remark":""}]}`, dest))
|
||||||
|
links, _, _, _, err := NewSubService("").GetSubs("s1", "req.example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetSubs: %v", err)
|
||||||
|
}
|
||||||
|
want := "vless://11111111-2222-4333-8444-000000004490@" +
|
||||||
|
"[::1%5D:443%23a%0Aincy:%2F%2Frouting%2Fonadd%2Fe30=%0D%0Avless:%2F%2Fx%40%5B::2]:443" +
|
||||||
|
"?encryption=none&security=none&type=tcp#n-n%40e"
|
||||||
|
if len(links) != 1 || links[0] != want {
|
||||||
|
t.Fatalf("links = %q, want [%q]", links, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJoinHostPortEscapesNonHostCharacters(t *testing.T) {
|
||||||
|
for host, want := range map[string]string{
|
||||||
|
"cdn.example.com": "cdn.example.com:443",
|
||||||
|
"203.0.113.5": "203.0.113.5:443",
|
||||||
|
"[2001:db8::1]": "[2001:db8::1]:443",
|
||||||
|
"fe80::1%eth0": "[fe80::1%eth0]:443",
|
||||||
|
"пример.рф": "пример.рф:443",
|
||||||
|
"a b\tc": "a%20b%09c:443",
|
||||||
|
"a\u0085b\u2028c": "a%C2%85b%E2%80%A8c:443",
|
||||||
|
"h?x=1#f": "h%3Fx=1%23f:443",
|
||||||
|
"u@h/p": "u%40h%2Fp:443",
|
||||||
|
} {
|
||||||
|
if got := joinHostPort(host, 443); got != want {
|
||||||
|
t.Errorf("joinHostPort(%q) = %q, want %q", host, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The VLESS id is the userinfo of its link and is escaped like every other protocol's.
|
||||||
|
func TestGenVlessLinkEscapesID(t *testing.T) {
|
||||||
|
in := &model.Inbound{
|
||||||
|
Listen: "203.0.113.5", Port: 443, Protocol: model.VLESS, Remark: "n",
|
||||||
|
Settings: `{"clients":[{"id":"a@b#c\nd","email":"user"}],"decryption":"none"}`,
|
||||||
|
StreamSettings: `{"network":"tcp","security":"none"}`,
|
||||||
|
}
|
||||||
|
got := NewSubService("").genVlessLink(in, "user")
|
||||||
|
if !strings.HasPrefix(got, "vless://a%40b%23c%0Ad@203.0.113.5:443?") {
|
||||||
|
t.Fatalf("vless id not escaped: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+17
-3
@@ -1359,7 +1359,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
params,
|
params,
|
||||||
security,
|
security,
|
||||||
func(ep map[string]any, dest string, port int) string {
|
func(ep map[string]any, dest string, port int) string {
|
||||||
return fmt.Sprintf("vless://%s@%s", applyVlessRoute(uuid, hostVlessRoute(ep)), joinHostPort(dest, port))
|
return fmt.Sprintf("vless://%s@%s", encodeUserinfo(applyVlessRoute(uuid, hostVlessRoute(ep))), joinHostPort(dest, port))
|
||||||
},
|
},
|
||||||
func(ep map[string]any) string {
|
func(ep map[string]any) string {
|
||||||
return s.endpointRemark(inbound, email, ep, streamNetwork)
|
return s.endpointRemark(inbound, email, ep, streamNetwork)
|
||||||
@@ -1367,7 +1367,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
link := fmt.Sprintf("vless://%s@%s", uuid, joinHostPort(address, port))
|
link := fmt.Sprintf("vless://%s@%s", encodeUserinfo(uuid), joinHostPort(address, port))
|
||||||
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", streamNetwork))
|
return buildLinkWithParams(link, params, s.genRemark(inbound, email, "", streamNetwork))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1441,7 +1441,21 @@ func encodeUserinfo(s string) string {
|
|||||||
// callers don't have to normalize upstream.
|
// callers don't have to normalize upstream.
|
||||||
func joinHostPort(host string, port int) string {
|
func joinHostPort(host string, port int) string {
|
||||||
host = strings.Trim(host, "[]")
|
host = strings.Trim(host, "[]")
|
||||||
return net.JoinHostPort(host, strconv.Itoa(port))
|
return net.JoinHostPort(escapeLinkHost(host), strconv.Itoa(port))
|
||||||
|
}
|
||||||
|
|
||||||
|
// escapeLinkHost percent-encodes whitespace, control characters and URI delimiters: a
|
||||||
|
// node-reported address is never re-parsed, so a raw line break would end the link's line.
|
||||||
|
func escapeLinkHost(host string) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
for _, r := range host {
|
||||||
|
if r == ' ' || !strconv.IsPrint(r) || strings.ContainsRune("\"#/<>?@[\\]^`{|}", r) {
|
||||||
|
sb.WriteString(encodeURIComponent(string(r)))
|
||||||
|
} else {
|
||||||
|
sb.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user