feat(reality): add live REALITY target scanner with IP/CIDR discovery

Replace the static reality-targets list with a server-side TLS 1.3 probe that checks TLS 1.3 + HTTP/2 + X25519 + a trusted certificate.

- Single-domain validate auto-fills target and serverNames from the cert SAN
- Discovery scans an IP/CIDR without SNI to find new targets from their certificates, deduped and ranked by feasibility then latency, private-IP guarded via netsafe
- New endpoints scanRealityTarget and scanRealityTargets with RealityScanResult, plus openapigen and api-docs entries
- Add scanner strings to all 13 locales
- Replace deprecated AntD Alert message prop with title across the panel
This commit is contained in:
MHSanaei
2026-06-26 22:18:47 +02:00
parent 451263f1db
commit 6964d84742
36 changed files with 1489 additions and 63 deletions
+22
View File
@@ -463,6 +463,28 @@ export const EXAMPLES: Record<string, unknown> = {
"xrayState": "",
"xrayVersion": "25.10.31"
},
"RealityScanResult": {
"alpn": "h2",
"certIssuer": "Google Trust Services",
"certSubject": "cloudflare.com",
"certValid": true,
"curveID": "X25519",
"feasible": true,
"h2": true,
"host": "www.cloudflare.com",
"ip": "104.16.124.96",
"latencyMs": 180,
"notAfter": "2026-08-01T00:00:00Z",
"port": 443,
"reason": "",
"serverNames": [
""
],
"target": "www.cloudflare.com:443",
"tls13": true,
"tlsVersion": "1.3",
"x25519": true
},
"Setting": {
"id": 0,
"key": "",
+98
View File
@@ -2120,6 +2120,104 @@ export const SCHEMAS: Record<string, unknown> = {
],
"type": "object"
},
"RealityScanResult": {
"properties": {
"alpn": {
"example": "h2",
"type": "string"
},
"certIssuer": {
"example": "Google Trust Services",
"type": "string"
},
"certSubject": {
"example": "cloudflare.com",
"type": "string"
},
"certValid": {
"example": true,
"type": "boolean"
},
"curveID": {
"example": "X25519",
"type": "string"
},
"feasible": {
"example": true,
"type": "boolean"
},
"h2": {
"example": true,
"type": "boolean"
},
"host": {
"example": "www.cloudflare.com",
"type": "string"
},
"ip": {
"example": "104.16.124.96",
"type": "string"
},
"latencyMs": {
"example": 180,
"type": "integer"
},
"notAfter": {
"example": "2026-08-01T00:00:00Z",
"type": "string"
},
"port": {
"example": 443,
"type": "integer"
},
"reason": {
"type": "string"
},
"serverNames": {
"items": {
"type": "string"
},
"type": "array"
},
"target": {
"example": "www.cloudflare.com:443",
"type": "string"
},
"tls13": {
"example": true,
"type": "boolean"
},
"tlsVersion": {
"example": "1.3",
"type": "string"
},
"x25519": {
"example": true,
"type": "boolean"
}
},
"required": [
"alpn",
"certIssuer",
"certSubject",
"certValid",
"curveID",
"feasible",
"h2",
"host",
"ip",
"latencyMs",
"notAfter",
"port",
"reason",
"serverNames",
"target",
"tls13",
"tlsVersion",
"x25519"
],
"type": "object"
},
"Setting": {
"description": "Setting stores key-value configuration settings for the 3x-ui panel.",
"properties": {
+21
View File
@@ -462,6 +462,27 @@ export interface ProbeResultUI {
xrayVersion: string;
}
export interface RealityScanResult {
alpn: string;
certIssuer: string;
certSubject: string;
certValid: boolean;
curveID: string;
feasible: boolean;
h2: boolean;
host: string;
ip: string;
latencyMs: number;
notAfter: string;
port: number;
reason: string;
serverNames: string[];
target: string;
tls13: boolean;
tlsVersion: string;
x25519: boolean;
}
export interface Setting {
id: number;
key: string;
+22
View File
@@ -494,6 +494,28 @@ export const ProbeResultUISchema = z.object({
});
export type ProbeResultUI = z.infer<typeof ProbeResultUISchema>;
export const RealityScanResultSchema = z.object({
alpn: z.string(),
certIssuer: z.string(),
certSubject: z.string(),
certValid: z.boolean(),
curveID: z.string(),
feasible: z.boolean(),
h2: z.boolean(),
host: z.string(),
ip: z.string(),
latencyMs: z.number().int(),
notAfter: z.string(),
port: z.number().int(),
reason: z.string(),
serverNames: z.array(z.string()),
target: z.string(),
tls13: z.boolean(),
tlsVersion: z.string(),
x25519: z.boolean(),
});
export type RealityScanResult = z.infer<typeof RealityScanResultSchema>;
export const SettingSchema = z.object({
id: z.number().int(),
key: z.string(),
-23
View File
@@ -1,23 +0,0 @@
export interface RealityTarget {
target: string;
sni: string;
}
export const REALITY_TARGETS: readonly RealityTarget[] = [
{ target: 'www.amazon.com:443', sni: 'www.amazon.com' },
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com' },
{ target: 'www.oracle.com:443', sni: 'www.oracle.com' },
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com' },
{ target: 'www.amd.com:443', sni: 'www.amd.com' },
{ target: 'www.intel.com:443', sni: 'www.intel.com' },
{ target: 'www.sony.com:443', sni: 'www.sony.com' },
];
export function getRandomRealityTarget(): RealityTarget {
const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length);
const selected = REALITY_TARGETS[randomIndex];
return {
target: selected.target,
sni: selected.sni,
};
}
+21
View File
@@ -489,6 +489,27 @@ export const sections: readonly Section[] = [
body: 'server=cloudflare-dns.com',
response: '{\n "success": true,\n "obj": [\n "e8e2d3..."\n ]\n}',
},
{
method: 'POST',
path: '/panel/api/server/scanRealityTarget',
summary: 'Run a live TLS 1.3 probe against a candidate REALITY target and return a feasibility verdict (TLS 1.3 + h2 + X25519 + trusted certificate) plus the certificate SAN DNS names.',
params: [
{ name: 'target', in: 'body (form)', type: 'string', desc: 'Candidate target as host or host:port (default port 443), e.g. www.cloudflare.com:443.' },
],
body: 'target=www.cloudflare.com:443',
responseSchema: 'RealityScanResult',
},
{
method: 'POST',
path: '/panel/api/server/scanRealityTargets',
summary: 'Probe/discover REALITY targets and return each verdict ranked by feasibility then latency. Each comma-separated token may be a domain (validated with SNI), a bare IP, or a CIDR range (discovered without SNI by reading the certificate domain). When empty, a built-in seed list is probed.',
params: [
{ name: 'targets', in: 'body (form)', type: 'string', optional: true, desc: 'Optional comma-separated tokens: domain[:port], IP[:port], or CIDR (e.g. 104.16.0.0/24). When omitted, a built-in seed list is probed.' },
],
body: 'targets=104.16.0.0/24,www.apple.com:443',
responseSchema: 'RealityScanResult',
responseSchemaArray: true,
},
{
method: 'GET',
path: '/panel/api/server/clientIps',
@@ -81,7 +81,7 @@ export default function BulkAttachInboundsModal({
{t('pages.clients.attachToInboundsDesc', { count })}
</Typography.Paragraph>
{targetOptions.length === 0 ? (
<Alert type="info" showIcon message={t('pages.clients.attachToInboundsNoTargets')} />
<Alert type="info" showIcon title={t('pages.clients.attachToInboundsNoTargets')} />
) : (
<>
<SelectAllClearButtons
@@ -81,7 +81,7 @@ export default function BulkDetachInboundsModal({
{t('pages.clients.detachFromInboundsDesc', { count })}
</Typography.Paragraph>
{targetOptions.length === 0 ? (
<Alert type="info" showIcon message={t('pages.clients.detachFromInboundsNoTargets')} />
<Alert type="info" showIcon title={t('pages.clients.detachFromInboundsNoTargets')} />
) : (
<>
<SelectAllClearButtons
@@ -139,7 +139,7 @@ export default function GroupAddClientsModal({
</Typography.Text>
</Space>
{rows.length === 0 ? (
<Alert type="info" showIcon message={t('pages.groups.addToGroupEmpty')} />
<Alert type="info" showIcon title={t('pages.groups.addToGroupEmpty')} />
) : (
<Table<ClientRow>
size="small"
@@ -192,7 +192,7 @@ export default function AttachClientsModal({
</Space>
{targetOptions.length === 0 ? (
<Alert type="info" showIcon message={t('pages.inbounds.attachClientsNoTargets')} />
<Alert type="info" showIcon title={t('pages.inbounds.attachClientsNoTargets')} />
) : (
<Select
mode="multiple"
@@ -180,7 +180,7 @@ export default function AttachExistingClientsModal({
</Typography.Paragraph>
{noClients ? (
<Alert type="info" showIcon message={t('pages.inbounds.attachExistingNoClients')} />
<Alert type="info" showIcon title={t('pages.inbounds.attachExistingNoClients')} />
) : (
<Spin spinning={loading}>
<Space orientation="vertical" size="small" style={{ width: '100%' }}>
@@ -17,6 +17,7 @@ import {
} from 'antd';
import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils';
import type { RealityScanResult } from '@/generated/types';
import {
rawInboundToFormValues,
formValuesToWirePayload,
@@ -174,6 +175,8 @@ export default function InboundFormModal({
const [messageApi, messageContextHolder] = message.useMessage();
const [form] = Form.useForm<InboundFormValues>();
const [saving, setSaving] = useState(false);
const [scanning, setScanning] = useState(false);
const [scanResult, setScanResult] = useState<RealityScanResult | null>(null);
const {
fallbacks,
fallbackChildOptions,
@@ -241,7 +244,9 @@ export default function InboundFormModal({
clearRealityKeypair,
genMldsa65,
clearMldsa65,
randomizeRealityTarget,
scanRealityTarget,
scanRealityCandidates,
applyRealityScanResult,
randomizeShortIds,
getNewEchCert,
clearEchCert,
@@ -250,7 +255,7 @@ export default function InboundFormModal({
setCertFromPanel,
clearCertFiles,
onSecurityChange,
} = useSecurityActions({ form, setSaving, messageApi, nodeId: typeof wNodeId === 'number' ? wNodeId : null });
} = useSecurityActions({ form, setSaving, messageApi, nodeId: typeof wNodeId === 'number' ? wNodeId : null, setScanResult, setScanning });
const toggleSockopt = (on: boolean) => {
@@ -347,6 +352,7 @@ export default function InboundFormModal({
: buildAddModeValues();
form.resetFields();
form.setFieldsValue(initial);
setScanResult(null);
const initialTag = (initial.tag ?? '') as string;
autoTagRef.current = isAutoInboundTag(initialTag, {
port: initial.port ?? 0,
@@ -890,7 +896,11 @@ export default function InboundFormModal({
{security === 'reality' && (
<RealityForm
saving={saving}
randomizeRealityTarget={randomizeRealityTarget}
scanning={scanning}
scanResult={scanResult}
scanRealityTarget={scanRealityTarget}
scanRealityCandidates={scanRealityCandidates}
applyRealityScanResult={applyRealityScanResult}
randomizeShortIds={randomizeShortIds}
genRealityKeypair={genRealityKeypair}
clearRealityKeypair={clearRealityKeypair}
@@ -0,0 +1,174 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Input, Modal, Space, Table, Tag, Tooltip, Typography } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import type { RealityScanResult } from '@/generated/types';
interface RealityTargetScannerModalProps {
open: boolean;
onClose: () => void;
scanRealityCandidates: (targets?: string) => Promise<RealityScanResult[]>;
onPick: (result: RealityScanResult) => void;
}
export default function RealityTargetScannerModal({
open,
onClose,
scanRealityCandidates,
onPick,
}: RealityTargetScannerModalProps) {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [query, setQuery] = useState('');
const [results, setResults] = useState<RealityScanResult[]>([]);
const scanRef = useRef(scanRealityCandidates);
scanRef.current = scanRealityCandidates;
const runScan = useCallback(async (targets?: string) => {
setLoading(true);
try {
setResults(await scanRef.current(targets));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
if (!open) return;
setResults([]);
runScan();
}, [open, runScan]);
const columns: ColumnsType<RealityScanResult> = [
{
title: t('pages.inbounds.form.target'),
dataIndex: 'target',
key: 'target',
width: 200,
render: (target: string, row) => (
<Tooltip title={row.ip ? `${target}${row.ip}` : target}>
<div style={{ lineHeight: 1.25 }}>
<div style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{target}</div>
{row.ip ? <div style={{ color: '#999', fontSize: 12 }}>{row.ip}</div> : null}
</div>
</Tooltip>
),
},
{
title: t('pages.inbounds.form.scanStatus'),
dataIndex: 'feasible',
key: 'feasible',
width: 95,
render: (feasible: boolean, row) =>
feasible ? (
<Tag color="success">{t('pages.inbounds.form.scanFeasible')}</Tag>
) : (
<Tooltip title={row.reason}>
<Tag color="warning">{t('pages.inbounds.form.scanNotFeasible')}</Tag>
</Tooltip>
),
},
{
title: 'TLS',
dataIndex: 'tlsVersion',
key: 'tlsVersion',
width: 60,
render: (v: string) => v || '—',
},
{
title: 'ALPN',
dataIndex: 'alpn',
key: 'alpn',
width: 75,
render: (v: string) => v || '—',
},
{
title: t('pages.inbounds.form.scanCurve'),
dataIndex: 'curveID',
key: 'curveID',
width: 130,
render: (v: string) => v || '—',
},
{
title: t('pages.inbounds.form.scanCert'),
dataIndex: 'certSubject',
key: 'certSubject',
width: 160,
ellipsis: true,
render: (_: string, row) =>
row.certValid ? (
<Tooltip title={`${row.certSubject} (${row.certIssuer})`}>
<span>{row.certSubject || '—'}</span>
</Tooltip>
) : (
<Tag>{t('pages.inbounds.form.scanCertInvalid')}</Tag>
),
},
{
title: t('pages.inbounds.form.scanLatency'),
dataIndex: 'latencyMs',
key: 'latencyMs',
width: 85,
render: (v: number) => (v > 0 ? `${v} ms` : '—'),
},
{
title: '',
key: 'action',
width: 64,
render: (_, row) => (
<Button
type="link"
size="small"
onClick={() => {
onPick(row);
onClose();
}}
>
{t('pages.inbounds.form.scanUse')}
</Button>
),
},
];
return (
<Modal
open={open}
onCancel={onClose}
footer={[
<Button key="rescan" onClick={() => runScan(query.trim() || undefined)} loading={loading}>
{t('pages.inbounds.form.scanRescan')}
</Button>,
<Button key="close" type="primary" onClick={onClose}>
{t('close')}
</Button>,
]}
title={t('pages.inbounds.form.scanModalTitle')}
width={960}
>
<Space orientation="vertical" size="small" style={{ width: '100%' }}>
<Typography.Paragraph type="secondary" style={{ marginBottom: 0 }}>
{t('pages.inbounds.form.scanModalDesc')}
</Typography.Paragraph>
<Input.Search
allowClear
enterButton={t('pages.inbounds.form.scan')}
loading={loading}
value={query}
onChange={(e) => setQuery(e.target.value)}
onSearch={() => runScan(query.trim() || undefined)}
placeholder={t('pages.inbounds.form.scanDiscoverPlaceholder')}
/>
<Table<RealityScanResult>
size="small"
rowKey="target"
loading={loading}
columns={columns}
dataSource={results}
pagination={false}
scroll={{ y: 360 }}
/>
</Space>
</Modal>
);
}
@@ -1,13 +1,20 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Collapse, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { Alert, Button, Collapse, Descriptions, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { RadarChartOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { UTLS_FINGERPRINT } from '@/schemas/primitives';
import { validateRealityTarget } from '@/lib/xray/stream-wire-normalize';
import type { RealityScanResult } from '@/generated/types';
import RealityTargetScannerModal from './RealityTargetScannerModal';
interface RealityFormProps {
saving: boolean;
randomizeRealityTarget: () => void;
scanning: boolean;
scanResult: RealityScanResult | null;
scanRealityTarget: () => void;
scanRealityCandidates: (targets?: string) => Promise<RealityScanResult[]>;
applyRealityScanResult: (result: RealityScanResult) => void;
randomizeShortIds: () => void;
genRealityKeypair: () => void;
clearRealityKeypair: () => void;
@@ -17,7 +24,11 @@ interface RealityFormProps {
export default function RealityForm({
saving,
randomizeRealityTarget,
scanning,
scanResult,
scanRealityTarget,
scanRealityCandidates,
applyRealityScanResult,
randomizeShortIds,
genRealityKeypair,
clearRealityKeypair,
@@ -25,6 +36,7 @@ export default function RealityForm({
clearMldsa65,
}: RealityFormProps) {
const { t } = useTranslation();
const [scannerOpen, setScannerOpen] = useState(false);
return (
<>
<Form.Item
@@ -49,7 +61,7 @@ export default function RealityForm({
label={t('pages.inbounds.form.target')}
tooltip={t('pages.inbounds.form.realityTargetHint')}
>
<Space.Compact block>
<Space.Compact block style={{ display: 'flex' }}>
<Form.Item
name={['streamSettings', 'realitySettings', 'target']}
noStyle
@@ -62,21 +74,48 @@ export default function RealityForm({
},
]}
>
<Input style={{ width: 'calc(100% - 32px)' }} placeholder="example.com:443" />
<Input style={{ flex: 1 }} placeholder="example.com:443" />
</Form.Item>
<Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
<Button icon={<RadarChartOutlined />} loading={scanning} onClick={scanRealityTarget}>
{t('pages.inbounds.form.scan')}
</Button>
<Button icon={<SearchOutlined />} onClick={() => setScannerOpen(true)}>
{t('pages.inbounds.form.findTargets')}
</Button>
</Space.Compact>
</Form.Item>
<Form.Item label="SNI">
<Space.Compact block style={{ display: 'flex' }}>
<Form.Item
name={['streamSettings', 'realitySettings', 'serverNames']}
noStyle
>
<Select mode="tags" tokenSeparators={[',']} style={{ flex: 1 }} />
</Form.Item>
<Button icon={<ReloadOutlined />} onClick={randomizeRealityTarget} />
</Space.Compact>
{scanResult && (
<Form.Item label=" " colon={false}>
<Alert
type={scanResult.feasible ? 'success' : 'warning'}
showIcon
title={
scanResult.feasible
? t('pages.inbounds.form.scanFeasible')
: scanResult.reason || t('pages.inbounds.form.scanNotFeasible')
}
description={
<Descriptions size="small" column={1}>
<Descriptions.Item label="TLS">{scanResult.tlsVersion || '—'}</Descriptions.Item>
<Descriptions.Item label="ALPN">{scanResult.alpn || '—'}</Descriptions.Item>
<Descriptions.Item label={t('pages.inbounds.form.scanCurve')}>
{scanResult.curveID || '—'}
</Descriptions.Item>
<Descriptions.Item label={t('pages.inbounds.form.scanCert')}>
{scanResult.certValid
? `${scanResult.certSubject} (${scanResult.certIssuer})`
: t('pages.inbounds.form.scanCertInvalid')}
</Descriptions.Item>
<Descriptions.Item label={t('pages.inbounds.form.scanLatency')}>
{scanResult.latencyMs > 0 ? `${scanResult.latencyMs} ms` : '—'}
</Descriptions.Item>
</Descriptions>
}
/>
</Form.Item>
)}
<Form.Item label="SNI" name={['streamSettings', 'realitySettings', 'serverNames']}>
<Select mode="tags" tokenSeparators={[',']} style={{ width: '100%' }} />
</Form.Item>
<Form.Item
name={['streamSettings', 'realitySettings', 'maxTimediff']}
@@ -201,6 +240,12 @@ export default function RealityForm({
},
]}
/>
<RealityTargetScannerModal
open={scannerOpen}
onClose={() => setScannerOpen(false)}
scanRealityCandidates={scanRealityCandidates}
onPick={applyRealityScanResult}
/>
</>
);
}
@@ -4,10 +4,10 @@ import type { FormInstance } from 'antd';
import type { MessageInstance } from 'antd/es/message/interface';
import { HttpUtil, RandomUtil } from '@/utils';
import { getRandomRealityTarget } from '@/models/reality-targets';
import { createTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
import type { InboundFormValues } from '@/schemas/forms/inbound-form';
import type { RealityScanResult } from '@/generated/types';
interface UseSecurityActionsArgs {
form: FormInstance<InboundFormValues>;
@@ -17,13 +17,15 @@ interface UseSecurityActionsArgs {
// Panel" must read the node's own cert paths for a node-assigned inbound —
// the central panel's paths don't exist on the node. See issue #4854.
nodeId: number | null;
setScanResult: Dispatch<SetStateAction<RealityScanResult | null>>;
setScanning: Dispatch<SetStateAction<boolean>>;
}
// Server-side TLS / Reality key + certificate generation handlers for the
// inbound modal's security tab. Each talks to a /panel server endpoint and
// writes the result back into the form. Lifted out of InboundFormModal so
// the modal body stays focused on orchestration.
export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseSecurityActionsArgs) {
export function useSecurityActions({ form, setSaving, messageApi, nodeId, setScanResult, setScanning }: UseSecurityActionsArgs) {
const { t } = useTranslation();
const genRealityKeypair = async () => {
@@ -64,13 +66,55 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
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 applyRealityScanResult = (r: RealityScanResult) => {
setScanResult(r);
form.setFieldValue(['streamSettings', 'realitySettings', 'target'], r.target);
if (r.serverNames?.length) {
form.setFieldValue(['streamSettings', 'realitySettings', 'serverNames'], r.serverNames);
}
};
const scanRealityTarget = async () => {
const target = ((form.getFieldValue(['streamSettings', 'realitySettings', 'target']) as string | undefined) ?? '').trim();
if (!target) {
messageApi.warning(t('pages.inbounds.form.realityTargetRequired'));
return;
}
setScanning(true);
try {
const msg = await HttpUtil.post<RealityScanResult>(
'/panel/api/server/scanRealityTarget',
{ target },
{ silent: true },
);
if (!msg?.success || !msg.obj) {
setScanResult(null);
messageApi.error(msg?.msg || t('pages.inbounds.toasts.scanRealityTargetError'));
return;
}
const r = msg.obj;
applyRealityScanResult(r);
if (r.feasible) {
messageApi.success(t('pages.inbounds.toasts.scanRealityTargetFeasible'));
} else {
messageApi.warning(r.reason || t('pages.inbounds.toasts.scanRealityTargetNotFeasible'));
}
} finally {
setScanning(false);
}
};
const scanRealityCandidates = async (targets?: string): Promise<RealityScanResult[]> => {
const msg = await HttpUtil.post<RealityScanResult[]>(
'/panel/api/server/scanRealityTargets',
targets ? { targets } : {},
{ silent: true },
);
if (!msg?.success || !Array.isArray(msg.obj)) {
messageApi.error(msg?.msg || t('pages.inbounds.toasts.scanRealityTargetError'));
return [];
}
return msg.obj;
};
const randomizeShortIds = () => {
@@ -209,6 +253,7 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
};
const onSecurityChange = async (next: string) => {
setScanResult(null);
const current = (form.getFieldValue('streamSettings') as Record<string, unknown>) ?? {};
const cleaned: Record<string, unknown> = { ...current, security: next };
delete cleaned.tlsSettings;
@@ -218,9 +263,8 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
}
if (next === 'reality') {
const reality = RealityStreamSettingsSchema.parse({}) as Record<string, unknown>;
const tgt = getRandomRealityTarget() as { target: string; sni: string };
reality.target = tgt.target;
reality.serverNames = tgt.sni.split(',').map((s) => s.trim()).filter(Boolean);
reality.target = '';
reality.serverNames = [];
reality.shortIds = RandomUtil.randomShortIds().split(',').map((s) => s.trim()).filter(Boolean);
cleaned.realitySettings = reality;
}
@@ -244,7 +288,9 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
clearRealityKeypair,
genMldsa65,
clearMldsa65,
randomizeRealityTarget,
scanRealityTarget,
scanRealityCandidates,
applyRealityScanResult,
randomizeShortIds,
getNewEchCert,
clearEchCert,
+1 -1
View File
@@ -41,7 +41,7 @@ function UpdateChannelChoice({ onChange }: { onChange: (dev: boolean) => void })
type="info"
showIcon
style={{ marginTop: 8 }}
message={t('pages.index.devChannelWarning')}
title={t('pages.index.devChannelWarning')}
/>
)}
</div>
@@ -98,7 +98,11 @@ describe('inbound security forms', () => {
renderInForm(() => (
<RealityForm
saving={false}
randomizeRealityTarget={noop}
scanning={false}
scanResult={null}
scanRealityTarget={noop}
scanRealityCandidates={async () => []}
applyRealityScanResult={noop}
randomizeShortIds={noop}
genRealityKeypair={noop}
clearRealityKeypair={noop}