From 8e9c82f56bd9cf8109a93aa0be8ee0be145e125f Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 12:13:29 +0200 Subject: [PATCH] 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. --- .../src/pages/xray/OutboundFormModal.new.tsx | 260 ++++++++++++++++++ 1 file changed, 260 insertions(+) diff --git a/frontend/src/pages/xray/OutboundFormModal.new.tsx b/frontend/src/pages/xray/OutboundFormModal.new.tsx index a4ab1232..369891f8 100644 --- a/frontend/src/pages/xray/OutboundFormModal.new.tsx +++ b/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 { + 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({ )} + + {streamAllowed && network && ( + <> + + + + + + + + + + + )} + + {network === 'grpc' && ( + <> + + + + + + + + + + + )} + + {network === 'httpupgrade' && ( + <> + + + + + + + + )} + + {network === 'xhttp' && ( + <> + + + + + + + + + +
+ XHTTP advanced fields (XMUX, sequence/session placement, + padding obfs) are still being migrated — edit them via + the JSON tab. +
+ + )} + + )} ), },