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) {