fix(sub): use configured spiderX instead of always randomizing

applyShareRealityParams and SubJsonService.realityData generated a fresh
random spx on every export, so share links, "export all links", and JSON
subscriptions never matched a spiderX configured on the inbound and two
exports of the same client disagreed with each other. Read the value from
realitySettings.settings like pbk/fp/pqv and keep the random value only as
a fallback when none is configured.

Closes #5718
This commit is contained in:
MHSanaei
2026-07-01 23:07:05 +02:00
parent 49773c18de
commit 1f2e3e1447
4 changed files with 89 additions and 21 deletions
+3 -1
View File
@@ -331,8 +331,10 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
rltyData["mldsa65Verify"] = rltyClientSettings["mldsa65Verify"]
// Set random data
rltyData["spiderX"] = "/" + random.Seq(15)
if spx, ok := rltyClientSettings["spiderX"].(string); ok && spx != "" {
rltyData["spiderX"] = spx
}
shortIds, ok := rData["shortIds"].([]any)
if ok && len(shortIds) > 0 {
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
+43
View File
@@ -254,3 +254,46 @@ func TestSubJsonServiceGlobalMuxWhenNoXmux(t *testing.T) {
t.Fatalf("mux payload wrong: %#v", m)
}
}
func TestSubJsonServiceRealityDataUsesConfiguredSpiderX(t *testing.T) {
svc := NewSubJsonService("", "", "", nil)
stream := svc.streamData(`{
"network":"tcp","security":"reality","tcpSettings":{"header":{"type":"none"}},
"realitySettings":{
"serverNames":["reality.example.com"],
"shortIds":["ab12cd"],
"settings":{"publicKey":"PBKvalue","fingerprint":"firefox","spiderX":"/mypath"}
}
}`)
rlty, _ := stream["realitySettings"].(map[string]any)
if rlty == nil {
t.Fatal("streamData dropped realitySettings")
}
if rlty["spiderX"] != "/mypath" {
t.Fatalf("spiderX = %v, want configured /mypath (#5718)", rlty["spiderX"])
}
}
func TestSubJsonServiceRealityDataSpiderXFallsBackToRandom(t *testing.T) {
svc := NewSubJsonService("", "", "", nil)
stream := svc.streamData(`{
"network":"tcp","security":"reality","tcpSettings":{"header":{"type":"none"}},
"realitySettings":{
"serverNames":["reality.example.com"],
"shortIds":["ab12cd"],
"settings":{"publicKey":"PBKvalue","fingerprint":"firefox"}
}
}`)
rlty, _ := stream["realitySettings"].(map[string]any)
if rlty == nil {
t.Fatal("streamData dropped realitySettings")
}
spx, _ := rlty["spiderX"].(string)
if len(spx) != 16 || spx[0] != '/' {
t.Fatalf("spiderX fallback = %q, want random 16-char /-prefixed value", spx)
}
}
+5
View File
@@ -1357,6 +1357,11 @@ func applyShareRealityParams(stream map[string]any, params map[string]string) {
}
}
params["spx"] = "/" + random.Seq(15)
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
}
}
}
+38 -20
View File
@@ -52,11 +52,43 @@ func TestGenVlessLink_TLSParamsMapped(t *testing.T) {
}
}
// TestGenVlessLink_RealityParamsMapped locks the reality field mapping
// (applyShareRealityParams, service.go:1147). serverNames/shortIds are single-element
// so random.Num is deterministic (index 0); spx is random so it is asserted by prefix.
// Distinct pbk/sid values catch a pbk<->sid swap mutant.
// Locks the reality field mapping of applyShareRealityParams; a configured
// spiderX must round-trip verbatim (#5718), distinct pbk/sid catch a swap mutant.
func TestGenVlessLink_RealityParamsMapped(t *testing.T) {
stream := `{
"network":"tcp","security":"reality",
"tcpSettings":{"header":{"type":"none"}},
"realitySettings":{
"serverNames":["reality.example.com"],
"shortIds":["ab12cd"],
"settings":{"publicKey":"PBKvalue","fingerprint":"firefox","spiderX":"/mypath"}
}
}`
s := &SubService{}
link := s.genVlessLink(shareLinkInbound(stream), "user")
wants := []string{
"security=reality",
"sni=reality.example.com",
"pbk=PBKvalue",
"sid=ab12cd",
"fp=firefox",
"spx=%2Fmypath",
}
for _, w := range wants {
if !strings.Contains(link, w) {
t.Fatalf("reality link missing %q\n got: %s", w, link)
}
}
// A pbk<->sid swap must not silently pass: pbk must not carry the shortId.
if strings.Contains(link, "pbk=ab12cd") || strings.Contains(link, "sid=PBKvalue") {
t.Fatalf("reality pbk/sid mapping crossed: %s", link)
}
}
// Without a configured spiderX, spx must still fall back to a random
// "/"-prefixed value so clients always receive a plausible path.
func TestGenVlessLink_RealitySpiderXFallsBackToRandom(t *testing.T) {
stream := `{
"network":"tcp","security":"reality",
"tcpSettings":{"header":{"type":"none"}},
@@ -69,21 +101,7 @@ func TestGenVlessLink_RealityParamsMapped(t *testing.T) {
s := &SubService{}
link := s.genVlessLink(shareLinkInbound(stream), "user")
wants := []string{
"security=reality",
"sni=reality.example.com",
"pbk=PBKvalue",
"sid=ab12cd",
"fp=firefox",
"spx=%2F", // "/" + random.Seq(15), percent-encoded leading slash
}
for _, w := range wants {
if !strings.Contains(link, w) {
t.Fatalf("reality link missing %q\n got: %s", w, link)
}
}
// A pbk<->sid swap must not silently pass: pbk must not carry the shortId.
if strings.Contains(link, "pbk=ab12cd") || strings.Contains(link, "sid=PBKvalue") {
t.Fatalf("reality pbk/sid mapping crossed: %s", link)
if !strings.Contains(link, "spx=%2F") {
t.Fatalf("reality link missing random spx fallback\n got: %s", link)
}
}