Просмотр исходного кода

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
MHSanaei 1 день назад
Родитель
Сommit
62c293e034

+ 5 - 0
frontend/src/lib/xray/outbound-form-adapter.ts

@@ -265,6 +265,10 @@ function freedomFromWire(raw: Raw): FreedomOutboundFormSettings {
       return (allowed.includes(s) ? s : '') as FreedomOutboundFormSettings['domainStrategy'];
     })(),
     redirect: asString(raw.redirect),
+    proxyProtocol: ((): FreedomOutboundFormSettings['proxyProtocol'] => {
+      const n = asNumber(raw.proxyProtocol, 0);
+      return (n === 1 || n === 2) ? n : 0;
+    })(),
     fragment: wireHasFragment
       ? {
           packets: asString(fragment.packets, '1-3'),
@@ -489,6 +493,7 @@ function freedomToWire(s: FreedomOutboundFormSettings) {
   return {
     domainStrategy: s.domainStrategy || undefined,
     redirect: s.redirect || undefined,
+    proxyProtocol: s.proxyProtocol || undefined,
     fragment: fragmentEnabled ? Object.fromEntries(fragmentEntries) : undefined,
     noises: s.noises.length > 0 ? s.noises : undefined,
     finalRules: s.finalRules.length > 0

+ 9 - 0
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -664,6 +664,15 @@ export default function OutboundFormModal({
                         <Form.Item label={t('pages.xray.outboundForm.redirect')} name={['settings', 'redirect']}>
                           <Input />
                         </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>
                           {() => {

+ 1 - 0
frontend/src/schemas/forms/outbound-form.ts

@@ -166,6 +166,7 @@ export type FreedomFinalRuleForm = z.infer<typeof FreedomFinalRuleFormSchema>;
 export const FreedomOutboundFormSettingsSchema = z.object({
   domainStrategy: z.union([OutboundDomainStrategySchema, z.literal('')]).default(''),
   redirect: z.string().default(''),
+  proxyProtocol: z.number().int().min(0).max(2).default(0),
   fragment: FreedomFragmentSchema.default({
     packets: '1-3',
     length: '',

+ 1 - 0
frontend/src/schemas/protocols/outbound/freedom.ts

@@ -52,6 +52,7 @@ export type FreedomFinalRule = z.infer<typeof FreedomFinalRuleSchema>;
 export const FreedomOutboundSettingsSchema = z.object({
   domainStrategy: OutboundDomainStrategySchema.optional(),
   redirect: z.string().optional(),
+  proxyProtocol: z.number().optional(),
   fragment: FreedomFragmentSchema.optional(),
   noises: z.array(FreedomNoiseSchema).optional(),
   finalRules: z.array(FreedomFinalRuleSchema).optional(),

+ 10 - 0
frontend/src/test/outbound-form-adapter.test.ts

@@ -235,16 +235,26 @@ describe('outbound-form-adapter: round-trip', () => {
       settings: {
         domainStrategy: 'UseIPv4',
         redirect: '1.1.1.1',
+        proxyProtocol: 2,
         fragment: { packets: 'tlshello', length: '100-200' },
       },
     }));
     expect(filled.settings).toMatchObject({
       domainStrategy: 'UseIPv4',
       redirect: '1.1.1.1',
+      proxyProtocol: 2,
       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', () => {
     // Disabled mux: omitted
     const disabled = formValuesToWirePayload(rawOutboundToFormValues({

+ 1 - 0
web/translation/ar-EG.json

@@ -1209,6 +1209,7 @@
         "interface": "الواجهة",
         "ipv6Only": "IPv6 فقط",
         "acceptProxyProtocol": "قبول proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (ثانية)"
       },

+ 1 - 0
web/translation/en-US.json

@@ -1209,6 +1209,7 @@
         "interface": "Interface",
         "ipv6Only": "IPv6 only",
         "acceptProxyProtocol": "Accept proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },

+ 1 - 0
web/translation/es-ES.json

@@ -1209,6 +1209,7 @@
         "interface": "Interfaz",
         "ipv6Only": "Solo IPv6",
         "acceptProxyProtocol": "Aceptar proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },

+ 1 - 0
web/translation/fa-IR.json

@@ -1209,6 +1209,7 @@
         "interface": "رابط",
         "ipv6Only": "فقط IPv6",
         "acceptProxyProtocol": "پذیرش Proxy Protocol",
+        "proxyProtocol": "Proxy Protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },

+ 1 - 0
web/translation/id-ID.json

@@ -1209,6 +1209,7 @@
         "interface": "Interface",
         "ipv6Only": "Hanya IPv6",
         "acceptProxyProtocol": "Terima proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (d)"
       },

+ 1 - 0
web/translation/ja-JP.json

@@ -1209,6 +1209,7 @@
         "interface": "インターフェース",
         "ipv6Only": "IPv6 のみ",
         "acceptProxyProtocol": "proxy protocol を受け入れる",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (秒)"
       },

+ 1 - 0
web/translation/pt-BR.json

@@ -1209,6 +1209,7 @@
         "interface": "Interface",
         "ipv6Only": "Apenas IPv6",
         "acceptProxyProtocol": "Aceitar proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },

+ 1 - 0
web/translation/ru-RU.json

@@ -1209,6 +1209,7 @@
         "interface": "Интерфейс",
         "ipv6Only": "Только IPv6",
         "acceptProxyProtocol": "Принимать proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (мс)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
       },

+ 1 - 0
web/translation/tr-TR.json

@@ -1209,6 +1209,7 @@
         "interface": "Arabirim",
         "ipv6Only": "Yalnızca IPv6",
         "acceptProxyProtocol": "Proxy protocol kabul et",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },

+ 1 - 0
web/translation/uk-UA.json

@@ -1209,6 +1209,7 @@
         "interface": "Інтерфейс",
         "ipv6Only": "Лише IPv6",
         "acceptProxyProtocol": "Приймати proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (мс)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (с)"
       },

+ 1 - 0
web/translation/vi-VN.json

@@ -1209,6 +1209,7 @@
         "interface": "Giao diện",
         "ipv6Only": "Chỉ IPv6",
         "acceptProxyProtocol": "Chấp nhận proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },

+ 1 - 0
web/translation/zh-CN.json

@@ -1209,6 +1209,7 @@
         "interface": "接口",
         "ipv6Only": "仅 IPv6",
         "acceptProxyProtocol": "接受 proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },

+ 1 - 0
web/translation/zh-TW.json

@@ -1209,6 +1209,7 @@
         "interface": "介面",
         "ipv6Only": "僅 IPv6",
         "acceptProxyProtocol": "接受 proxy protocol",
+        "proxyProtocol": "Proxy protocol",
         "tcpUserTimeoutMs": "TCP user timeout (ms)",
         "tcpKeepAliveIdleS": "TCP keep-alive idle (s)"
       },