Explorar el Código

feat(frontend): OutboundFormModal.new.tsx stream tab (TCP/KCP/WS/gRPC/HTTPUpgrade)

Wire the stream sub-form into the Pattern A modal:

- newStreamSlice(network) helper bootstraps the per-network DU branch
  with Xray defaults (mtu=1350, tti=20, uplinkCapacity=5, etc.).
- streamSettings is seeded once when the protocol supports streams
  but the form has no slice yet (new outbound + protocol switch).
- onNetworkChange swaps the sub-key and preserves security when the
  new network still supports it, else snaps back to 'none'.
- Per-network sub-forms wired:
    TCP: HTTP camouflage Switch (sets header.type = 'http' / 'none')
    KCP: 6 numeric tuning fields
    WS: host + path + heartbeat
    gRPC: service name + authority + multi-mode switch
    HTTPUpgrade: host + path
    XHTTP: host + path + mode + padding bytes (advanced fields via JSON)

Security radio, TLS/Reality sub-forms, sockopt, and mux still pending.
MHSanaei hace 9 horas
padre
commit
8e9c82f56b
Se han modificado 1 ficheros con 260 adiciones y 0 borrados
  1. 260 0
      frontend/src/pages/xray/OutboundFormModal.new.tsx

+ 260 - 0
frontend/src/pages/xray/OutboundFormModal.new.tsx

@@ -31,6 +31,7 @@ import {
 } from '@/schemas/forms/outbound-form';
 import {
   DNSRuleActions,
+  MODE_OPTION,
   OutboundDomainStrategies,
   OutboundProtocols as Protocols,
   SNIFFING_OPTION,
@@ -38,6 +39,11 @@ import {
   USERS_SECURITY,
   WireguardDomainStrategy,
 } from '@/schemas/primitives';
+import {
+  canEnableReality,
+  canEnableStream,
+  canEnableTls,
+} from '@/lib/xray/protocol-capabilities';
 import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
 import { antdRule } from '@/utils/zodForm';
 import './OutboundFormModal.css';
@@ -59,6 +65,58 @@ const PROTOCOL_OPTIONS = Object.values(Protocols).map((p) => ({ value: p, label:
 const SECURITY_OPTIONS = Object.values(USERS_SECURITY).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 MODE_OPTIONS = Object.values(MODE_OPTION).map((v) => ({ value: v, label: v }));
+
+const NETWORK_OPTIONS: { value: string; label: string }[] = [
+  { value: 'tcp', label: 'TCP (RAW)' },
+  { value: 'kcp', label: 'mKCP' },
+  { value: 'ws', label: 'WebSocket' },
+  { value: 'grpc', label: 'gRPC' },
+  { value: 'httpupgrade', label: 'HTTPUpgrade' },
+  { value: 'xhttp', label: 'XHTTP' },
+];
+
+// Per-network bootstrap. Mirrors the legacy class constructors so the
+// initial state for each transport matches what xray-core expects.
+function newStreamSlice(network: string): Record<string, unknown> {
+  switch (network) {
+    case 'tcp':
+      return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
+    case 'kcp':
+      return {
+        network: 'kcp',
+        kcpSettings: {
+          mtu: 1350, tti: 20, uplinkCapacity: 5, downlinkCapacity: 20,
+          cwndMultiplier: 1, maxSendingWindow: 2097152,
+        },
+      };
+    case 'ws':
+      return {
+        network: 'ws',
+        wsSettings: { path: '/', host: '', headers: {}, heartbeatPeriod: 0 },
+      };
+    case 'grpc':
+      return {
+        network: 'grpc',
+        grpcSettings: { serviceName: '', authority: '', multiMode: false },
+      };
+    case 'httpupgrade':
+      return {
+        network: 'httpupgrade',
+        httpupgradeSettings: { path: '/', host: '', headers: {} },
+      };
+    case 'xhttp':
+      return {
+        network: 'xhttp',
+        xhttpSettings: {
+          path: '/', host: '', mode: '', headers: [],
+          xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
+        },
+      };
+    default:
+      return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
+  }
+}
 
 // Protocols whose form schema carries a flat connect target — these all
 // get the shared "server" sub-block (address + port) at the top of the
@@ -106,6 +164,19 @@ 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 streamAllowed = canEnableStream({ protocol });
+
+  // Seed streamSettings when the user picks a protocol that supports
+  // streams but the form does not yet have a stream slice (new outbound,
+  // or wire payload arrived without streamSettings).
+  useEffect(() => {
+    if (!streamAllowed) return;
+    if (network) return;
+    form.setFieldValue('streamSettings', { ...newStreamSlice('tcp'), security: 'none' });
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [streamAllowed, network]);
 
   // Switching protocol resets the settings sub-object to fresh defaults
   // so leftover fields from the previous protocol do not bleed through.
@@ -119,6 +190,22 @@ export default function OutboundFormModalNew({
     }
   }
 
+  // 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'.
+  function onNetworkChange(next: string) {
+    const currentSecurity = form.getFieldValue(['streamSettings', 'security']) ?? 'none';
+    const stillAllowed = canEnableTls({ protocol, streamSettings: { network: next, security: currentSecurity } });
+    const stillReality = canEnableReality({ protocol, streamSettings: { network: next, security: currentSecurity } });
+    const newSecurity =
+      currentSecurity === 'tls' && !stillAllowed
+        ? 'none'
+        : currentSecurity === 'reality' && !stillReality
+          ? 'none'
+          : currentSecurity;
+    form.setFieldValue('streamSettings', { ...newStreamSlice(next), security: newSecurity });
+  }
+
   const duplicateTag = useMemo(() => {
     const myTag = tag.trim();
     if (!myTag) return false;
@@ -885,6 +972,179 @@ export default function OutboundFormModalNew({
                         </Form.List>
                       </>
                     )}
+
+                    {streamAllowed && network && (
+                      <>
+                        <Form.Item
+                          label={t('transmission')}
+                          name={['streamSettings', 'network']}
+                        >
+                          <Select
+                            value={network}
+                            onChange={onNetworkChange}
+                            options={NETWORK_OPTIONS}
+                          />
+                        </Form.Item>
+
+                        {network === 'tcp' && (
+                          <Form.Item shouldUpdate noStyle>
+                            {() => {
+                              const type =
+                                form.getFieldValue([
+                                  'streamSettings',
+                                  'tcpSettings',
+                                  'header',
+                                  'type',
+                                ]) ?? 'none';
+                              return (
+                                <>
+                                  <Form.Item label={`HTTP ${t('camouflage')}`}>
+                                    <Switch
+                                      checked={type === 'http'}
+                                      onChange={(checked) =>
+                                        form.setFieldValue(
+                                          ['streamSettings', 'tcpSettings', 'header'],
+                                          checked
+                                            ? { type: 'http', request: undefined, response: undefined }
+                                            : { type: 'none' },
+                                        )
+                                      }
+                                    />
+                                  </Form.Item>
+                                </>
+                              );
+                            }}
+                          </Form.Item>
+                        )}
+
+                        {network === 'kcp' && (
+                          <>
+                            <Form.Item label="MTU" name={['streamSettings', 'kcpSettings', 'mtu']}>
+                              <InputNumber min={0} />
+                            </Form.Item>
+                            <Form.Item label="TTI (ms)" name={['streamSettings', 'kcpSettings', 'tti']}>
+                              <InputNumber min={0} />
+                            </Form.Item>
+                            <Form.Item
+                              label="Uplink (MB/s)"
+                              name={['streamSettings', 'kcpSettings', 'uplinkCapacity']}
+                            >
+                              <InputNumber min={0} />
+                            </Form.Item>
+                            <Form.Item
+                              label="Downlink (MB/s)"
+                              name={['streamSettings', 'kcpSettings', 'downlinkCapacity']}
+                            >
+                              <InputNumber min={0} />
+                            </Form.Item>
+                            <Form.Item
+                              label="CWND multiplier"
+                              name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
+                            >
+                              <InputNumber min={1} />
+                            </Form.Item>
+                            <Form.Item
+                              label="Max sending window"
+                              name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
+                            >
+                              <InputNumber min={0} />
+                            </Form.Item>
+                          </>
+                        )}
+
+                        {network === 'ws' && (
+                          <>
+                            <Form.Item label={t('host')} name={['streamSettings', 'wsSettings', 'host']}>
+                              <Input />
+                            </Form.Item>
+                            <Form.Item label={t('path')} name={['streamSettings', 'wsSettings', 'path']}>
+                              <Input />
+                            </Form.Item>
+                            <Form.Item
+                              label="Heartbeat (s)"
+                              name={['streamSettings', 'wsSettings', 'heartbeatPeriod']}
+                            >
+                              <InputNumber min={0} />
+                            </Form.Item>
+                          </>
+                        )}
+
+                        {network === 'grpc' && (
+                          <>
+                            <Form.Item
+                              label="Service name"
+                              name={['streamSettings', 'grpcSettings', 'serviceName']}
+                            >
+                              <Input />
+                            </Form.Item>
+                            <Form.Item
+                              label="Authority"
+                              name={['streamSettings', 'grpcSettings', 'authority']}
+                            >
+                              <Input />
+                            </Form.Item>
+                            <Form.Item
+                              label="Multi mode"
+                              name={['streamSettings', 'grpcSettings', 'multiMode']}
+                              valuePropName="checked"
+                            >
+                              <Switch />
+                            </Form.Item>
+                          </>
+                        )}
+
+                        {network === 'httpupgrade' && (
+                          <>
+                            <Form.Item
+                              label={t('host')}
+                              name={['streamSettings', 'httpupgradeSettings', 'host']}
+                            >
+                              <Input />
+                            </Form.Item>
+                            <Form.Item
+                              label={t('path')}
+                              name={['streamSettings', 'httpupgradeSettings', 'path']}
+                            >
+                              <Input />
+                            </Form.Item>
+                          </>
+                        )}
+
+                        {network === 'xhttp' && (
+                          <>
+                            <Form.Item
+                              label={t('host')}
+                              name={['streamSettings', 'xhttpSettings', 'host']}
+                            >
+                              <Input />
+                            </Form.Item>
+                            <Form.Item
+                              label={t('path')}
+                              name={['streamSettings', 'xhttpSettings', 'path']}
+                            >
+                              <Input />
+                            </Form.Item>
+                            <Form.Item
+                              label="Mode"
+                              name={['streamSettings', 'xhttpSettings', 'mode']}
+                            >
+                              <Select options={MODE_OPTIONS} />
+                            </Form.Item>
+                            <Form.Item
+                              label="Padding Bytes"
+                              name={['streamSettings', 'xhttpSettings', 'xPaddingBytes']}
+                            >
+                              <Input />
+                            </Form.Item>
+                            <div style={{ marginTop: 4, opacity: 0.6, fontStyle: 'italic' }}>
+                              XHTTP advanced fields (XMUX, sequence/session placement,
+                              padding obfs) are still being migrated — edit them via
+                              the JSON tab.
+                            </div>
+                          </>
+                        )}
+                      </>
+                    )}
                   </>
                 ),
               },