From 1f2e3e1447176687abc69b662ad22301e43bb3ae Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Wed, 1 Jul 2026 23:07:05 +0200 Subject: [PATCH] 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 --- internal/sub/json_service.go | 4 +- internal/sub/json_service_test.go | 43 +++++++++++++++++++ internal/sub/service.go | 5 +++ internal/sub/service_sharelink_test.go | 58 +++++++++++++++++--------- 4 files changed, 89 insertions(+), 21 deletions(-) diff --git a/internal/sub/json_service.go b/internal/sub/json_service.go index 99c36cddb..892baa6e3 100644 --- a/internal/sub/json_service.go +++ b/internal/sub/json_service.go @@ -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) diff --git a/internal/sub/json_service_test.go b/internal/sub/json_service_test.go index a2c1e66c2..f69002b5d 100644 --- a/internal/sub/json_service_test.go +++ b/internal/sub/json_service_test.go @@ -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) + } +} diff --git a/internal/sub/service.go b/internal/sub/service.go index f41dc21a3..4727a4eb1 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -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 + } + } } } diff --git a/internal/sub/service_sharelink_test.go b/internal/sub/service_sharelink_test.go index a41c5987b..8f19f3fdb 100644 --- a/internal/sub/service_sharelink_test.go +++ b/internal/sub/service_sharelink_test.go @@ -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) } }