mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-08-25 12:27:13 +00:00
feat(reality): derive a stable per-client spiderX for shared links
The inbound's spiderX now acts as a per-client seed: exports emit sha256(seed|subKey) truncated to a 15-hex "/path", so a client's spx no longer changes on every subscription fetch (#5718) while different clients stop sharing one fingerprintable value. The form gains a regenerate button that rotates every client's path at once. The frontend link builders derive through the same function (lib/xray/spider-x.ts, @noble/hashes) keyed on subId-then-email like the Go subKey, so panel QR/copy links and subscription output agree — cross-language vector tests lock both sides byte-for-byte. streamData now tolerates malformed stored stream settings (unparseable JSON, null tls/reality settings) instead of panicking the subscription request.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -52,8 +53,8 @@ func TestGenVlessLink_TLSParamsMapped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Locks the reality field mapping of applyShareRealityParams; a configured
|
||||
// spiderX must round-trip verbatim (#5718), distinct pbk/sid catch a swap mutant.
|
||||
// Locks the reality field mapping of applyShareRealityParams; distinct pbk/sid
|
||||
// catch a swap mutant. spx is now a per-client derived value (#5718 / follow-up).
|
||||
func TestGenVlessLink_RealityParamsMapped(t *testing.T) {
|
||||
stream := `{
|
||||
"network":"tcp","security":"reality",
|
||||
@@ -73,7 +74,7 @@ func TestGenVlessLink_RealityParamsMapped(t *testing.T) {
|
||||
"pbk=PBKvalue",
|
||||
"sid=ab12cd",
|
||||
"fp=firefox",
|
||||
"spx=%2Fmypath",
|
||||
"spx=%2F",
|
||||
}
|
||||
for _, w := range wants {
|
||||
if !strings.Contains(link, w) {
|
||||
@@ -86,22 +87,92 @@ func TestGenVlessLink_RealityParamsMapped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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"}},
|
||||
"realitySettings":{
|
||||
"serverNames":["reality.example.com"],
|
||||
"shortIds":["ab12cd"],
|
||||
"settings":{"publicKey":"PBKvalue","fingerprint":"firefox"}
|
||||
}
|
||||
}`
|
||||
s := &SubService{}
|
||||
link := s.genVlessLink(shareLinkInbound(stream), "user")
|
||||
|
||||
if !strings.Contains(link, "spx=%2F") {
|
||||
t.Fatalf("reality link missing random spx fallback\n got: %s", link)
|
||||
// realityTwoClientInbound builds a reality VLESS inbound carrying two clients
|
||||
// with distinct subIds so the per-client spx derivation can be exercised.
|
||||
func realityTwoClientInbound() *model.Inbound {
|
||||
return &model.Inbound{
|
||||
Listen: "203.0.113.1",
|
||||
Port: 443,
|
||||
Protocol: model.VLESS,
|
||||
Remark: "sharelink",
|
||||
Settings: `{"clients":[
|
||||
{"id":"11111111-2222-4333-8444-555555555555","email":"alice","subId":"subAlice"},
|
||||
{"id":"22222222-3333-4444-8555-666666666666","email":"bob","subId":"subBob"}
|
||||
],"decryption":"none","encryption":"none"}`,
|
||||
StreamSettings: `{
|
||||
"network":"tcp","security":"reality",
|
||||
"tcpSettings":{"header":{"type":"none"}},
|
||||
"realitySettings":{
|
||||
"serverNames":["reality.example.com"],
|
||||
"shortIds":["ab12cd"],
|
||||
"settings":{"publicKey":"PBKvalue","fingerprint":"firefox","spiderX":"/seed"}
|
||||
}
|
||||
}`,
|
||||
}
|
||||
}
|
||||
|
||||
func spxParam(t *testing.T, link string) string {
|
||||
t.Helper()
|
||||
u, err := url.Parse(link)
|
||||
if err != nil {
|
||||
t.Fatalf("parse link %q: %v", link, err)
|
||||
}
|
||||
spx := u.Query().Get("spx")
|
||||
if spx == "" || spx[0] != '/' {
|
||||
t.Fatalf("spx missing or not /-prefixed in %q", link)
|
||||
}
|
||||
return spx
|
||||
}
|
||||
|
||||
// spx must be stable for a given client across repeated exports (the #5718
|
||||
// complaint) yet differ between clients so the value can't be fingerprinted.
|
||||
func TestGenVlessLink_RealitySpiderXPerClientStable(t *testing.T) {
|
||||
s := &SubService{}
|
||||
inbound := realityTwoClientInbound()
|
||||
|
||||
aliceFirst := spxParam(t, s.genVlessLink(inbound, "alice"))
|
||||
aliceSecond := spxParam(t, s.genVlessLink(inbound, "alice"))
|
||||
bob := spxParam(t, s.genVlessLink(inbound, "bob"))
|
||||
|
||||
if aliceFirst != aliceSecond {
|
||||
t.Fatalf("spx not stable for the same client: %q vs %q", aliceFirst, aliceSecond)
|
||||
}
|
||||
if aliceFirst == bob {
|
||||
t.Fatalf("spx identical across clients (fingerprintable): %q", aliceFirst)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeriveSpiderX(t *testing.T) {
|
||||
if got := deriveSpiderX("seed", "clientA"); got != deriveSpiderX("seed", "clientA") {
|
||||
t.Fatalf("deriveSpiderX not deterministic: %q", got)
|
||||
}
|
||||
if deriveSpiderX("seed", "clientA") == deriveSpiderX("seed", "clientB") {
|
||||
t.Fatal("deriveSpiderX must differ per client")
|
||||
}
|
||||
if deriveSpiderX("seedA", "clientA") == deriveSpiderX("seedB", "clientA") {
|
||||
t.Fatal("rotating the seed must rotate a client's spx")
|
||||
}
|
||||
got := deriveSpiderX("seed", "clientA")
|
||||
if len(got) != 16 || got[0] != '/' {
|
||||
t.Fatalf("deriveSpiderX shape = %q, want /-prefixed 15-char path", got)
|
||||
}
|
||||
if fallback := deriveSpiderX("", ""); len(fallback) != 16 || fallback[0] != '/' {
|
||||
t.Fatalf("empty-input fallback = %q, want /-prefixed path", fallback)
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-language vectors shared with frontend/src/test/spider-x.test.ts: the
|
||||
// panel builds these links in TS, so both derivations must agree byte-for-byte.
|
||||
func TestDeriveSpiderXMatchesFrontendVectors(t *testing.T) {
|
||||
vectors := map[string]struct{ seed, clientKey, want string }{
|
||||
"seed and subId": {"/seed", "subAlice", "/c252fbc3ecd3e3c"},
|
||||
"seed only": {"/", "", "/d08ed99bd9afc60"},
|
||||
}
|
||||
for name, v := range vectors {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if got := deriveSpiderX(v.seed, v.clientKey); got != v.want {
|
||||
t.Fatalf("deriveSpiderX(%q, %q) = %q, want %q (must match frontend/src/lib/xray/spider-x.ts)", v.seed, v.clientKey, got, v.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user