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
@@ -248,6 +248,7 @@ export default function InboundFormModal({
scanRealityCandidates,
applyRealityScanResult,
randomizeShortIds,
randomizeSpiderX,
getNewEchCert,
clearEchCert,
pinFromCert,
@@ -896,6 +897,7 @@ export default function InboundFormModal({
scanRealityCandidates={scanRealityCandidates}
applyRealityScanResult={applyRealityScanResult}
randomizeShortIds={randomizeShortIds}
randomizeSpiderX={randomizeSpiderX}
genRealityKeypair={genRealityKeypair}
clearRealityKeypair={clearRealityKeypair}
genMldsa65={genMldsa65}
@@ -16,6 +16,7 @@ interface RealityFormProps {
scanRealityCandidates: (targets?: string) => Promise<RealityScanResult[]>;
applyRealityScanResult: (result: RealityScanResult) => void;
randomizeShortIds: () => void;
randomizeSpiderX: () => void;
genRealityKeypair: () => void;
clearRealityKeypair: () => void;
genMldsa65: () => void;
@@ -30,6 +31,7 @@ export default function RealityForm({
scanRealityCandidates,
applyRealityScanResult,
randomizeShortIds,
randomizeSpiderX,
genRealityKeypair,
clearRealityKeypair,
genMldsa65,
@@ -147,10 +149,18 @@ export default function RealityForm({
</Space.Compact>
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
label={t('pages.inbounds.form.spiderX')}
tooltip={t('pages.inbounds.form.spiderXHint')}
>
<Input />
<Space.Compact block style={{ display: 'flex' }}>
<Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
noStyle
>
<Input style={{ flex: 1 }} />
</Form.Item>
<Button aria-label={t('regenerate')} icon={<ReloadOutlined />} onClick={randomizeSpiderX} />
</Space.Compact>
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
@@ -124,6 +124,13 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId, setSca
);
};
const randomizeSpiderX = () => {
form.setFieldValue(
['streamSettings', 'realitySettings', 'settings', 'spiderX'],
`/${RandomUtil.randomSeq(15)}`,
);
};
const getNewEchCert = async () => {
const sni = form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']);
setSaving(true);
@@ -270,6 +277,7 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId, setSca
}
form.setFieldValue('streamSettings', cleaned);
if (next === 'reality') {
randomizeSpiderX();
try {
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
if (msg?.success) {
@@ -292,6 +300,7 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId, setSca
scanRealityCandidates,
applyRealityScanResult,
randomizeShortIds,
randomizeSpiderX,
getNewEchCert,
clearEchCert,
pinFromCert,