Selaa lähdekoodia

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.
MHSanaei 9 tuntia sitten
vanhempi
sitoutus
bfc9c12c05
1 muutettua tiedostoa jossa 151 lisäystä ja 7 poistoa
  1. 151 7
      frontend/src/pages/xray/OutboundFormModal.new.tsx

+ 151 - 7
frontend/src/pages/xray/OutboundFormModal.new.tsx

@@ -6,6 +6,7 @@ import {
   Input,
   InputNumber,
   Modal,
+  Radio,
   Select,
   Space,
   Switch,
@@ -30,6 +31,7 @@ import {
   type OutboundFormValues,
 } from '@/schemas/forms/outbound-form';
 import {
+  ALPN_OPTION,
   DNSRuleActions,
   MODE_OPTION,
   OutboundDomainStrategies,
@@ -37,12 +39,14 @@ import {
   SNIFFING_OPTION,
   TLS_FLOW_CONTROL,
   USERS_SECURITY,
+  UTLS_FINGERPRINT,
   WireguardDomainStrategy,
 } from '@/schemas/primitives';
 import {
   canEnableReality,
   canEnableStream,
   canEnableTls,
+  canEnableTlsFlow,
 } from '@/lib/xray/protocol-capabilities';
 import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
 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 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 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 }[] = [
   { value: 'tcp', label: 'TCP (RAW)' },
@@ -165,8 +171,12 @@ export default function OutboundFormModalNew({
   const tag = Form.useWatch('tag', form) ?? '';
   const protocol = (Form.useWatch('protocol', form) ?? 'vless') 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 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
   // 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,
   // wsSettings, etc.) so the DU branch matches. Preserve security if
   // the new network supports it, otherwise force back to 'none'.
@@ -369,13 +410,6 @@ export default function OutboundFormModalNew({
                         >
                           <Input />
                         </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']}>
                           <Input placeholder="optional" />
                         </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>
+                      </>
+                    )}
                   </>
                 ),
               },