feat(frontend): security tab Reality + ECH + mldsa65 controls (Pattern A)

Adds the Reality sub-form and the four API-call buttons that drive
the server-generated material:

- genRealityKeypair calls /panel/api/server/getNewX25519Cert and writes
  the result into ['streamSettings', 'realitySettings', 'privateKey']
  and the nested settings.publicKey path.
- genMldsa65 calls /panel/api/server/getNewmldsa65 for the
  post-quantum seed/verify pair.
- getNewEchCert calls /panel/api/server/getNewEchCert with the current
  serverName and writes echServerKeys + settings.echConfigList.
- randomizeRealityTarget seeds target + serverNames from the random
  reality-targets pool.
- randomizeShortIds calls RandomUtil.randomShortIds (comma-joined
  string) and splits into the schema's string[] form.

Reality fields are bound directly to schema paths — show/xver/target,
maxTimediff, min/max ClientVer, the settings.{publicKey, fingerprint,
spiderX, mldsa65Verify} nested subtree, plus the array fields
(serverNames, shortIds) rendered as Select mode="tags" since both ship
as string[] on the wire.

TLS certificates list (Form.List with the useFile DU) still pending —
that's a chunky sub-form on its own.
This commit is contained in:
MHSanaei
2026-05-26 02:36:11 +02:00
parent 534e954954
commit 8db1be8592

View File

@@ -31,6 +31,7 @@ import {
isSS2022,
} from '@/lib/xray/protocol-capabilities';
import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
import { getRandomRealityTarget } from '@/models/reality-targets';
import {
InboundFormBaseSchema,
InboundFormSchema,
@@ -128,6 +129,80 @@ export default function InboundFormModalNew({
const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
const genRealityKeypair = async () => {
setSaving(true);
try {
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
if (msg?.success) {
const obj = msg.obj as { privateKey: string; publicKey: string };
form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], obj.privateKey);
form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], obj.publicKey);
}
} finally {
setSaving(false);
}
};
const clearRealityKeypair = () => {
form.setFieldValue(['streamSettings', 'realitySettings', 'privateKey'], '');
form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'publicKey'], '');
};
const genMldsa65 = async () => {
setSaving(true);
try {
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
if (msg?.success) {
const obj = msg.obj as { seed: string; verify: string };
form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], obj.seed);
form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], obj.verify);
}
} finally {
setSaving(false);
}
};
const clearMldsa65 = () => {
form.setFieldValue(['streamSettings', 'realitySettings', 'mldsa65Seed'], '');
form.setFieldValue(['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify'], '');
};
const randomizeRealityTarget = () => {
const tgt = getRandomRealityTarget() as { target: string; sni: string };
form.setFieldValue(['streamSettings', 'realitySettings', 'target'], tgt.target);
form.setFieldValue(
['streamSettings', 'realitySettings', 'serverNames'],
tgt.sni.split(',').map((s) => s.trim()).filter(Boolean),
);
};
const randomizeShortIds = () => {
form.setFieldValue(
['streamSettings', 'realitySettings', 'shortIds'],
RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean),
);
};
const getNewEchCert = async () => {
const sni = form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']);
setSaving(true);
try {
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni });
if (msg?.success) {
const obj = msg.obj as { echServerKeys: string; echConfigList: string };
form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], obj.echServerKeys);
form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], obj.echConfigList);
}
} finally {
setSaving(false);
}
};
const clearEchCert = () => {
form.setFieldValue(['streamSettings', 'tlsSettings', 'echServerKeys'], '');
form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
};
const onSecurityChange = (next: string) => {
const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
const cleaned: Record<string, unknown> = { ...current, security: next };
@@ -1480,6 +1555,146 @@ export default function InboundFormModalNew({
>
<Switch />
</Form.Item>
<Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label="ECH key">
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'tlsSettings', 'settings', 'echConfigList']}
label="ECH config"
>
<Input />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={getNewEchCert}>
Get New ECH Cert
</Button>
<Button danger onClick={clearEchCert}>Clear</Button>
</Space>
</Form.Item>
</>
)}
{security === 'reality' && (
<>
<Form.Item
name={['streamSettings', 'realitySettings', 'show']}
label="Show"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item name={['streamSettings', 'realitySettings', 'xver']} label="Xver">
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'fingerprint']}
label="uTLS"
>
<Select>
{Object.values(UTLS_FINGERPRINT).map((fp) => (
<Select.Option key={fp} value={fp}>{fp}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'target']}
label={
<>
Target{' '}
<SyncOutlined className="random-icon" onClick={randomizeRealityTarget} />
</>
}
>
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'serverNames']}
label={
<>
SNI{' '}
<SyncOutlined className="random-icon" onClick={randomizeRealityTarget} />
</>
}
>
<Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'maxTimediff']}
label="Max Time Diff (ms)"
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'minClientVer']}
label="Min Client Ver"
>
<Input placeholder="25.9.11" />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'maxClientVer']}
label="Max Client Ver"
>
<Input placeholder="25.9.11" />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'shortIds']}
label={
<>
Short IDs{' '}
<SyncOutlined className="random-icon" onClick={randomizeShortIds} />
</>
}
>
<Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'spiderX']}
label="SpiderX"
>
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'publicKey']}
label={t('pages.inbounds.publicKey')}
>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'privateKey']}
label={t('pages.inbounds.privatekey')}
>
<Input.TextArea autoSize={{ minRows: 1, maxRows: 4 }} />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={genRealityKeypair}>
Get New Cert
</Button>
<Button danger onClick={clearRealityKeypair}>Clear</Button>
</Space>
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'mldsa65Seed']}
label="mldsa65 Seed"
>
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'settings', 'mldsa65Verify']}
label="mldsa65 Verify"
>
<Input.TextArea autoSize={{ minRows: 2, maxRows: 6 }} />
</Form.Item>
<Form.Item label=" ">
<Space>
<Button type="primary" loading={saving} onClick={genMldsa65}>
Get New Seed
</Button>
<Button danger onClick={clearMldsa65}>Clear</Button>
</Space>
</Form.Item>
</>
)}
</>