mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 20:54:14 +00:00
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:
parent
8e9c82f56b
commit
bfc9c12c05
1 changed files with 151 additions and 7 deletions
|
|
@ -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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue