feat(frontend): stream tab skeleton with TCP + KCP (Pattern A)

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.
This commit is contained in:
MHSanaei
2026-05-26 02:22:22 +02:00
parent b1ccf915db
commit 985e647d6e

View File

@@ -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<string, unknown>) ?? {};
const cleaned: Record<string, unknown> = { ...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 && (
<Form.Item label="Transmission">
<Select
value={network}
style={{ width: '75%' }}
onChange={onNetworkChange}
>
<Select.Option value="tcp">TCP (RAW)</Select.Option>
<Select.Option value="kcp">mKCP</Select.Option>
<Select.Option value="ws">WebSocket</Select.Option>
<Select.Option value="grpc">gRPC</Select.Option>
<Select.Option value="httpupgrade">HTTPUpgrade</Select.Option>
<Select.Option value="xhttp">XHTTP</Select.Option>
</Select>
</Form.Item>
)}
{network === 'tcp' && (
<>
<Form.Item
name={['streamSettings', 'tcpSettings', 'acceptProxyProtocol']}
label="Proxy Protocol"
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item label={`HTTP ${t('camouflage')}`}>
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
prev.streamSettings?.tcpSettings?.header?.type
!== curr.streamSettings?.tcpSettings?.header?.type
}
>
{({ getFieldValue, setFieldValue }) => {
const headerType = getFieldValue(
['streamSettings', 'tcpSettings', 'header', 'type'],
) as string | undefined;
return (
<Switch
checked={headerType === 'http'}
onChange={(v) => {
setFieldValue(
['streamSettings', 'tcpSettings', 'header'],
v ? { type: 'http' } : { type: 'none' },
);
}}
/>
);
}}
</Form.Item>
</Form.Item>
</>
)}
{network === 'kcp' && (
<>
<Form.Item name={['streamSettings', 'kcpSettings', 'mtu']} label="MTU">
<InputNumber min={576} max={1460} />
</Form.Item>
<Form.Item name={['streamSettings', 'kcpSettings', 'tti']} label="TTI (ms)">
<InputNumber min={10} max={100} />
</Form.Item>
<Form.Item name={['streamSettings', 'kcpSettings', 'upCap']} label="Uplink (MB/s)">
<InputNumber min={0} />
</Form.Item>
<Form.Item name={['streamSettings', 'kcpSettings', 'downCap']} label="Downlink (MB/s)">
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'kcpSettings', 'cwndMultiplier']}
label="CWND Multiplier"
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
name={['streamSettings', 'kcpSettings', 'maxSendingWindow']}
label="Max Sending Window"
>
<InputNumber min={0} />
</Form.Item>
</>
)}
</>
);
const sniffingTab = (
<>
<Form.Item name={['sniffing', 'enabled']} label={t('enable')} valuePropName="checked">
@@ -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 },
]} />
</Form>