From 985e647d6e171e60c65ab80f436e4c44fce41a00 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 02:22:22 +0200 Subject: [PATCH] feat(frontend): stream tab skeleton with TCP + KCP (Pattern A) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opens the stream tab on the sibling-file rewrite. Tab visibility is driven by canEnableStream from lib/xray/protocol-capabilities — same gate the legacy modal used, now schema-aware. Transmission picker (network select) is hidden for HYSTERIA since that protocol's network is implicit. onNetworkChange clears any stale per-network settings keys (tcpSettings/kcpSettings/...) and seeds an empty object for the new branch so AntD Form.Items don't read from undefined nested paths. TCP section: acceptProxyProtocol Switch (literal-true-optional on the wire — the form stores true/false but Zod's strip behavior keeps false-as-omission round-trips clean) plus an HTTP-camouflage toggle that flips header.type between 'none' and 'http'. The full HTTP camouflage request/response sub-form lands in a follow-up commit. KCP section: six numeric knobs (mtu, tti, upCap, downCap, cwndMultiplier, maxSendingWindow). WS / gRPC / HTTPUpgrade / XHTTP / external-proxy / sockopt / hysteria stream / FinalMaskForm hookup all still pending. --- .../pages/inbounds/InboundFormModal.new.tsx | 110 +++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/inbounds/InboundFormModal.new.tsx b/frontend/src/pages/inbounds/InboundFormModal.new.tsx index dc1cb908..fcb23311 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.new.tsx +++ b/frontend/src/pages/inbounds/InboundFormModal.new.tsx @@ -24,7 +24,7 @@ import { formValuesToWirePayload, } from '@/lib/xray/inbound-form-adapter'; import { createDefaultInboundSettings } from '@/lib/xray/inbound-defaults'; -import { isSS2022 } from '@/lib/xray/protocol-capabilities'; +import { canEnableStream, isSS2022 } from '@/lib/xray/protocol-capabilities'; import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks'; import { InboundFormBaseSchema, @@ -105,6 +105,8 @@ export default function InboundFormModalNew({ settings: typeof ssMethod === 'string' ? { method: ssMethod } : {}, }); const mixedUdpOn = Form.useWatch(['settings', 'udp'], form) ?? false; + const network = Form.useWatch(['streamSettings', 'network'], form) ?? ''; + const streamEnabled = canEnableStream({ protocol }); const wgSecretKey = Form.useWatch(['settings', 'secretKey'], form); const wgPubKey = typeof wgSecretKey === 'string' && wgSecretKey.length > 0 ? Wireguard.generateKeypair(wgSecretKey).publicKey @@ -744,6 +746,109 @@ export default function InboundFormModalNew({ ); + // Switching `network` swaps which per-network key (tcpSettings, wsSettings, + // grpcSettings, ...) appears on the wire. We clear the previously selected + // network's settings blob and seed a default empty object for the new one + // so AntD's Form.Items aren't pointed at undefined nested paths. + const onNetworkChange = (next: string) => { + const ALL = ['tcpSettings', 'kcpSettings', 'wsSettings', 'grpcSettings', 'httpupgradeSettings', 'xhttpSettings']; + const current = (form.getFieldValue('streamSettings') as Record) ?? {}; + const cleaned: Record = { ...current, network: next }; + for (const k of ALL) { + if (k !== `${next}Settings`) delete cleaned[k]; + } + cleaned[`${next}Settings`] = {}; + form.setFieldValue('streamSettings', cleaned); + }; + + const streamTab = ( + <> + {protocol !== Protocols.HYSTERIA && ( + + + + )} + + {network === 'tcp' && ( + <> + + + + + + prev.streamSettings?.tcpSettings?.header?.type + !== curr.streamSettings?.tcpSettings?.header?.type + } + > + {({ getFieldValue, setFieldValue }) => { + const headerType = getFieldValue( + ['streamSettings', 'tcpSettings', 'header', 'type'], + ) as string | undefined; + return ( + { + setFieldValue( + ['streamSettings', 'tcpSettings', 'header'], + v ? { type: 'http' } : { type: 'none' }, + ); + }} + /> + ); + }} + + + + )} + + {network === 'kcp' && ( + <> + + + + + + + + + + + + + + + + + + + + )} + + ); + const sniffingTab = ( <> @@ -839,6 +944,9 @@ export default function InboundFormModalNew({ ] as string[]).includes(protocol) ? [{ key: 'protocol', label: t('pages.inbounds.protocol'), children: protocolTab }] : []), + ...(streamEnabled + ? [{ key: 'stream', label: t('pages.inbounds.streamTab'), children: streamTab }] + : []), { key: 'sniffing', label: t('pages.inbounds.sniffingTab'), children: sniffingTab }, ]} />