refactor(frontend): extract inbound sockopt + external-proxy blocks

Move the inbound Sockopt (~250 lines) and External Proxy stream blocks
out of InboundFormModal into presentational components under
inbounds/form/transport/, mirroring the outbound extraction. Each takes
its toggle handler (toggleSockopt / toggleExternalProxy) as a prop and
keeps its render-prop getFieldValue gate. InboundFormModal drops from
1708 to 1332 lines.

Extend inbound-form-blocks.test.tsx with isolated render-snapshot
coverage for both (SockoptForm seeded enabled + happyEyeballs;
ExternalProxyForm seeded with one TLS entry). No behavior change.
This commit is contained in:
MHSanaei 2026-05-30 20:43:40 +02:00
parent 2aec803181
commit 7739c3367d
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
6 changed files with 492 additions and 385 deletions

View file

@ -22,7 +22,6 @@ import {
ArrowDownOutlined, ArrowDownOutlined,
ArrowUpOutlined, ArrowUpOutlined,
DeleteOutlined, DeleteOutlined,
MinusOutlined,
PlusOutlined, PlusOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
@ -47,18 +46,10 @@ import {
} from '@/schemas/forms/inbound-form'; } from '@/schemas/forms/inbound-form';
import { antdRule } from '@/utils/zodForm'; import { antdRule } from '@/utils/zodForm';
import { import {
ALPN_OPTION,
Address_Port_Strategy,
DOMAIN_STRATEGY_OPTION,
Protocols, Protocols,
SNIFFING_OPTION, SNIFFING_OPTION,
TCP_CONGESTION_OPTION,
UTLS_FINGERPRINT,
} from '@/schemas/primitives'; } from '@/schemas/primitives';
import { import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
HappyEyeballsSchema,
SockoptStreamSettingsSchema,
} from '@/schemas/protocols/stream/sockopt';
import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria'; import { HysteriaStreamSettingsSchema } from '@/schemas/protocols/stream/hysteria';
import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls'; import { TlsStreamSettingsSchema } from '@/schemas/protocols/security/tls';
import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality'; import { RealityStreamSettingsSchema } from '@/schemas/protocols/security/reality';
@ -86,10 +77,12 @@ import {
WireguardFields, WireguardFields,
} from './protocols'; } from './protocols';
import { import {
ExternalProxyForm,
GrpcForm, GrpcForm,
HttpUpgradeForm, HttpUpgradeForm,
KcpForm, KcpForm,
RawForm, RawForm,
SockoptForm,
WsForm, WsForm,
XhttpForm, XhttpForm,
} from './transport'; } from './transport';
@ -1058,378 +1051,9 @@ export default function InboundFormModal({
{network === 'kcp' && <KcpForm />} {network === 'kcp' && <KcpForm />}
<Form.Item <ExternalProxyForm toggleExternalProxy={toggleExternalProxy} />
noStyle
shouldUpdate={(prev, curr) => {
const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0);
}}
>
{({ getFieldValue }) => {
const arr = getFieldValue(['streamSettings', 'externalProxy']);
const on = Array.isArray(arr) && arr.length > 0;
return (
<>
<Form.Item label={t('pages.inbounds.form.externalProxy')}>
<Switch checked={on} onChange={toggleExternalProxy} />
</Form.Item>
{on && (
<Form.List name={['streamSettings', 'externalProxy']}>
{(fields, { add, remove }) => (
<>
<Form.Item label=" " colon={false}>
<Button
size="small"
type="primary"
onClick={() => add({
forceTls: 'same',
dest: '',
port: 443,
remark: '',
sni: '',
fingerprint: '',
alpn: [],
})}
>
<PlusOutlined />
</Button>
</Form.Item>
<Form.Item wrapperCol={{ span: 24 }}>
{fields.map((field) => (
<div key={field.key} style={{ margin: '8px 0' }}>
<Space.Compact block>
<Form.Item name={[field.name, 'forceTls']} noStyle>
<Select
style={{ width: '20%' }}
options={[
{ value: 'same', label: t('pages.inbounds.same') },
{ value: 'none', label: t('none') },
{ value: 'tls', label: 'TLS' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'dest']} noStyle>
<Input style={{ width: '30%' }} placeholder={t('host')} />
</Form.Item>
<Form.Item name={[field.name, 'port']} noStyle>
<InputNumber style={{ width: '15%' }} min={1} max={65535} />
</Form.Item>
<Form.Item name={[field.name, 'remark']} noStyle>
<Input style={{ width: '25%' }} placeholder={t('pages.inbounds.remark')} />
</Form.Item>
<InputAddon onClick={() => remove(field.name)}>
<MinusOutlined />
</InputAddon>
</Space.Compact>
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
prev.streamSettings?.externalProxy?.[field.name]?.forceTls
!== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
}
>
{({ getFieldValue }) => {
const ft = getFieldValue([
'streamSettings', 'externalProxy', field.name, 'forceTls',
]);
if (ft !== 'tls') return null;
return (
<Space.Compact style={{ marginTop: 6 }} block>
<Form.Item name={[field.name, 'sni']} noStyle>
<Input style={{ width: '30%' }} placeholder={t('pages.inbounds.form.sniPlaceholder')} />
</Form.Item>
<Form.Item name={[field.name, 'fingerprint']} noStyle>
<Select
style={{ width: '30%' }}
placeholder={t('pages.inbounds.form.fingerprint')}
options={[
{ value: '', label: t('pages.inbounds.form.defaultOption') },
...Object.values(UTLS_FINGERPRINT).map((fp) => ({
value: fp,
label: fp,
})),
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'alpn']} noStyle>
<Select
mode="multiple"
style={{ width: '40%' }}
placeholder="ALPN"
options={Object.values(ALPN_OPTION).map((a) => ({
value: a,
label: a,
}))}
/>
</Form.Item>
</Space.Compact>
);
}}
</Form.Item>
</div>
))}
</Form.Item>
</>
)}
</Form.List>
)}
</>
);
}}
</Form.Item>
<Form.Item <SockoptForm toggleSockopt={toggleSockopt} />
noStyle
shouldUpdate={(prev, curr) => {
const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt;
const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt;
return !!a !== !!b;
}}
>
{({ getFieldValue }) => {
const sock = getFieldValue(['streamSettings', 'sockopt']);
const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
return (
<>
<Form.Item label="Sockopt">
<Switch checked={on} onChange={toggleSockopt} />
</Form.Item>
{on && (
<>
<Form.Item name={['streamSettings', 'sockopt', 'mark']} label={t('pages.inbounds.form.routeMark')}>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
label={t('pages.inbounds.form.tcpKeepAliveInterval')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
label={t('pages.inbounds.form.tcpKeepAliveIdle')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'tcpMaxSeg']} label={t('pages.inbounds.form.tcpMaxSeg')}>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
label={t('pages.inbounds.form.tcpUserTimeout')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
label={t('pages.inbounds.form.tcpWindowClamp')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
label={t('pages.inbounds.form.proxyProtocol')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpFastOpen']}
label={t('pages.inbounds.form.tcpFastOpen')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpMptcp']}
label={t('pages.inbounds.form.multipathTcp')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'penetrate']}
label={t('pages.inbounds.form.penetrate')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'V6Only']}
label={t('pages.inbounds.form.v6Only')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'domainStrategy']}
label={t('pages.xray.wireguard.domainStrategy')}
>
<Select
style={{ width: '50%' }}
options={Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ({ value: d, label: d }))}
/>
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpcongestion']}
label={t('pages.inbounds.form.tcpCongestion')}
>
<Select
style={{ width: '50%' }}
options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
/>
</Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
<Select
style={{ width: '50%' }}
options={[
{ value: 'off', label: 'Off' },
{ value: 'redirect', label: 'Redirect' },
{ value: 'tproxy', label: 'TProxy' },
]}
/>
</Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label={t('pages.inbounds.form.dialerProxy')}>
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'interfaceName']}
label={t('pages.inbounds.info.interfaceName')}
>
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
label={t('pages.inbounds.form.trustedXForwardedFor')}
>
<Select
mode="tags"
style={{ width: '100%' }}
tokenSeparators={[',']}
options={[
{ value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
{ value: 'X-Real-IP', label: 'X-Real-IP' },
{ value: 'True-Client-IP', label: 'True-Client-IP' },
{ value: 'X-Client-IP', label: 'X-Client-IP' },
]}
/>
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'addressPortStrategy']}
label={t('pages.inbounds.form.addressPortStrategy')}
>
<Select
style={{ width: '50%' }}
options={Object.values(Address_Port_Strategy).map((v) => ({ value: v, label: v }))}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{({ getFieldValue, setFieldValue }) => {
const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
const hasHe = he != null;
return (
<>
<Form.Item label="Happy Eyeballs">
<Switch
checked={hasHe}
onChange={(v) => {
setFieldValue(
['streamSettings', 'sockopt', 'happyEyeballs'],
v ? HappyEyeballsSchema.parse({}) : undefined,
);
}}
/>
</Form.Item>
{hasHe && (
<>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
label={t('pages.inbounds.form.tryDelayMs')}
>
<InputNumber min={0} placeholder="0 disabled — 250 recommended" />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
label={t('pages.inbounds.form.prioritizeIPv6')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
label={t('pages.inbounds.form.interleave')}
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
label={t('pages.inbounds.form.maxConcurrentTry')}
>
<InputNumber min={0} />
</Form.Item>
</>
)}
</>
);
}}
</Form.Item>
<Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
{(fields, { add, remove }) => (
<>
<Form.Item label={t('pages.inbounds.form.customSockopt')}>
<Button
type="dashed"
size="small"
onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
>
+ {t('pages.inbounds.form.addCustomOption')}
</Button>
</Form.Item>
{fields.map((field) => (
<Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
<Form.Item name={[field.name, 'system']} noStyle>
<Select
placeholder="all"
allowClear
style={{ width: 100 }}
options={[
{ value: 'linux', label: 'linux' },
{ value: 'windows', label: 'windows' },
{ value: 'darwin', label: 'darwin' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'type']} noStyle>
<Select
style={{ width: 80 }}
options={[
{ value: 'int', label: 'int' },
{ value: 'str', label: 'str' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'level']} noStyle>
<Input placeholder="level (6=TCP)" style={{ width: 100 }} />
</Form.Item>
<Form.Item name={[field.name, 'opt']} noStyle>
<Input placeholder="opt" style={{ width: 120 }} />
</Form.Item>
<Form.Item name={[field.name, 'value']} noStyle>
<Input placeholder="value" style={{ flex: 1 }} />
</Form.Item>
<Button danger onClick={() => remove(field.name)}></Button>
</Space.Compact>
))}
</>
)}
</Form.List>
</>
)}
</>
);
}}
</Form.Item>
<FinalMaskForm <FinalMaskForm
name={['streamSettings', 'finalmask']} name={['streamSettings', 'finalmask']}

View file

@ -0,0 +1,136 @@
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
import { InputAddon } from '@/components/ui';
import { ALPN_OPTION, UTLS_FINGERPRINT } from '@/schemas/primitives';
export default function ExternalProxyForm({
toggleExternalProxy,
}: {
toggleExternalProxy: (on: boolean) => void;
}) {
const { t } = useTranslation();
return (
<Form.Item
noStyle
shouldUpdate={(prev, curr) => {
const a = (prev.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
const b = (curr.streamSettings as { externalProxy?: unknown[] } | undefined)?.externalProxy;
return (Array.isArray(a) ? a.length : 0) !== (Array.isArray(b) ? b.length : 0);
}}
>
{({ getFieldValue }) => {
const arr = getFieldValue(['streamSettings', 'externalProxy']);
const on = Array.isArray(arr) && arr.length > 0;
return (
<>
<Form.Item label={t('pages.inbounds.form.externalProxy')}>
<Switch checked={on} onChange={toggleExternalProxy} />
</Form.Item>
{on && (
<Form.List name={['streamSettings', 'externalProxy']}>
{(fields, { add, remove }) => (
<>
<Form.Item label=" " colon={false}>
<Button
size="small"
type="primary"
onClick={() => add({
forceTls: 'same',
dest: '',
port: 443,
remark: '',
sni: '',
fingerprint: '',
alpn: [],
})}
>
<PlusOutlined />
</Button>
</Form.Item>
<Form.Item wrapperCol={{ span: 24 }}>
{fields.map((field) => (
<div key={field.key} style={{ margin: '8px 0' }}>
<Space.Compact block>
<Form.Item name={[field.name, 'forceTls']} noStyle>
<Select
style={{ width: '20%' }}
options={[
{ value: 'same', label: t('pages.inbounds.same') },
{ value: 'none', label: t('none') },
{ value: 'tls', label: 'TLS' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'dest']} noStyle>
<Input style={{ width: '30%' }} placeholder={t('host')} />
</Form.Item>
<Form.Item name={[field.name, 'port']} noStyle>
<InputNumber style={{ width: '15%' }} min={1} max={65535} />
</Form.Item>
<Form.Item name={[field.name, 'remark']} noStyle>
<Input style={{ width: '25%' }} placeholder={t('pages.inbounds.remark')} />
</Form.Item>
<InputAddon onClick={() => remove(field.name)}>
<MinusOutlined />
</InputAddon>
</Space.Compact>
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
prev.streamSettings?.externalProxy?.[field.name]?.forceTls
!== curr.streamSettings?.externalProxy?.[field.name]?.forceTls
}
>
{({ getFieldValue }) => {
const ft = getFieldValue([
'streamSettings', 'externalProxy', field.name, 'forceTls',
]);
if (ft !== 'tls') return null;
return (
<Space.Compact style={{ marginTop: 6 }} block>
<Form.Item name={[field.name, 'sni']} noStyle>
<Input style={{ width: '30%' }} placeholder={t('pages.inbounds.form.sniPlaceholder')} />
</Form.Item>
<Form.Item name={[field.name, 'fingerprint']} noStyle>
<Select
style={{ width: '30%' }}
placeholder={t('pages.inbounds.form.fingerprint')}
options={[
{ value: '', label: t('pages.inbounds.form.defaultOption') },
...Object.values(UTLS_FINGERPRINT).map((fp) => ({
value: fp,
label: fp,
})),
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'alpn']} noStyle>
<Select
mode="multiple"
style={{ width: '40%' }}
placeholder="ALPN"
options={Object.values(ALPN_OPTION).map((a) => ({
value: a,
label: a,
}))}
/>
</Form.Item>
</Space.Compact>
);
}}
</Form.Item>
</div>
))}
</Form.Item>
</>
)}
</Form.List>
)}
</>
);
}}
</Form.Item>
);
}

View file

@ -4,3 +4,5 @@ export { default as GrpcForm } from './grpc';
export { default as XhttpForm } from './xhttp'; export { default as XhttpForm } from './xhttp';
export { default as HttpUpgradeForm } from './httpupgrade'; export { default as HttpUpgradeForm } from './httpupgrade';
export { default as KcpForm } from './kcp'; export { default as KcpForm } from './kcp';
export { default as ExternalProxyForm } from './external-proxy';
export { default as SockoptForm } from './sockopt';

View file

@ -0,0 +1,270 @@
import { useTranslation } from 'react-i18next';
import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
import {
Address_Port_Strategy,
DOMAIN_STRATEGY_OPTION,
TCP_CONGESTION_OPTION,
} from '@/schemas/primitives';
import { HappyEyeballsSchema } from '@/schemas/protocols/stream/sockopt';
export default function SockoptForm({
toggleSockopt,
}: {
toggleSockopt: (on: boolean) => void;
}) {
const { t } = useTranslation();
return (
<Form.Item
noStyle
shouldUpdate={(prev, curr) => {
const a = (prev.streamSettings as { sockopt?: object } | undefined)?.sockopt;
const b = (curr.streamSettings as { sockopt?: object } | undefined)?.sockopt;
return !!a !== !!b;
}}
>
{({ getFieldValue }) => {
const sock = getFieldValue(['streamSettings', 'sockopt']);
const on = !!sock && typeof sock === 'object' && Object.keys(sock).length > 0;
return (
<>
<Form.Item label="Sockopt">
<Switch checked={on} onChange={toggleSockopt} />
</Form.Item>
{on && (
<>
<Form.Item name={['streamSettings', 'sockopt', 'mark']} label={t('pages.inbounds.form.routeMark')}>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpKeepAliveInterval']}
label={t('pages.inbounds.form.tcpKeepAliveInterval')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpKeepAliveIdle']}
label={t('pages.inbounds.form.tcpKeepAliveIdle')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'tcpMaxSeg']} label={t('pages.inbounds.form.tcpMaxSeg')}>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
label={t('pages.inbounds.form.tcpUserTimeout')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpWindowClamp']}
label={t('pages.inbounds.form.tcpWindowClamp')}
>
<InputNumber min={0} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
label={t('pages.inbounds.form.proxyProtocol')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpFastOpen']}
label={t('pages.inbounds.form.tcpFastOpen')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpMptcp']}
label={t('pages.inbounds.form.multipathTcp')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'penetrate']}
label={t('pages.inbounds.form.penetrate')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'V6Only']}
label={t('pages.inbounds.form.v6Only')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'domainStrategy']}
label={t('pages.xray.wireguard.domainStrategy')}
>
<Select
style={{ width: '50%' }}
options={Object.values(DOMAIN_STRATEGY_OPTION).map((d) => ({ value: d, label: d }))}
/>
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'tcpcongestion']}
label={t('pages.inbounds.form.tcpCongestion')}
>
<Select
style={{ width: '50%' }}
options={Object.values(TCP_CONGESTION_OPTION).map((c) => ({ value: c, label: c }))}
/>
</Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'tproxy']} label="TProxy">
<Select
style={{ width: '50%' }}
options={[
{ value: 'off', label: 'Off' },
{ value: 'redirect', label: 'Redirect' },
{ value: 'tproxy', label: 'TProxy' },
]}
/>
</Form.Item>
<Form.Item name={['streamSettings', 'sockopt', 'dialerProxy']} label={t('pages.inbounds.form.dialerProxy')}>
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'interfaceName']}
label={t('pages.inbounds.info.interfaceName')}
>
<Input />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'trustedXForwardedFor']}
label={t('pages.inbounds.form.trustedXForwardedFor')}
>
<Select
mode="tags"
style={{ width: '100%' }}
tokenSeparators={[',']}
options={[
{ value: 'CF-Connecting-IP', label: 'CF-Connecting-IP' },
{ value: 'X-Real-IP', label: 'X-Real-IP' },
{ value: 'True-Client-IP', label: 'True-Client-IP' },
{ value: 'X-Client-IP', label: 'X-Client-IP' },
]}
/>
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'addressPortStrategy']}
label={t('pages.inbounds.form.addressPortStrategy')}
>
<Select
style={{ width: '50%' }}
options={Object.values(Address_Port_Strategy).map((v) => ({ value: v, label: v }))}
/>
</Form.Item>
<Form.Item shouldUpdate noStyle>
{({ getFieldValue, setFieldValue }) => {
const he = getFieldValue(['streamSettings', 'sockopt', 'happyEyeballs']);
const hasHe = he != null;
return (
<>
<Form.Item label="Happy Eyeballs">
<Switch
checked={hasHe}
onChange={(v) => {
setFieldValue(
['streamSettings', 'sockopt', 'happyEyeballs'],
v ? HappyEyeballsSchema.parse({}) : undefined,
);
}}
/>
</Form.Item>
{hasHe && (
<>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'tryDelayMs']}
label={t('pages.inbounds.form.tryDelayMs')}
>
<InputNumber min={0} placeholder="0 disabled — 250 recommended" />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'prioritizeIPv6']}
label={t('pages.inbounds.form.prioritizeIPv6')}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'interleave']}
label={t('pages.inbounds.form.interleave')}
>
<InputNumber min={1} />
</Form.Item>
<Form.Item
name={['streamSettings', 'sockopt', 'happyEyeballs', 'maxConcurrentTry']}
label={t('pages.inbounds.form.maxConcurrentTry')}
>
<InputNumber min={0} />
</Form.Item>
</>
)}
</>
);
}}
</Form.Item>
<Form.List name={['streamSettings', 'sockopt', 'customSockopt']}>
{(fields, { add, remove }) => (
<>
<Form.Item label={t('pages.inbounds.form.customSockopt')}>
<Button
type="dashed"
size="small"
onClick={() => add({ type: 'int', level: '6', opt: '', value: '' })}
>
+ {t('pages.inbounds.form.addCustomOption')}
</Button>
</Form.Item>
{fields.map((field) => (
<Space.Compact key={field.key} style={{ display: 'flex', marginBottom: 8 }}>
<Form.Item name={[field.name, 'system']} noStyle>
<Select
placeholder="all"
allowClear
style={{ width: 100 }}
options={[
{ value: 'linux', label: 'linux' },
{ value: 'windows', label: 'windows' },
{ value: 'darwin', label: 'darwin' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'type']} noStyle>
<Select
style={{ width: 80 }}
options={[
{ value: 'int', label: 'int' },
{ value: 'str', label: 'str' },
]}
/>
</Form.Item>
<Form.Item name={[field.name, 'level']} noStyle>
<Input placeholder="level (6=TCP)" style={{ width: 100 }} />
</Form.Item>
<Form.Item name={[field.name, 'opt']} noStyle>
<Input placeholder="opt" style={{ width: 120 }} />
</Form.Item>
<Form.Item name={[field.name, 'value']} noStyle>
<Input placeholder="value" style={{ flex: 1 }} />
</Form.Item>
<Button danger onClick={() => remove(field.name)}></Button>
</Space.Compact>
))}
</>
)}
</Form.List>
</>
)}
</>
);
}}
</Form.Item>
);
}

View file

@ -36,6 +36,12 @@ exports[`inbound security forms > TlsForm field structure is stable 1`] = `
] ]
`; `;
exports[`inbound transport forms > ExternalProxyForm field structure is stable (one TLS entry) 1`] = `
[
"External Proxy",
]
`;
exports[`inbound transport forms > GrpcForm field structure is stable 1`] = ` exports[`inbound transport forms > GrpcForm field structure is stable 1`] = `
[ [
"Service Name", "Service Name",
@ -71,6 +77,36 @@ exports[`inbound transport forms > RawForm field structure is stable 1`] = `
] ]
`; `;
exports[`inbound transport forms > SockoptForm field structure is stable (enabled + happy eyeballs) 1`] = `
[
"Sockopt",
"Route Mark",
"TCP Keep Alive Interval",
"TCP Keep Alive Idle",
"TCP Max Seg",
"TCP User Timeout",
"TCP Window Clamp",
"Proxy Protocol",
"TCP Fast Open",
"Multipath TCP",
"Penetrate",
"V6 Only",
"Domain Strategy",
"TCP Congestion",
"TProxy",
"Dialer Proxy",
"Interface name",
"Trusted X-Forwarded-For",
"Address+port strategy",
"Happy Eyeballs",
"Try delay (ms)",
"Prioritize IPv6",
"Interleave",
"Max concurrent try",
"Custom sockopt",
]
`;
exports[`inbound transport forms > WsForm field structure is stable 1`] = ` exports[`inbound transport forms > WsForm field structure is stable 1`] = `
[ [
"Proxy Protocol", "Proxy Protocol",

View file

@ -3,10 +3,12 @@ import { Form, type FormInstance } from 'antd';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { import {
ExternalProxyForm,
GrpcForm, GrpcForm,
HttpUpgradeForm, HttpUpgradeForm,
KcpForm, KcpForm,
RawForm, RawForm,
SockoptForm,
WsForm, WsForm,
XhttpForm, XhttpForm,
} from '@/pages/inbounds/form/transport'; } from '@/pages/inbounds/form/transport';
@ -14,13 +16,22 @@ import { RealityForm, TlsForm } from '@/pages/inbounds/form/security';
import type { InboundFormValues } from '@/schemas/forms/inbound-form'; import type { InboundFormValues } from '@/schemas/forms/inbound-form';
import { renderWithProviders, fieldLabels } from './test-utils'; import { renderWithProviders, fieldLabels } from './test-utils';
function FormHarness({ children }: { children: (form: FormInstance<InboundFormValues>) => ReactNode }) { function FormHarness({
children,
initialValues,
}: {
children: (form: FormInstance<InboundFormValues>) => ReactNode;
initialValues?: Record<string, unknown>;
}) {
const [form] = Form.useForm<InboundFormValues>(); const [form] = Form.useForm<InboundFormValues>();
return <Form form={form}>{children(form)}</Form>; return <Form form={form} initialValues={initialValues}>{children(form)}</Form>;
} }
function renderInForm(node: (form: FormInstance<InboundFormValues>) => ReactNode) { function renderInForm(
return renderWithProviders(<FormHarness>{node}</FormHarness>); node: (form: FormInstance<InboundFormValues>) => ReactNode,
initialValues?: Record<string, unknown>,
) {
return renderWithProviders(<FormHarness initialValues={initialValues}>{node}</FormHarness>);
} }
const noop = () => {}; const noop = () => {};
@ -55,6 +66,34 @@ describe('inbound transport forms', () => {
renderInForm((form) => <XhttpForm form={form} />); renderInForm((form) => <XhttpForm form={form} />);
expect(fieldLabels()).toMatchSnapshot(); expect(fieldLabels()).toMatchSnapshot();
}); });
it('ExternalProxyForm field structure is stable (one TLS entry)', () => {
renderInForm(
() => <ExternalProxyForm toggleExternalProxy={noop} />,
{
streamSettings: {
externalProxy: [{
forceTls: 'tls',
dest: '',
port: 443,
remark: '',
sni: '',
fingerprint: '',
alpn: [],
}],
},
},
);
expect(fieldLabels()).toMatchSnapshot();
});
it('SockoptForm field structure is stable (enabled + happy eyeballs)', () => {
renderInForm(
() => <SockoptForm toggleSockopt={noop} />,
{ streamSettings: { sockopt: { happyEyeballs: {} } } },
);
expect(fieldLabels()).toMatchSnapshot();
});
}); });
describe('inbound security forms', () => { describe('inbound security forms', () => {