feat(frontend): OutboundFormModal.new.tsx security tab (TLS + Reality + Flow)

- onSecurityChange cascade: swaps tlsSettings/realitySettings sub-key
  matching the DU branch, seeding the new sub-form with empty/default
  fields so the UI does not reference undefined values.

- Flow Select rendered when canEnableTlsFlow is true (VLESS + TCP +
  TLS/Reality). Moved from the basic VLESS section so it only appears
  in the relevant security context — matches the legacy modal UX.

- Security Radio (none / TLS / Reality) gated by canEnableTls and
  canEnableReality pure-function predicates from
  lib/xray/protocol-capabilities.

- TLS sub-form: 6 outbound-specific fields (SNI/uTLS/ALPN/ECH/
  verifyPeerCertByName/pinnedPeerCertSha256) matching the legacy
  TlsStreamSettings flat shape (no certificates list — outbound is
  client-side).

- Reality sub-form: 6 fields (SNI/uTLS/shortId/spiderX/publicKey/
  mldsa65Verify). publicKey + mldsa65Verify get TextAreas to handle
  the long base64 strings.
This commit is contained in:
MHSanaei 2026-05-26 12:16:54 +02:00
parent 8e9c82f56b
commit bfc9c12c05
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A

View file

@ -6,6 +6,7 @@ import {
Input, Input,
InputNumber, InputNumber,
Modal, Modal,
Radio,
Select, Select,
Space, Space,
Switch, Switch,
@ -30,6 +31,7 @@ import {
type OutboundFormValues, type OutboundFormValues,
} from '@/schemas/forms/outbound-form'; } from '@/schemas/forms/outbound-form';
import { import {
ALPN_OPTION,
DNSRuleActions, DNSRuleActions,
MODE_OPTION, MODE_OPTION,
OutboundDomainStrategies, OutboundDomainStrategies,
@ -37,12 +39,14 @@ import {
SNIFFING_OPTION, SNIFFING_OPTION,
TLS_FLOW_CONTROL, TLS_FLOW_CONTROL,
USERS_SECURITY, USERS_SECURITY,
UTLS_FINGERPRINT,
WireguardDomainStrategy, WireguardDomainStrategy,
} from '@/schemas/primitives'; } from '@/schemas/primitives';
import { import {
canEnableReality, canEnableReality,
canEnableStream, canEnableStream,
canEnableTls, canEnableTls,
canEnableTlsFlow,
} from '@/lib/xray/protocol-capabilities'; } from '@/lib/xray/protocol-capabilities';
import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks'; import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
import { antdRule } from '@/utils/zodForm'; import { antdRule } from '@/utils/zodForm';
@ -66,6 +70,8 @@ const SECURITY_OPTIONS = Object.values(USERS_SECURITY).map((v) => ({ value: v, l
const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL).map((v) => ({ value: v, label: v })); const FLOW_OPTIONS = Object.values(TLS_FLOW_CONTROL).map((v) => ({ value: v, label: v }));
const SS_METHOD_OPTIONS = SSMethodSchema.options.map((v) => ({ value: v, label: v })); const SS_METHOD_OPTIONS = SSMethodSchema.options.map((v) => ({ value: v, label: v }));
const MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v })); const MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v }));
const UTLS_OPTIONS = Object.values(UTLS_FINGERPRINT).map((v) => ({ value: v, label: v }));
const ALPN_OPTIONS = Object.values(ALPN_OPTION).map((v) => ({ value: v, label: v }));
const NETWORK_OPTIONS: { value: string; label: string }[] = [ const NETWORK_OPTIONS: { value: string; label: string }[] = [
{ value: 'tcp', label: 'TCP (RAW)' }, { value: 'tcp', label: 'TCP (RAW)' },
@ -165,8 +171,12 @@ export default function OutboundFormModalNew({
const tag = Form.useWatch('tag', form) ?? ''; const tag = Form.useWatch('tag', form) ?? '';
const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string; const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
const network = (Form.useWatch(['streamSettings', 'network'], form) ?? '') as string; const network = (Form.useWatch(['streamSettings', 'network'], form) ?? '') as string;
const security = (Form.useWatch(['streamSettings', 'security'], form) ?? 'none') as string;
const streamAllowed = canEnableStream({ protocol }); const streamAllowed = canEnableStream({ protocol });
const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
// Seed streamSettings when the user picks a protocol that supports // Seed streamSettings when the user picks a protocol that supports
// streams but the form does not yet have a stream slice (new outbound, // streams but the form does not yet have a stream slice (new outbound,
@ -190,6 +200,37 @@ export default function OutboundFormModalNew({
} }
} }
// Security change cascade: swap the security sub-key so the DU branch
// matches. Seed default field values when entering tls/reality so the
// sub-forms render without `undefined` field references.
function onSecurityChange(next: string) {
const stream = form.getFieldValue('streamSettings') ?? {};
const cleaned = { ...stream } as Record<string, unknown>;
delete cleaned.tlsSettings;
delete cleaned.realitySettings;
if (next === 'tls') {
cleaned.tlsSettings = {
serverName: '',
alpn: [],
fingerprint: '',
echConfigList: '',
verifyPeerCertByName: '',
pinnedPeerCertSha256: '',
};
} else if (next === 'reality') {
cleaned.realitySettings = {
publicKey: '',
fingerprint: 'chrome',
serverName: '',
shortId: '',
spiderX: '',
mldsa65Verify: '',
};
}
cleaned.security = next;
form.setFieldValue('streamSettings', cleaned);
}
// Network change cascade: swap the per-network sub-key (tcpSettings, // Network change cascade: swap the per-network sub-key (tcpSettings,
// wsSettings, etc.) so the DU branch matches. Preserve security if // wsSettings, etc.) so the DU branch matches. Preserve security if
// the new network supports it, otherwise force back to 'none'. // the new network supports it, otherwise force back to 'none'.
@ -369,13 +410,6 @@ export default function OutboundFormModalNew({
> >
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label="Flow" name={['settings', 'flow']}>
<Select
allowClear
placeholder={t('none')}
options={FLOW_OPTIONS}
/>
</Form.Item>
<Form.Item label="Reverse tag" name={['settings', 'reverseTag']}> <Form.Item label="Reverse tag" name={['settings', 'reverseTag']}>
<Input placeholder="optional" /> <Input placeholder="optional" />
</Form.Item> </Form.Item>
@ -1145,6 +1179,116 @@ export default function OutboundFormModalNew({
)} )}
</> </>
)} )}
{tlsFlowAllowed && (
<Form.Item label="Flow" name={['settings', 'flow']}>
<Select
allowClear
placeholder={t('none')}
options={FLOW_OPTIONS}
/>
</Form.Item>
)}
{streamAllowed && network && (
<Form.Item label={t('security')}>
<Radio.Group
value={security}
buttonStyle="solid"
onChange={(e) => onSecurityChange(e.target.value as string)}
>
<Radio.Button value="none">{t('none')}</Radio.Button>
{tlsAllowed && <Radio.Button value="tls">TLS</Radio.Button>}
{realityAllowed && <Radio.Button value="reality">Reality</Radio.Button>}
</Radio.Group>
</Form.Item>
)}
{security === 'tls' && tlsAllowed && (
<>
<Form.Item
label="SNI"
name={['streamSettings', 'tlsSettings', 'serverName']}
>
<Input placeholder="server name" />
</Form.Item>
<Form.Item
label="uTLS"
name={['streamSettings', 'tlsSettings', 'fingerprint']}
>
<Select
allowClear
placeholder={t('none')}
options={UTLS_OPTIONS}
/>
</Form.Item>
<Form.Item
label="ALPN"
name={['streamSettings', 'tlsSettings', 'alpn']}
>
<Select mode="multiple" options={ALPN_OPTIONS} />
</Form.Item>
<Form.Item
label="ECH"
name={['streamSettings', 'tlsSettings', 'echConfigList']}
>
<Input />
</Form.Item>
<Form.Item
label="Verify peer name"
name={['streamSettings', 'tlsSettings', 'verifyPeerCertByName']}
>
<Input placeholder="cloudflare-dns.com" />
</Form.Item>
<Form.Item
label="Pinned SHA256"
name={['streamSettings', 'tlsSettings', 'pinnedPeerCertSha256']}
>
<Input placeholder="base64 SHA256" />
</Form.Item>
</>
)}
{security === 'reality' && realityAllowed && (
<>
<Form.Item
label="SNI"
name={['streamSettings', 'realitySettings', 'serverName']}
>
<Input />
</Form.Item>
<Form.Item
label="uTLS"
name={['streamSettings', 'realitySettings', 'fingerprint']}
>
<Select options={UTLS_OPTIONS} />
</Form.Item>
<Form.Item
label="Short ID"
name={['streamSettings', 'realitySettings', 'shortId']}
>
<Input />
</Form.Item>
<Form.Item
label="SpiderX"
name={['streamSettings', 'realitySettings', 'spiderX']}
>
<Input />
</Form.Item>
<Form.Item
label={t('pages.inbounds.publicKey')}
name={['streamSettings', 'realitySettings', 'publicKey']}
>
<Input.TextArea autoSize={{ minRows: 2 }} />
</Form.Item>
<Form.Item
label="mldsa65 verify"
name={['streamSettings', 'realitySettings', 'mldsa65Verify']}
>
<Input.TextArea autoSize={{ minRows: 2 }} />
</Form.Item>
</>
)}
</> </>
), ),
}, },