fix(link): sanitize numeric quicParams taken from a share link's fm= param

The fm= finalmask blob was JSON-decoded and attached to streamSettings
verbatim, both by the Go parser (outbound subscriptions) and the
frontend import. Some providers emit duration strings for the strictly
integer quicParams fields (e.g. keepAlivePeriod "10s"), and xray-core
then refuses to load the whole config at startup - one bad subscription
entry took the panel's Xray down on the next refresh. Coerce numeric
strings, convert duration strings to whole seconds, and drop values
that cannot be represented as integers; genuinely string-typed fields
(congestion, bbrProfile, brutalUp/Down, udpHop) pass through untouched.

Closes #5783
This commit is contained in:
MHSanaei
2026-07-05 20:16:57 +02:00
parent 579a9daaa0
commit 11e45e81b6
4 changed files with 149 additions and 0 deletions
+39
View File
@@ -1,6 +1,7 @@
package link
import (
"net/url"
"strings"
"testing"
)
@@ -35,6 +36,44 @@ func TestParseVlessLink(t *testing.T) {
}
}
func TestParseVlessLink_FinalMaskQuicParamsSanitized(t *testing.T) {
fm := url.QueryEscape(`{"mask":"dtls","quicParams":{"keepAlivePeriod":"10s","maxIdleTimeout":"30","initStreamReceiveWindow":524288,"maxIncomingStreams":true,"brutalUp":"100 mbps"}}`)
res, err := ParseLink("vless://uuid@1.2.3.4:443?type=tcp&security=none&fm=" + fm + "#node1")
if err != nil {
t.Fatalf("parse vless with fm: %v", err)
}
stream, ok := res.Outbound["streamSettings"].(map[string]any)
if !ok {
t.Fatalf("missing streamSettings: %v", res.Outbound)
}
finalmask, ok := stream["finalmask"].(map[string]any)
if !ok {
t.Fatalf("missing finalmask: %v", stream)
}
if finalmask["mask"] != "dtls" {
t.Errorf("mask changed: %v", finalmask["mask"])
}
qp, ok := finalmask["quicParams"].(map[string]any)
if !ok {
t.Fatalf("missing quicParams: %v", finalmask)
}
if got := qp["keepAlivePeriod"]; got != float64(10) {
t.Errorf("keepAlivePeriod: expected 10, got %v (%T)", got, got)
}
if got := qp["maxIdleTimeout"]; got != float64(30) {
t.Errorf("maxIdleTimeout: expected 30, got %v (%T)", got, got)
}
if got := qp["initStreamReceiveWindow"]; got != float64(524288) {
t.Errorf("initStreamReceiveWindow: expected 524288, got %v (%T)", got, got)
}
if _, exists := qp["maxIncomingStreams"]; exists {
t.Errorf("maxIncomingStreams should be dropped, got %v", qp["maxIncomingStreams"])
}
if got := qp["brutalUp"]; got != "100 mbps" {
t.Errorf("brutalUp should stay a string, got %v (%T)", got, got)
}
}
func TestParseSubscriptionBody_Base64(t *testing.T) {
// base64 of the two joined links:
// vless://u@h:443?type=tcp#A\nvless://u2@h2:443?type=tcp#B