feat(frontend): symmetric TCP HTTP host/path + extra sockopt knobs

OutboundFormModal:
- Sockopt section gains 5 common-but-rarely-tweaked knobs:
  acceptProxyProtocol, tproxy (off/redirect/tproxy), tcpcongestion
  (bbr/cubic/reno), V6Only, tcpUserTimeout. The remaining sockopt
  fields (tcpKeepAliveIdle, tcpMaxSeg, tcpWindowClamp,
  trustedXForwardedFor) are still edit-via-JSON; they are deeply
  tunable and not commonly touched.

InboundFormModal:
- TCP HTTP camouflage gains host + path inputs symmetric to the
  outbound side. Switch ON seeds request with sensible defaults
  (version 1.1, method GET, path ['/'], empty headers). The two
  inputs use the same normalize/getValueProps comma-string ↔
  string[] dance the outbound side uses, so the wire shape stays
  identical to what xray-core expects.
This commit is contained in:
MHSanaei
2026-05-26 12:41:23 +02:00
parent ad3d3937b0
commit e62ad84bb7
2 changed files with 111 additions and 1 deletions

View File

@@ -1157,7 +1157,17 @@ export default function InboundFormModal({
onChange={(v) => {
setFieldValue(
['streamSettings', 'tcpSettings', 'header'],
v ? { type: 'http' } : { type: 'none' },
v
? {
type: 'http',
request: {
version: '1.1',
method: 'GET',
path: ['/'],
headers: {},
},
}
: { type: 'none' },
);
}}
/>
@@ -1165,6 +1175,62 @@ export default function InboundFormModal({
}}
</Form.Item>
</Form.Item>
{/* Host + path camouflage inputs only render when the Switch
above is on. Both are string[] on the wire; normalize +
getValueProps translate to/from comma-joined input. Mirrors
the symmetric outbound side. */}
<Form.Item
noStyle
shouldUpdate={(prev, curr) =>
prev.streamSettings?.tcpSettings?.header?.type
!== curr.streamSettings?.tcpSettings?.header?.type
}
>
{({ getFieldValue }) => {
const headerType = getFieldValue(
['streamSettings', 'tcpSettings', 'header', 'type'],
) as string | undefined;
if (headerType !== 'http') return null;
return (
<>
<Form.Item
label={t('host')}
name={[
'streamSettings', 'tcpSettings', 'header',
'request', 'headers', 'Host',
]}
normalize={(v: unknown) =>
typeof v === 'string'
? v.split(',').map((s) => s.trim()).filter(Boolean)
: Array.isArray(v) ? v : []
}
getValueProps={(v: unknown) => ({
value: Array.isArray(v) ? v.join(',') : '',
})}
>
<Input placeholder="example.com,cdn.example.com" />
</Form.Item>
<Form.Item
label={t('path')}
name={[
'streamSettings', 'tcpSettings', 'header',
'request', 'path',
]}
normalize={(v: unknown) =>
typeof v === 'string'
? v.split(',').map((s) => s.trim()).filter(Boolean)
: Array.isArray(v) ? v : ['/']
}
getValueProps={(v: unknown) => ({
value: Array.isArray(v) ? v.join(',') : '/',
})}
>
<Input placeholder="/,/api,/static" />
</Form.Item>
</>
);
}}
</Form.Item>
</>
)}

View File

@@ -38,6 +38,7 @@ import {
OutboundDomainStrategies,
OutboundProtocols as Protocols,
SNIFFING_OPTION,
TCP_CONGESTION_OPTION,
TLS_FLOW_CONTROL,
USERS_SECURITY,
UTLS_FINGERPRINT,
@@ -1516,6 +1517,49 @@ export default function OutboundFormModal({
>
<Input />
</Form.Item>
<Form.Item
label="TProxy"
name={['streamSettings', 'sockopt', 'tproxy']}
>
<Select
options={[
{ value: 'off', label: 'off' },
{ value: 'redirect', label: 'redirect' },
{ value: 'tproxy', label: 'tproxy' },
]}
/>
</Form.Item>
<Form.Item
label="TCP congestion"
name={['streamSettings', 'sockopt', 'tcpcongestion']}
>
<Select
options={Object.values(TCP_CONGESTION_OPTION).map((v) => ({
value: v,
label: v,
}))}
/>
</Form.Item>
<Form.Item
label="IPv6 only"
name={['streamSettings', 'sockopt', 'V6Only']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="Accept proxy protocol"
name={['streamSettings', 'sockopt', 'acceptProxyProtocol']}
valuePropName="checked"
>
<Switch />
</Form.Item>
<Form.Item
label="TCP user timeout (ms)"
name={['streamSettings', 'sockopt', 'tcpUserTimeout']}
>
<InputNumber min={0} style={{ width: '100%' }} />
</Form.Item>
</>
)}
</>