mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-05-31 10:14:15 +00:00
fix(outbounds): support proxyProtocol on freedom outbound
Xray's freedom outbound accepts a numeric proxyProtocol (0 disabled, 1 or 2 for the PROXY protocol version), but the panel had no field for it and the typed form adapter dropped the key on save — so a value set via the JSON editor disappeared the moment the outbound was saved. Model proxyProtocol through the freedom wire schema, the form schema, and both adapter directions (clamped to 0/1/2, omitted from the wire when 0), and add a Select (none / v1 / v2) to the freedom section of the outbound form. Add round-trip test coverage and the proxyProtocol label across all locales. Closes #4486
This commit is contained in:
parent
5d0081a3b9
commit
62c293e034
18 changed files with 39 additions and 0 deletions
|
|
@ -265,6 +265,10 @@ function freedomFromWire(raw: Raw): FreedomOutboundFormSettings {
|
||||||
return (allowed.includes(s) ? s : '') as FreedomOutboundFormSettings['domainStrategy'];
|
return (allowed.includes(s) ? s : '') as FreedomOutboundFormSettings['domainStrategy'];
|
||||||
})(),
|
})(),
|
||||||
redirect: asString(raw.redirect),
|
redirect: asString(raw.redirect),
|
||||||
|
proxyProtocol: ((): FreedomOutboundFormSettings['proxyProtocol'] => {
|
||||||
|
const n = asNumber(raw.proxyProtocol, 0);
|
||||||
|
return (n === 1 || n === 2) ? n : 0;
|
||||||
|
})(),
|
||||||
fragment: wireHasFragment
|
fragment: wireHasFragment
|
||||||
? {
|
? {
|
||||||
packets: asString(fragment.packets, '1-3'),
|
packets: asString(fragment.packets, '1-3'),
|
||||||
|
|
@ -489,6 +493,7 @@ function freedomToWire(s: FreedomOutboundFormSettings) {
|
||||||
return {
|
return {
|
||||||
domainStrategy: s.domainStrategy || undefined,
|
domainStrategy: s.domainStrategy || undefined,
|
||||||
redirect: s.redirect || undefined,
|
redirect: s.redirect || undefined,
|
||||||
|
proxyProtocol: s.proxyProtocol || undefined,
|
||||||
fragment: fragmentEnabled ? Object.fromEntries(fragmentEntries) : undefined,
|
fragment: fragmentEnabled ? Object.fromEntries(fragmentEntries) : undefined,
|
||||||
noises: s.noises.length > 0 ? s.noises : undefined,
|
noises: s.noises.length > 0 ? s.noises : undefined,
|
||||||
finalRules: s.finalRules.length > 0
|
finalRules: s.finalRules.length > 0
|
||||||
|
|
|
||||||
|
|
@ -664,6 +664,15 @@ export default function OutboundFormModal({
|
||||||
<Form.Item label={t('pages.xray.outboundForm.redirect')} name={['settings', 'redirect']}>
|
<Form.Item label={t('pages.xray.outboundForm.redirect')} name={['settings', 'redirect']}>
|
||||||
<Input />
|
<Input />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
<Form.Item label={t('pages.xray.outboundForm.proxyProtocol')} name={['settings', 'proxyProtocol']}>
|
||||||
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: 0, label: `(${t('none')})` },
|
||||||
|
{ value: 1, label: 'v1' },
|
||||||
|
{ value: 2, label: 'v2' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Form.Item label={t('pages.xray.outboundForm.fragment')} shouldUpdate noStyle>
|
<Form.Item label={t('pages.xray.outboundForm.fragment')} shouldUpdate noStyle>
|
||||||
{() => {
|
{() => {
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,7 @@ export type FreedomFinalRuleForm = z.infer<typeof FreedomFinalRuleFormSchema>;
|
||||||
export const FreedomOutboundFormSettingsSchema = z.object({
|
export const FreedomOutboundFormSettingsSchema = z.object({
|
||||||
domainStrategy: z.union([OutboundDomainStrategySchema, z.literal('')]).default(''),
|
domainStrategy: z.union([OutboundDomainStrategySchema, z.literal('')]).default(''),
|
||||||
redirect: z.string().default(''),
|
redirect: z.string().default(''),
|
||||||
|
proxyProtocol: z.number().int().min(0).max(2).default(0),
|
||||||
fragment: FreedomFragmentSchema.default({
|
fragment: FreedomFragmentSchema.default({
|
||||||
packets: '1-3',
|
packets: '1-3',
|
||||||
length: '',
|
length: '',
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ export type FreedomFinalRule = z.infer<typeof FreedomFinalRuleSchema>;
|
||||||
export const FreedomOutboundSettingsSchema = z.object({
|
export const FreedomOutboundSettingsSchema = z.object({
|
||||||
domainStrategy: OutboundDomainStrategySchema.optional(),
|
domainStrategy: OutboundDomainStrategySchema.optional(),
|
||||||
redirect: z.string().optional(),
|
redirect: z.string().optional(),
|
||||||
|
proxyProtocol: z.number().optional(),
|
||||||
fragment: FreedomFragmentSchema.optional(),
|
fragment: FreedomFragmentSchema.optional(),
|
||||||
noises: z.array(FreedomNoiseSchema).optional(),
|
noises: z.array(FreedomNoiseSchema).optional(),
|
||||||
finalRules: z.array(FreedomFinalRuleSchema).optional(),
|
finalRules: z.array(FreedomFinalRuleSchema).optional(),
|
||||||
|
|
|
||||||
|
|
@ -235,16 +235,26 @@ describe('outbound-form-adapter: round-trip', () => {
|
||||||
settings: {
|
settings: {
|
||||||
domainStrategy: 'UseIPv4',
|
domainStrategy: 'UseIPv4',
|
||||||
redirect: '1.1.1.1',
|
redirect: '1.1.1.1',
|
||||||
|
proxyProtocol: 2,
|
||||||
fragment: { packets: 'tlshello', length: '100-200' },
|
fragment: { packets: 'tlshello', length: '100-200' },
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
expect(filled.settings).toMatchObject({
|
expect(filled.settings).toMatchObject({
|
||||||
domainStrategy: 'UseIPv4',
|
domainStrategy: 'UseIPv4',
|
||||||
redirect: '1.1.1.1',
|
redirect: '1.1.1.1',
|
||||||
|
proxyProtocol: 2,
|
||||||
fragment: { packets: 'tlshello', length: '100-200' },
|
fragment: { packets: 'tlshello', length: '100-200' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('freedom omits proxyProtocol when disabled (0)', () => {
|
||||||
|
const round = formValuesToWirePayload(rawOutboundToFormValues({
|
||||||
|
protocol: 'freedom',
|
||||||
|
settings: { proxyProtocol: 0 },
|
||||||
|
}));
|
||||||
|
expect((round.settings as { proxyProtocol?: number }).proxyProtocol).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
it('mux is only emitted when enabled AND protocol/network/flow allow it', () => {
|
it('mux is only emitted when enabled AND protocol/network/flow allow it', () => {
|
||||||
// Disabled mux: omitted
|
// Disabled mux: omitted
|
||||||
const disabled = formValuesToWirePayload(rawOutboundToFormValues({
|
const disabled = formValuesToWirePayload(rawOutboundToFormValues({
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "الواجهة",
|
"interface": "الواجهة",
|
||||||
"ipv6Only": "IPv6 فقط",
|
"ipv6Only": "IPv6 فقط",
|
||||||
"acceptProxyProtocol": "قبول proxy protocol",
|
"acceptProxyProtocol": "قبول proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (ثانية)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (ثانية)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Interface",
|
"interface": "Interface",
|
||||||
"ipv6Only": "IPv6 only",
|
"ipv6Only": "IPv6 only",
|
||||||
"acceptProxyProtocol": "Accept proxy protocol",
|
"acceptProxyProtocol": "Accept proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Interfaz",
|
"interface": "Interfaz",
|
||||||
"ipv6Only": "Solo IPv6",
|
"ipv6Only": "Solo IPv6",
|
||||||
"acceptProxyProtocol": "Aceptar proxy protocol",
|
"acceptProxyProtocol": "Aceptar proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "رابط",
|
"interface": "رابط",
|
||||||
"ipv6Only": "فقط IPv6",
|
"ipv6Only": "فقط IPv6",
|
||||||
"acceptProxyProtocol": "پذیرش Proxy Protocol",
|
"acceptProxyProtocol": "پذیرش Proxy Protocol",
|
||||||
|
"proxyProtocol": "Proxy Protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Interface",
|
"interface": "Interface",
|
||||||
"ipv6Only": "Hanya IPv6",
|
"ipv6Only": "Hanya IPv6",
|
||||||
"acceptProxyProtocol": "Terima proxy protocol",
|
"acceptProxyProtocol": "Terima proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (d)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (d)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "インターフェース",
|
"interface": "インターフェース",
|
||||||
"ipv6Only": "IPv6 のみ",
|
"ipv6Only": "IPv6 のみ",
|
||||||
"acceptProxyProtocol": "proxy protocol を受け入れる",
|
"acceptProxyProtocol": "proxy protocol を受け入れる",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (秒)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (秒)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Interface",
|
"interface": "Interface",
|
||||||
"ipv6Only": "Apenas IPv6",
|
"ipv6Only": "Apenas IPv6",
|
||||||
"acceptProxyProtocol": "Aceitar proxy protocol",
|
"acceptProxyProtocol": "Aceitar proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Интерфейс",
|
"interface": "Интерфейс",
|
||||||
"ipv6Only": "Только IPv6",
|
"ipv6Only": "Только IPv6",
|
||||||
"acceptProxyProtocol": "Принимать proxy protocol",
|
"acceptProxyProtocol": "Принимать proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (мс)",
|
"tcpUserTimeoutMs": "TCP user timeout (мс)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Arabirim",
|
"interface": "Arabirim",
|
||||||
"ipv6Only": "Yalnızca IPv6",
|
"ipv6Only": "Yalnızca IPv6",
|
||||||
"acceptProxyProtocol": "Proxy protocol kabul et",
|
"acceptProxyProtocol": "Proxy protocol kabul et",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Інтерфейс",
|
"interface": "Інтерфейс",
|
||||||
"ipv6Only": "Лише IPv6",
|
"ipv6Only": "Лише IPv6",
|
||||||
"acceptProxyProtocol": "Приймати proxy protocol",
|
"acceptProxyProtocol": "Приймати proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (мс)",
|
"tcpUserTimeoutMs": "TCP user timeout (мс)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "Giao diện",
|
"interface": "Giao diện",
|
||||||
"ipv6Only": "Chỉ IPv6",
|
"ipv6Only": "Chỉ IPv6",
|
||||||
"acceptProxyProtocol": "Chấp nhận proxy protocol",
|
"acceptProxyProtocol": "Chấp nhận proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "接口",
|
"interface": "接口",
|
||||||
"ipv6Only": "仅 IPv6",
|
"ipv6Only": "仅 IPv6",
|
||||||
"acceptProxyProtocol": "接受 proxy protocol",
|
"acceptProxyProtocol": "接受 proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1209,6 +1209,7 @@
|
||||||
"interface": "介面",
|
"interface": "介面",
|
||||||
"ipv6Only": "僅 IPv6",
|
"ipv6Only": "僅 IPv6",
|
||||||
"acceptProxyProtocol": "接受 proxy protocol",
|
"acceptProxyProtocol": "接受 proxy protocol",
|
||||||
|
"proxyProtocol": "Proxy protocol",
|
||||||
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
"tcpUserTimeoutMs": "TCP user timeout (ms)",
|
||||||
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
"tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue