3 Commits 90a64a1b22 ... 8c30ddbfd9

Autor SHA1 Mensagem Data
  MHSanaei 8c30ddbfd9 fix(outbounds): persist optional blocks and fix stale edit reopen 10 horas atrás
  MHSanaei 62c293e034 fix(outbounds): support proxyProtocol on freedom outbound 12 horas atrás
  MHSanaei 5d0081a3b9 fix(qr): hide QR for post-quantum links on client QR page 12 horas atrás

+ 8 - 8
frontend/package-lock.json

@@ -24,7 +24,7 @@
         "react": "^19.2.6",
         "react-dom": "^19.2.6",
         "react-i18next": "^17.0.8",
-        "react-router-dom": "^7.15.1",
+        "react-router-dom": "^7.16.0",
         "recharts": "^3.8.1",
         "swagger-ui-react": "^5.32.6",
         "zod": "^4.4.3"
@@ -6004,9 +6004,9 @@
       }
     },
     "node_modules/react-router": {
-      "version": "7.15.1",
-      "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.15.1.tgz",
-      "integrity": "sha512-R8rl9HhgikFYoPJymnUtPXWbnDb3oget6lQnfIoupbt61aT9aOhRkDsY2XRhZRyX1Z/8a5sL74fXmFNm3NRK5A==",
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.16.0.tgz",
+      "integrity": "sha512-wArC8lVyJb3+jM9OpDyW6hLCizACWkvQR/sSGqSs+o5uEXEtGlqdZ4v8hENR3Jad6i+LRkK93q/+bQAcvl6V1A==",
       "license": "MIT",
       "dependencies": {
         "cookie": "^1.0.1",
@@ -6026,12 +6026,12 @@
       }
     },
     "node_modules/react-router-dom": {
-      "version": "7.15.1",
-      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.15.1.tgz",
-      "integrity": "sha512-AzF62gjY6U9rkMq4RfP/r2EVtQ7DMfNMjyOp/flLTCrtRylLiK4wT4pSq6O8rOXZ2eXdZYJPEYe+ifomiv+Igg==",
+      "version": "7.16.0",
+      "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.16.0.tgz",
+      "integrity": "sha512-kMUAbimWB5FVbF4Bce4bJsiKJWLIUHq/mEG8+CFDnCSgltptBiG5nguducmsJeGKytlCvQud9Qhzpn49iduTlA==",
       "license": "MIT",
       "dependencies": {
-        "react-router": "7.15.1"
+        "react-router": "7.16.0"
       },
       "engines": {
         "node": ">=20.0.0"

+ 1 - 1
frontend/package.json

@@ -36,7 +36,7 @@
     "react": "^19.2.6",
     "react-dom": "^19.2.6",
     "react-i18next": "^17.0.8",
-    "react-router-dom": "^7.15.1",
+    "react-router-dom": "^7.16.0",
     "recharts": "^3.8.1",
     "swagger-ui-react": "^5.32.6",
     "zod": "^4.4.3"

+ 7 - 0
frontend/src/lib/xray/inbound-link.ts

@@ -944,3 +944,10 @@ export function genWireguardConfigs(input: GenWireguardFanoutInput): string {
     }))
     .join('\r\n');
 }
+
+export function isPostQuantumLink(link: string): boolean {
+  if (/[?&]pqv=/.test(link)) return true;
+  if (link.includes('mlkem768') || link.includes('mldsa65')) return true;
+  if (link.includes('ML-KEM-768')) return true;
+  return false;
+}

+ 26 - 1
frontend/src/lib/xray/outbound-form-adapter.ts

@@ -1,3 +1,4 @@
+import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
 import { Wireguard } from '@/utils';
 
 import type {
@@ -265,6 +266,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'),
@@ -341,6 +346,23 @@ export interface RawOutboundRow {
   mux?: unknown;
 }
 
+export const XMUX_DEFAULTS = XHttpXmuxSchema.parse({});
+
+function hydrateStreamForm(stream: Raw): OutboundStreamFormValues {
+  const next = { ...stream };
+  const xh = next.xhttpSettings;
+  if (xh && typeof xh === 'object' && !Array.isArray(xh)) {
+    const xhttp = { ...(xh as Raw) };
+    const xmux = xhttp.xmux;
+    if (xmux && typeof xmux === 'object' && !Array.isArray(xmux)) {
+      xhttp.enableXmux = true;
+      xhttp.xmux = { ...XMUX_DEFAULTS, ...(xmux as Raw) };
+    }
+    next.xhttpSettings = xhttp;
+  }
+  return next as unknown as OutboundStreamFormValues;
+}
+
 export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues {
   const protocol = asString(raw.protocol, 'vless');
   const settings = asObject(raw.settings);
@@ -351,7 +373,7 @@ export function rawOutboundToFormValues(raw: RawOutboundRow): OutboundFormValues
     && typeof raw.streamSettings === 'object'
     && Object.keys(raw.streamSettings as Raw).length > 0;
   const streamSettings = hasStream
-    ? (raw.streamSettings as unknown as OutboundStreamFormValues)
+    ? hydrateStreamForm(raw.streamSettings as Raw)
     : undefined;
 
   let typed: OutboundFormSettings;
@@ -489,6 +511,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
@@ -553,7 +576,9 @@ function stripUiOnlyStreamFields(stream: unknown): Raw {
   const xh = next.xhttpSettings;
   if (xh && typeof xh === 'object') {
     const cleaned = { ...(xh as Raw) };
+    const xmuxEnabled = cleaned.enableXmux === true;
     delete cleaned.enableXmux;
+    if (!xmuxEnabled) delete cleaned.xmux;
     next.xhttpSettings = dropEmptyStrings(cleaned);
   }
   return next;

+ 1 - 14
frontend/src/pages/clients/ClientInfoModal.tsx

@@ -6,6 +6,7 @@ import { CopyOutlined, QrcodeOutlined } from '@ant-design/icons';
 import { ClipboardManager, HttpUtil, IntlUtil, SizeFormatter } from '@/utils';
 import { useDatepicker } from '@/hooks/useDatepicker';
 import type { ClientRecord, InboundOption } from '@/hooks/useClients';
+import { isPostQuantumLink } from '@/lib/xray/inbound-link';
 import QrPanel from '@/pages/inbounds/QrPanel';
 import './ClientInfoModal.css';
 
@@ -33,20 +34,6 @@ const INBOUND_PROTOCOL_COLORS: Record<string, string> = {
 
 const INBOUND_CHIP_LIMIT = 1;
 
-// Post-quantum keys blow up the encoded URL past what a single QR can
-// hold. In VLESS share links the algorithm names don't appear as plain
-// text — they ride inside query params:
-//   - mldsa65Verify becomes `pqv=<base64>` (sub/subService.go:841)
-//   - ML-KEM-768 becomes `encryption=mlkem768x25519plus.<...>`
-// We also keep the literal substrings so configs that DO embed them
-// directly (e.g. wireguard config text) still match.
-function isPostQuantumLink(link: string): boolean {
-  if (/[?&]pqv=/.test(link)) return true;
-  if (link.includes('mlkem768') || link.includes('mldsa65')) return true;
-  if (link.includes('ML-KEM-768')) return true;
-  return false;
-}
-
 // 3x-ui's genRemark concatenates inbound remark + client email (and an
 // optional extra) using a configurable separator. The email half is
 // redundant in the row title — the modal already names the client by

+ 8 - 1
frontend/src/pages/clients/ClientQrModal.tsx

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
 import { useTranslation } from 'react-i18next';
 import { Collapse, Modal, Spin } from 'antd';
 import { HttpUtil } from '@/utils';
+import { isPostQuantumLink } from '@/lib/xray/inbound-link';
 import QrPanel from '@/pages/inbounds/QrPanel';
 import type { ClientRecord } from '@/hooks/useClients';
 
@@ -93,7 +94,13 @@ export default function ClientQrModal({
       out.push({
         key: `l${idx}`,
         label: `${t('pages.clients.link')} ${idx + 1}`,
-        children: <QrPanel value={link} remark={`${client?.email || ''} #${idx + 1}`} />,
+        children: (
+          <QrPanel
+            value={link}
+            remark={`${client?.email || ''} #${idx + 1}`}
+            showQr={!isPostQuantumLink(link)}
+          />
+        ),
       });
     });
     return out;

+ 2 - 1
frontend/src/pages/inbounds/QrCodeModal.tsx

@@ -8,6 +8,7 @@ import {
   genAllLinks,
   genWireguardConfigs,
   genWireguardLinks,
+  isPostQuantumLink,
 } from '@/lib/xray/inbound-link';
 import { inboundFromDb, type DbInboundLike } from '@/lib/xray/inbound-from-db';
 import QrPanel from './QrPanel';
@@ -140,7 +141,7 @@ export default function QrCodeModal({
           value={item.value}
           remark={item.header}
           downloadName={item.downloadName || ''}
-          showQr={!item.value.includes('mldsa65') && !item.value.includes('ML-KEM-768')}
+          showQr={!isPostQuantumLink(item.value)}
         />
       ),
     })),

+ 1 - 13
frontend/src/pages/sub/SubPage.tsx

@@ -31,6 +31,7 @@ import {
 } from '@ant-design/icons';
 
 import { ClipboardManager, IntlUtil, LanguageManager } from '@/utils';
+import { isPostQuantumLink } from '@/lib/xray/inbound-link';
 import { setMessageInstance } from '@/utils/messageBus';
 import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme';
 import SubUsageSummary from './SubUsageSummary';
@@ -90,19 +91,6 @@ function trimEmail(remark: string, email: string): string {
     .trim();
 }
 
-// Post-quantum keys blow up the encoded URL past what a single QR can
-// hold. The algorithm names don't appear as plain text in the URL —
-// they ride inside query params: mldsa65Verify → `pqv=<base64>`,
-// ML-KEM-768 → `encryption=mlkem768x25519plus.<...>`. The literal
-// substrings are also matched in case a config (e.g. wireguard) embeds
-// them directly.
-function isPostQuantumLink(link: string): boolean {
-  if (/[?&]pqv=/.test(link)) return true;
-  if (link.includes('mlkem768') || link.includes('mldsa65')) return true;
-  if (link.includes('ML-KEM-768')) return true;
-  return false;
-}
-
 // Decode a base64 string as UTF-8. atob() returns a binary string where
 // each char holds one raw byte (Latin-1 interpretation), which mangles
 // any multi-byte UTF-8 sequence in the payload — most commonly the

+ 48 - 48
frontend/src/pages/xray/OutboundFormModal.tsx

@@ -21,6 +21,7 @@ import InputAddon from '@/components/InputAddon';
 import JsonEditor from '@/components/JsonEditor';
 import { Wireguard } from '@/utils';
 import {
+  XMUX_DEFAULTS,
   formValuesToWirePayload,
   rawOutboundToFormValues,
 } from '@/lib/xray/outbound-form-adapter';
@@ -335,6 +336,14 @@ export default function OutboundFormModal({
     form.setFieldValue('streamSettings', { ...newStreamSlice(next), security: newSecurity });
   }
 
+  function onXmuxToggle(checked: boolean) {
+    if (!checked) return;
+    const existing = form.getFieldValue(['streamSettings', 'xhttpSettings', 'xmux']);
+    const hasValues = existing && typeof existing === 'object' && Object.keys(existing).length > 0;
+    if (hasValues) return;
+    form.setFieldValue(['streamSettings', 'xhttpSettings', 'xmux'], { ...XMUX_DEFAULTS });
+  }
+
   const duplicateTag = useMemo(() => {
     const myTag = tag.trim();
     if (!myTag) return false;
@@ -392,17 +401,40 @@ export default function OutboundFormModal({
   }
 
   async function onOk() {
-    if (activeKey === '2' && !applyJsonToForm()) return;
-    try {
-      await form.validateFields();
-    } catch {
+    let values: OutboundFormValues;
+    if (activeKey === '2') {
+      const raw = jsonText.trim();
+      if (!raw) return;
+      let parsed: Record<string, unknown>;
+      try {
+        parsed = JSON.parse(raw) as Record<string, unknown>;
+      } catch (e) {
+        messageApi.error(`JSON: ${(e as Error).message}`);
+        return;
+      }
+      values = rawOutboundToFormValues(parsed);
+      form.resetFields();
+      form.setFieldsValue(values);
+      setJsonDirty(false);
+    } else {
+      try {
+        await form.validateFields();
+      } catch {
+        return;
+      }
+      values = form.getFieldsValue(true) as OutboundFormValues;
+    }
+    const tagValue = (values.tag ?? '').trim();
+    if (!tagValue) {
+      messageApi.error(t('pages.xray.outboundForm.tagRequired'));
       return;
     }
-    if (duplicateTag) {
+    const isDuplicateTag = (existingTags || []).includes(tagValue)
+      && !(isEdit && (outboundProp?.tag as string | undefined) === tagValue);
+    if (isDuplicateTag) {
       messageApi.error('Tag already used by another outbound');
       return;
     }
-    const values = form.getFieldsValue(true) as OutboundFormValues;
     onConfirm(formValuesToWirePayload(values));
   }
 
@@ -664,6 +696,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>
                           {() => {
@@ -1179,47 +1220,6 @@ export default function OutboundFormModal({
                                       >
                                         <Input placeholder="1.1" />
                                       </Form.Item>
-                                      <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
                                         label={t('pages.inbounds.form.requestHeaders')}
                                         name={[
@@ -1667,7 +1667,7 @@ export default function OutboundFormModal({
                               name={['streamSettings', 'xhttpSettings', 'enableXmux']}
                               valuePropName="checked"
                             >
-                              <Switch />
+                              <Switch onChange={onXmuxToggle} />
                             </Form.Item>
                             <Form.Item shouldUpdate noStyle>
                               {() => {

+ 1 - 1
frontend/src/pages/xray/OutboundsTab.tsx

@@ -375,7 +375,7 @@ export default function OutboundsTab({
       },
     ],
     // eslint-disable-next-line react-hooks/exhaustive-deps
-    [t, testMode, rows.length, outboundTestStates, outboundsTraffic],
+    [t, testMode, rows, outboundTestStates, outboundsTraffic],
   );
 
   return (

+ 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(),

+ 106 - 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({
@@ -300,3 +310,99 @@ describe('outbound-form-adapter: round-trip', () => {
     expect(form.protocol).toBe('vless');
   });
 });
+
+describe('outbound-form-adapter: xhttp xmux toggle', () => {
+  const xmuxWire = {
+    protocol: 'vless',
+    tag: 'out-xhttp',
+    settings: {
+      address: 's', port: 443, id: '11111111-2222-4333-8444-555555555555',
+      flow: '', encryption: 'none',
+    },
+    streamSettings: {
+      network: 'xhttp',
+      security: 'none',
+      xhttpSettings: {
+        path: '/', host: '', mode: '',
+        xPaddingBytes: '100-1000', scMaxEachPostBytes: '1000000',
+        xmux: { maxConcurrency: '11', maxConnections: '1', hMaxRequestTimes: '1', hMaxReusableSecs: '1' },
+      },
+    },
+  };
+
+  it('derives enableXmux from a saved xmux object and backfills missing knobs', () => {
+    const form = rawOutboundToFormValues(xmuxWire);
+    const stream = form.streamSettings as Record<string, unknown>;
+    const xhttp = stream.xhttpSettings as Record<string, unknown>;
+    expect(xhttp.enableXmux).toBe(true);
+    expect(xhttp.xmux).toMatchObject({
+      maxConcurrency: '11',
+      maxConnections: '1',
+      hMaxRequestTimes: '1',
+      hMaxReusableSecs: '1',
+      cMaxReuseTimes: 0,
+      hKeepAlivePeriod: 0,
+    });
+  });
+
+  it('round-trips xmux on save and strips the UI-only enableXmux flag', () => {
+    const back = formValuesToWirePayload(rawOutboundToFormValues(xmuxWire));
+    const xhttp = (back.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
+    expect(xhttp).not.toHaveProperty('enableXmux');
+    expect(xhttp.xmux).toMatchObject({ maxConcurrency: '11', maxConnections: '1' });
+  });
+
+  it('drops xmux on save when the toggle is off', () => {
+    const form = rawOutboundToFormValues(xmuxWire);
+    const xhttp = (form.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
+    xhttp.enableXmux = false;
+    const back = formValuesToWirePayload(form);
+    const wireXhttp = (back.streamSettings as Record<string, unknown>).xhttpSettings as Record<string, unknown>;
+    expect(wireXhttp).not.toHaveProperty('xmux');
+  });
+});
+
+describe('outbound-form-adapter: full optional-block round-trip', () => {
+  const wire = {
+    protocol: 'vless',
+    settings: {
+      address: '1', port: 443, id: '1', flow: '', encryption: 'none',
+      reverse: {
+        tag: '1',
+        sniffing: {
+          enabled: true,
+          destOverride: ['http', 'tls', 'quic', 'fakedns'],
+          metadataOnly: true,
+          routeOnly: true,
+          ipsExcluded: ['1'],
+          domainsExcluded: ['1'],
+        },
+      },
+    },
+    tag: '1',
+    streamSettings: {
+      network: 'tcp',
+      tcpSettings: { header: { type: 'http', request: { version: '1.1', method: 'GET', path: ['/'], headers: { '1': ['1'] } }, response: { version: '1.1', status: '200', reason: 'OK', headers: { '1': ['1'] } } } },
+      security: 'none',
+      sockopt: { tcpFastOpen: true, customSockopt: [{ type: 'int', level: '6', opt: '1', value: '1' }] },
+      finalmask: { tcp: [{ type: 'fragment', settings: { packets: '1-3', length: '1', delay: '1', maxSplit: '1' } }] },
+    },
+    sendThrough: '1',
+    mux: { enabled: true, concurrency: 8, xudpConcurrency: 16, xudpProxyUDP443: 'reject' },
+  };
+
+  it('preserves sockopt, finalmask, mux, and reverse excludes', () => {
+    const back = formValuesToWirePayload(rawOutboundToFormValues(wire));
+    const settings = back.settings as Record<string, unknown>;
+    const sniffing = (settings.reverse as Record<string, unknown>).sniffing as Record<string, unknown>;
+    expect(sniffing.ipsExcluded).toEqual(['1']);
+    expect(sniffing.domainsExcluded).toEqual(['1']);
+
+    const stream = back.streamSettings as Record<string, unknown>;
+    expect(stream.sockopt).toMatchObject({ tcpFastOpen: true });
+    expect((stream.sockopt as Record<string, unknown>).customSockopt).toHaveLength(1);
+    expect(stream.finalmask).toMatchObject({ tcp: [{ type: 'fragment' }] });
+
+    expect(back.mux).toMatchObject({ enabled: true });
+  });
+});

+ 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)"
       },