From 93847dd1069e5ad4116db3893a7cc436fb59c6f6 Mon Sep 17 00:00:00 2001 From: Farhan Zare Date: Fri, 2 Oct 2026 18:31:52 -0400 Subject: [PATCH] fix(sub): keep the spider settings in a reality spiderX seed's query (#6694) * fix(sub): keep the spider settings in a reality spiderX seed's query xray's REALITY client reads p, c, t, i and r from the spiderX query as its spider's own settings (padding, concurrency, times, interval, return). deriveSpiderX hashes the whole seed into a bare /path per client, so any query set on the inbound was dropped from every share link and JSON subscription, and the spider always ran with defaults. Keep the seed's query after the derived path. The hash input is unchanged, so every existing client's spx stays the same; only seeds that carry a query gain it. The frontend mirror and the cross-language vectors are updated together. * docs(sub): keep the deriveSpiderX comments within two lines Review feedback on #6694: the added lines pushed both doc blocks past the two-line cap. --- frontend/src/lib/xray/spider-x.ts | 10 ++++++---- frontend/src/test/spider-x.test.ts | 6 ++++++ internal/sub/service.go | 10 +++++++--- internal/sub/service_sharelink_test.go | 21 +++++++++++++++++++++ 4 files changed, 40 insertions(+), 7 deletions(-) diff --git a/frontend/src/lib/xray/spider-x.ts b/frontend/src/lib/xray/spider-x.ts index b70003abc..a920b88c4 100644 --- a/frontend/src/lib/xray/spider-x.ts +++ b/frontend/src/lib/xray/spider-x.ts @@ -1,10 +1,12 @@ import { sha256 } from '@noble/hashes/sha2.js'; import { bytesToHex, utf8ToBytes } from '@noble/hashes/utils.js'; -// Mirrors deriveSpiderX in internal/sub/service.go byte-for-byte so panel -// links and subscription links agree; returns '' when there is no seed and -// no client key (the caller then omits spx, as the legacy builder did). +// Mirrors deriveSpiderX in internal/sub/service.go byte-for-byte, seed query included (#6693); +// '' with neither seed nor client key, so the caller omits spx as the legacy builder did. export function deriveSpiderX(seed: string, clientKey: string): string { if (!seed && !clientKey) return ''; - return `/${bytesToHex(sha256(utf8ToBytes(`${seed}|${clientKey}`))).slice(0, 15)}`; + const path = `/${bytesToHex(sha256(utf8ToBytes(`${seed}|${clientKey}`))).slice(0, 15)}`; + const at = seed.indexOf('?'); + const query = at === -1 ? '' : seed.slice(at + 1); + return query ? `${path}?${query}` : path; } diff --git a/frontend/src/test/spider-x.test.ts b/frontend/src/test/spider-x.test.ts index bf405ef06..1155b1dff 100644 --- a/frontend/src/test/spider-x.test.ts +++ b/frontend/src/test/spider-x.test.ts @@ -9,6 +9,12 @@ describe('deriveSpiderX', () => { it('matches the Go deriveSpiderX vectors', () => { expect(deriveSpiderX('/seed', 'subAlice')).toBe('/c252fbc3ecd3e3c'); expect(deriveSpiderX('/', '')).toBe('/d08ed99bd9afc60'); + expect(deriveSpiderX('/seed?p=40-400&r=500-2000', 'subAlice')).toBe( + '/09dd00b3f8c01f5?p=40-400&r=500-2000', + ); + expect(deriveSpiderX('/?p=40-400&c=1-4&t=1-3&i=1500-6000&r=500-2000', '')).toBe( + '/ac2cb268d22908e?p=40-400&c=1-4&t=1-3&i=1500-6000&r=500-2000', + ); }); it('is stable per client, distinct across clients, and rotates with the seed', () => { diff --git a/internal/sub/service.go b/internal/sub/service.go index d1f832ee6..a407a50c8 100644 --- a/internal/sub/service.go +++ b/internal/sub/service.go @@ -2016,14 +2016,18 @@ func subKey(c model.Client) string { return c.Email } -// deriveSpiderX maps the inbound's spiderX seed plus a stable client key to a -// deterministic per-client "/path"; frontend/src/lib/xray/spider-x.ts mirrors it. +// deriveSpiderX maps the seed and a stable client key to a per-client "/path" plus the seed's +// query, where xray reads its spider settings (#6693); frontend/src/lib/xray/spider-x.ts mirrors it. func deriveSpiderX(seed, clientKey string) string { if seed == "" && clientKey == "" { return "/" + random.Seq(15) } sum := sha256.Sum256([]byte(seed + "|" + clientKey)) - return "/" + hex.EncodeToString(sum[:])[:15] + path := "/" + hex.EncodeToString(sum[:])[:15] + if _, query, _ := strings.Cut(seed, "?"); query != "" { + return path + "?" + query + } + return path } func buildVmessLink(obj map[string]any) string { diff --git a/internal/sub/service_sharelink_test.go b/internal/sub/service_sharelink_test.go index 7da4367fb..de2478094 100644 --- a/internal/sub/service_sharelink_test.go +++ b/internal/sub/service_sharelink_test.go @@ -143,6 +143,23 @@ func TestGenVlessLink_RealitySpiderXPerClientStable(t *testing.T) { } } +// A seed's spider settings (p, c, t, i, r) ride along in the share link's spx, +// escaped so they stay inside that one parameter. +func TestGenVlessLink_RealitySpiderXKeepsSpiderSettings(t *testing.T) { + s := &SubService{} + inbound := realityTwoClientInbound() + inbound.StreamSettings = strings.Replace(inbound.StreamSettings, `"spiderX":"/seed"`, `"spiderX":"/seed?p=40-400&r=500-2000"`, 1) + + link := s.genVlessLink(inbound, "alice") + if got, want := spxParam(t, link), "/09dd00b3f8c01f5?p=40-400&r=500-2000"; got != want { + t.Fatalf("spx = %q, want %q", got, want) + } + u, _ := url.Parse(link) + if u.Query().Get("p") != "" || u.Query().Get("r") != "" { + t.Fatalf("spider settings leaked out of spx into the link's own query: %q", link) + } +} + func TestDeriveSpiderX(t *testing.T) { if got := deriveSpiderX("seed", "clientA"); got != deriveSpiderX("seed", "clientA") { t.Fatalf("deriveSpiderX not deterministic: %q", got) @@ -168,6 +185,10 @@ func TestDeriveSpiderXMatchesFrontendVectors(t *testing.T) { vectors := map[string]struct{ seed, clientKey, want string }{ "seed and subId": {"/seed", "subAlice", "/c252fbc3ecd3e3c"}, "seed only": {"/", "", "/d08ed99bd9afc60"}, + // xray reads p, c, t, i and r from the spiderX query as the spider's + // own settings, so the seed's query must survive the derivation. + "seed with spider settings": {"/seed?p=40-400&r=500-2000", "subAlice", "/09dd00b3f8c01f5?p=40-400&r=500-2000"}, + "spider settings only": {"/?p=40-400&c=1-4&t=1-3&i=1500-6000&r=500-2000", "", "/ac2cb268d22908e?p=40-400&c=1-4&t=1-3&i=1500-6000&r=500-2000"}, } for name, v := range vectors { t.Run(name, func(t *testing.T) {