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:
MHSanaei
2026-07-02 12:53:08 +02:00
parent 64c306037f
commit c8ef1b1f68
28 changed files with 287 additions and 67 deletions
+25 -7
View File
@@ -683,7 +683,7 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
case "tls":
applyShareTLSParams(stream, params)
case "reality":
applyShareRealityParams(stream, params)
applyShareRealityParams(stream, params, subKey(clients[clientIndex]))
default:
params["security"] = "none"
}
@@ -734,7 +734,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
case "tls":
applyShareTLSParams(stream, params)
case "reality":
applyShareRealityParams(stream, params)
applyShareRealityParams(stream, params, subKey(clients[clientIndex]))
if streamNetwork == "tcp" && len(clients[clientIndex].Flow) > 0 {
params["flow"] = clients[clientIndex].Flow
}
@@ -1330,7 +1330,7 @@ func hysteriaPinHex(pin string) string {
return pin
}
func applyShareRealityParams(stream map[string]any, params map[string]string) {
func applyShareRealityParams(stream map[string]any, params map[string]string, clientKey string) {
params["security"] = "reality"
realitySetting, _ := stream["realitySettings"].(map[string]any)
realitySettings, _ := searchKey(realitySetting, "settings")
@@ -1356,15 +1356,33 @@ func applyShareRealityParams(stream map[string]any, params map[string]string) {
params["pqv"] = pqv
}
}
params["spx"] = "/" + random.Seq(15)
seed := ""
if spxValue, ok := searchKey(realitySettings, "spiderX"); ok {
if spx, ok := spxValue.(string); ok && len(spx) > 0 {
params["spx"] = spx
}
seed, _ = spxValue.(string)
}
params["spx"] = deriveSpiderX(seed, clientKey)
}
}
// subKey returns a stable per-client identity for deterministic derivations,
// preferring the subscription id and falling back to the (unique) email.
func subKey(c model.Client) string {
if c.SubID != "" {
return c.SubID
}
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.
func deriveSpiderX(seed, clientKey string) string {
if seed == "" && clientKey == "" {
return "/" + random.Seq(15)
}
sum := sha256.Sum256([]byte(seed + "|" + clientKey))
return "/" + hex.EncodeToString(sum[:])[:15]
}
func buildVmessLink(obj map[string]any) string {
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)