mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
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.
This commit is contained in:
parent
e8721a207c
commit
8e9c82f56b
1 changed files with 260 additions and 0 deletions
|
|
@ -31,6 +31,7 @@ import {
|
||||||
} from '@/schemas/forms/outbound-form';
|
} from '@/schemas/forms/outbound-form';
|
||||||
import {
|
import {
|
||||||
DNSRuleActions,
|
DNSRuleActions,
|
||||||
|
MODE_OPTION,
|
||||||
OutboundDomainStrategies,
|
OutboundDomainStrategies,
|
||||||
OutboundProtocols as Protocols,
|
OutboundProtocols as Protocols,
|
||||||
SNIFFING_OPTION,
|
SNIFFING_OPTION,
|
||||||
|
|
@ -38,6 +39,11 @@ import {
|
||||||
USERS_SECURITY,
|
USERS_SECURITY,
|
||||||
WireguardDomainStrategy,
|
WireguardDomainStrategy,
|
||||||
} from '@/schemas/primitives';
|
} from '@/schemas/primitives';
|
||||||
|
import {
|
||||||
|
canEnableReality,
|
||||||
|
canEnableStream,
|
||||||
|
canEnableTls,
|
||||||
|
} from '@/lib/xray/protocol-capabilities';
|
||||||
import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
|
import { SSMethodSchema } from '@/schemas/protocols/inbound/shadowsocks';
|
||||||
import { antdRule } from '@/utils/zodForm';
|
import { antdRule } from '@/utils/zodForm';
|
||||||
import './OutboundFormModal.css';
|
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 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 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 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
|
// Protocols whose form schema carries a flat connect target — these all
|
||||||
// get the shared "server" sub-block (address + port) at the top of the
|
// 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 tag = Form.useWatch('tag', form) ?? '';
|
||||||
const protocol = (Form.useWatch('protocol', form) ?? 'vless') as string;
|
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
|
// Switching protocol resets the settings sub-object to fresh defaults
|
||||||
// so leftover fields from the previous protocol do not bleed through.
|
// 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 duplicateTag = useMemo(() => {
|
||||||
const myTag = tag.trim();
|
const myTag = tag.trim();
|
||||||
if (!myTag) return false;
|
if (!myTag) return false;
|
||||||
|
|
@ -885,6 +972,179 @@ export default function OutboundFormModalNew({
|
||||||
</Form.List>
|
</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue