Преглед изворни кода

feat(tls,reality): port xray TLS/REALITY fields, cert-hash helpers, fallback UX

TLS: add verifyPeerCertByName (vcn) to inbound settings + emit in both share-link generators (frontend + Go sub) and outbound parser; the allowInsecure replacement xray removed after 2026-06-01. Add server-side curvePreferences, masterKeyLog, echSockopt (passthrough + form) at tlsSettings top-level so they survive the panel-only settings strip.

REALITY: add limitFallbackUpload/Download (afterBytes/bytesPerSec/burstBytesPerSec) with per-field tooltips, plus masterKeyLog. Verified field names/semantics against pinned xray v1.260327.1 (bytesPerSec=0 disables).

Hosts: fix verify_peer_cert_by_name column bool->string (xray expects comma-separated names) with an idempotent, history-gate-free migration (SQLite typeof blank; Postgres ALTER once); emit vcn for hosts/external proxies.

Server: add getCertHash (local cert DER SHA-256) and getRemoteCertHash (xray tls ping) endpoints + api-docs; wire pinned-cert field buttons. Drop the meaningless random-hash button.

Xray UI: metrics endpoint (listen/tag) config in Basics; import/export for routing rules and outbounds.

Fallbacks card: compact empty state, header-aligned actions, responsive labeled grid rows.

i18n: add all new keys to every locale; drop unused generateRandomPin.
MHSanaei пре 19 часа
родитељ
комит
7c8889466b
48 измењених фајлова са 1316 додато и 173 уклоњено
  1. 98 6
      frontend/public/openapi.json
  2. 1 1
      frontend/src/generated/examples.ts
  3. 1 1
      frontend/src/generated/schemas.ts
  4. 1 1
      frontend/src/generated/types.ts
  5. 1 1
      frontend/src/generated/zod.ts
  6. 2 0
      frontend/src/lib/hosts/host-link.ts
  7. 18 0
      frontend/src/lib/xray/inbound-link.ts
  8. 2 1
      frontend/src/lib/xray/outbound-link-parser.ts
  9. 14 0
      frontend/src/lib/xray/stream-wire-normalize.ts
  10. 21 0
      frontend/src/pages/api-docs/endpoints.ts
  11. 3 3
      frontend/src/pages/hosts/HostFormModal.tsx
  12. 110 91
      frontend/src/pages/inbounds/form/FallbacksCard.tsx
  13. 4 2
      frontend/src/pages/inbounds/form/InboundFormModal.tsx
  14. 49 1
      frontend/src/pages/inbounds/form/security/reality.tsx
  15. 112 15
      frontend/src/pages/inbounds/form/security/tls.tsx
  16. 62 12
      frontend/src/pages/inbounds/form/useSecurityActions.ts
  17. 45 0
      frontend/src/pages/xray/basics/BasicsTab.tsx
  18. 44 1
      frontend/src/pages/xray/outbounds/OutboundsTab.tsx
  19. 71 5
      frontend/src/pages/xray/routing/RoutingTab.tsx
  20. 10 2
      frontend/src/schemas/api/host.ts
  21. 16 0
      frontend/src/schemas/protocols/security/reality.ts
  22. 14 1
      frontend/src/schemas/protocols/security/tls.ts
  23. 1 0
      frontend/src/schemas/protocols/stream/external-proxy.ts
  24. 5 0
      frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap
  25. 5 0
      frontend/src/test/__snapshots__/inbound-full.test.ts.snap
  26. 1 0
      frontend/src/test/__snapshots__/security.test.ts.snap
  27. 2 0
      frontend/src/test/host-link.test.ts
  28. 2 1
      frontend/src/test/inbound-form-blocks.test.tsx
  29. 38 0
      internal/database/db.go
  30. 1 1
      internal/database/model/model.go
  31. 3 0
      internal/sub/host_sub.go
  32. 3 0
      internal/sub/json_service.go
  33. 42 0
      internal/sub/service.go
  34. 24 0
      internal/web/controller/server.go
  35. 99 0
      internal/web/service/server.go
  36. 33 5
      internal/web/translation/ar-EG.json
  37. 29 1
      internal/web/translation/en-US.json
  38. 29 1
      internal/web/translation/es-ES.json
  39. 29 1
      internal/web/translation/fa-IR.json
  40. 33 5
      internal/web/translation/id-ID.json
  41. 29 1
      internal/web/translation/ja-JP.json
  42. 29 1
      internal/web/translation/pt-BR.json
  43. 29 1
      internal/web/translation/ru-RU.json
  44. 33 5
      internal/web/translation/tr-TR.json
  45. 29 1
      internal/web/translation/uk-UA.json
  46. 29 1
      internal/web/translation/vi-VN.json
  47. 31 3
      internal/web/translation/zh-CN.json
  48. 29 1
      internal/web/translation/zh-TW.json

+ 98 - 6
frontend/public/openapi.json

@@ -1474,7 +1474,7 @@
             "type": "integer"
           },
           "verifyPeerCertByName": {
-            "type": "boolean"
+            "type": "string"
           },
           "vlessRoute": {
             "description": "VlessRoute is a free-form port/range routing spec (e.g. \"53,443,1000-2000\");\nstored verbatim, format-validated on the frontend.",
@@ -4408,6 +4408,98 @@
         }
       }
     },
+    "/panel/api/server/getCertHash": {
+      "post": {
+        "tags": [
+          "Server"
+        ],
+        "summary": "Compute the hex SHA-256 of a certificate (DER) for pinning (pinnedPeerCertSha256). Provide either a server file path or inline PEM/DER content.",
+        "operationId": "post_panel_api_server_getCertHash",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "success": {
+                      "type": "boolean"
+                    },
+                    "msg": {
+                      "type": "string"
+                    },
+                    "obj": {}
+                  }
+                },
+                "example": {
+                  "success": true,
+                  "obj": [
+                    "e8e2d3..."
+                  ]
+                }
+              }
+            }
+          }
+        }
+      }
+    },
+    "/panel/api/server/getRemoteCertHash": {
+      "post": {
+        "tags": [
+          "Server"
+        ],
+        "summary": "Run `xray tls ping` against a remote server and return its live leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).",
+        "operationId": "post_panel_api_server_getRemoteCertHash",
+        "requestBody": {
+          "required": true,
+          "content": {
+            "application/json": {
+              "schema": {
+                "type": "object"
+              }
+            }
+          }
+        },
+        "responses": {
+          "200": {
+            "description": "Successful response",
+            "content": {
+              "application/json": {
+                "schema": {
+                  "type": "object",
+                  "properties": {
+                    "success": {
+                      "type": "boolean"
+                    },
+                    "msg": {
+                      "type": "string"
+                    },
+                    "obj": {}
+                  }
+                },
+                "example": {
+                  "success": true,
+                  "obj": [
+                    "e8e2d3..."
+                  ]
+                }
+              }
+            }
+          }
+        }
+      }
+    },
     "/panel/api/server/clientIps": {
       "get": {
         "tags": [
@@ -7259,7 +7351,7 @@
                         ""
                       ],
                       "updatedAt": 0,
-                      "verifyPeerCertByName": false,
+                      "verifyPeerCertByName": "",
                       "vlessRoute": ""
                     }
                   ]
@@ -7351,7 +7443,7 @@
                       ""
                     ],
                     "updatedAt": 0,
-                    "verifyPeerCertByName": false,
+                    "verifyPeerCertByName": "",
                     "vlessRoute": ""
                   }
                 }
@@ -7446,7 +7538,7 @@
                         ""
                       ],
                       "updatedAt": 0,
-                      "verifyPeerCertByName": false,
+                      "verifyPeerCertByName": "",
                       "vlessRoute": ""
                     }
                   ]
@@ -7586,7 +7678,7 @@
                       ""
                     ],
                     "updatedAt": 0,
-                    "verifyPeerCertByName": false,
+                    "verifyPeerCertByName": "",
                     "vlessRoute": ""
                   }
                 }
@@ -7698,7 +7790,7 @@
                       ""
                     ],
                     "updatedAt": 0,
-                    "verifyPeerCertByName": false,
+                    "verifyPeerCertByName": "",
                     "vlessRoute": ""
                   }
                 }

+ 1 - 1
frontend/src/generated/examples.ts

@@ -317,7 +317,7 @@ export const EXAMPLES: Record<string, unknown> = {
       ""
     ],
     "updatedAt": 0,
-    "verifyPeerCertByName": false,
+    "verifyPeerCertByName": "",
     "vlessRoute": ""
   },
   "Inbound": {

+ 1 - 1
frontend/src/generated/schemas.ts

@@ -1448,7 +1448,7 @@ export const SCHEMAS: Record<string, unknown> = {
         "type": "integer"
       },
       "verifyPeerCertByName": {
-        "type": "boolean"
+        "type": "string"
       },
       "vlessRoute": {
         "description": "VlessRoute is a free-form port/range routing spec (e.g. \"53,443,1000-2000\");\nstored verbatim, format-validated on the frontend.",

+ 1 - 1
frontend/src/generated/types.ts

@@ -324,7 +324,7 @@ export interface Host {
   sortOrder: number;
   tags: string[];
   updatedAt: number;
-  verifyPeerCertByName: boolean;
+  verifyPeerCertByName: string;
   vlessRoute: string;
 }
 

+ 1 - 1
frontend/src/generated/zod.ts

@@ -347,7 +347,7 @@ export const HostSchema = z.object({
   sortOrder: z.number().int(),
   tags: z.array(z.string()),
   updatedAt: z.number().int(),
-  verifyPeerCertByName: z.boolean(),
+  verifyPeerCertByName: z.string(),
   vlessRoute: z.string(),
 });
 export type Host = z.infer<typeof HostSchema>;

+ 2 - 0
frontend/src/lib/hosts/host-link.ts

@@ -13,6 +13,7 @@ export type HostLinkInput = Pick<
   | 'alpn'
   | 'fingerprint'
   | 'pinnedPeerCertSha256'
+  | 'verifyPeerCertByName'
   | 'echConfigList'
   | 'overrideSniFromAddress'
   | 'keepSniBlank'
@@ -45,6 +46,7 @@ export function hostToExternalProxyEntry(host: HostLinkInput): ExternalProxyEntr
     alpn: host.alpn && host.alpn.length > 0 ? host.alpn : undefined,
     pinnedPeerCertSha256:
       host.pinnedPeerCertSha256 && host.pinnedPeerCertSha256.length > 0 ? host.pinnedPeerCertSha256 : undefined,
+    verifyPeerCertByName: host.verifyPeerCertByName || undefined,
     echConfigList: host.echConfigList || undefined,
   };
 }

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

@@ -157,6 +157,9 @@ function applyExternalProxyTLSObj(
   if (alpn.length > 0) obj.alpn = alpn;
   const pins = externalProxyPins(externalProxy.pinnedPeerCertSha256);
   if (pins.length > 0) obj.pcs = pins;
+  if (externalProxy.verifyPeerCertByName && externalProxy.verifyPeerCertByName.length > 0) {
+    obj.vcn = externalProxy.verifyPeerCertByName;
+  }
   if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) obj.ech = externalProxy.echConfigList;
 }
 
@@ -254,6 +257,9 @@ export function genVmessLink(input: GenVmessLinkInput): string {
     if (tlsSettings.settings.fingerprint.length > 0) obj.fp = tlsSettings.settings.fingerprint;
     if (tlsSettings.alpn.length > 0) obj.alpn = tlsSettings.alpn.join(',');
     if (tlsSettings.settings.echConfigList.length > 0) obj.ech = tlsSettings.settings.echConfigList;
+    if (tlsSettings.settings.verifyPeerCertByName.length > 0) {
+      obj.vcn = tlsSettings.settings.verifyPeerCertByName;
+    }
     if (tlsSettings.settings.pinnedPeerCertSha256.length > 0) {
       obj.pcs = tlsSettings.settings.pinnedPeerCertSha256.join(',');
     }
@@ -301,6 +307,9 @@ function applyExternalProxyTLSParams(
   if (alpn.length > 0) params.set('alpn', alpn);
   const pins = externalProxyPins(externalProxy.pinnedPeerCertSha256);
   if (pins.length > 0) params.set('pcs', pins);
+  if (externalProxy.verifyPeerCertByName && externalProxy.verifyPeerCertByName.length > 0) {
+    params.set('vcn', externalProxy.verifyPeerCertByName);
+  }
   if (externalProxy.echConfigList && externalProxy.echConfigList.length > 0) params.set('ech', externalProxy.echConfigList);
 }
 
@@ -384,6 +393,9 @@ export function genVlessLink(input: GenVlessLinkInput): string {
       params.set('alpn', tls.alpn.join(','));
       if (tls.serverName.length > 0) params.set('sni', tls.serverName);
       if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
+      if (tls.settings.verifyPeerCertByName.length > 0) {
+        params.set('vcn', tls.settings.verifyPeerCertByName);
+      }
       if (tls.settings.pinnedPeerCertSha256.length > 0) {
         params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
       }
@@ -476,6 +488,9 @@ function writeTlsParams(stream: NonNullable<Inbound['streamSettings']>, params:
   params.set('alpn', tls.alpn.join(','));
   if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
   if (tls.serverName.length > 0) params.set('sni', tls.serverName);
+  if (tls.settings.verifyPeerCertByName.length > 0) {
+    params.set('vcn', tls.settings.verifyPeerCertByName);
+  }
   if (tls.settings.pinnedPeerCertSha256.length > 0) {
     params.set('pcs', tls.settings.pinnedPeerCertSha256.join(','));
   }
@@ -701,6 +716,9 @@ export function genHysteriaLink(input: GenHysteriaLinkInput): string {
   if (tls.alpn.length > 0) params.set('alpn', tls.alpn.join(','));
   if (tls.settings.echConfigList.length > 0) params.set('ech', tls.settings.echConfigList);
   if (tls.serverName.length > 0) params.set('sni', tls.serverName);
+  if (tls.settings.verifyPeerCertByName.length > 0) {
+    params.set('vcn', tls.settings.verifyPeerCertByName);
+  }
   if (tls.settings.pinnedPeerCertSha256.length > 0) {
     params.set('pinSHA256', tls.settings.pinnedPeerCertSha256.map(hysteriaPinHex).join(','));
   }

+ 2 - 1
frontend/src/lib/xray/outbound-link-parser.ts

@@ -213,6 +213,7 @@ function applySecurityParams(stream: Raw, params: URLSearchParams): void {
     const alpn = params.get('alpn');
     if (alpn) tls.alpn = alpn.split(',');
     tls.echConfigList = params.get('ech') ?? '';
+    tls.verifyPeerCertByName = params.get('vcn') ?? '';
     tls.pinnedPeerCertSha256 = params.get('pcs') ?? '';
   } else if (stream.security === 'reality') {
     const reality = stream.realitySettings as Raw;
@@ -434,7 +435,7 @@ export function parseHysteria2Link(link: string): Raw | null {
       alpn: alpn ? alpn.split(',') : ['h3'],
       fingerprint: params.get('fp') ?? '',
       echConfigList: params.get('ech') ?? '',
-      verifyPeerCertByName: '',
+      verifyPeerCertByName: params.get('vcn') ?? '',
       pinnedPeerCertSha256: params.get('pinSHA256') ?? '',
     },
   };

+ 14 - 0
frontend/src/lib/xray/stream-wire-normalize.ts

@@ -129,6 +129,20 @@ function normalizeTlsForWire(raw: Record<string, unknown>): Record<string, unkno
   const out: Record<string, unknown> = { ...raw };
   if (out.fingerprint === '') delete out.fingerprint;
 
+  // Empty server-side tuning fields mean "use xray-core's default" — never emit them.
+  if (Array.isArray(out.curvePreferences) && out.curvePreferences.length === 0) {
+    delete out.curvePreferences;
+  }
+  if (out.masterKeyLog === '' || out.masterKeyLog == null) delete out.masterKeyLog;
+  if (isRecord(out.echSockopt)) {
+    const echSock = normalizeSockoptForWire(out.echSockopt);
+    if (echSock) {
+      out.echSockopt = echSock;
+    } else {
+      delete out.echSockopt;
+    }
+  }
+
   const settings = out.settings;
   if (isRecord(settings)) {
     const settingsOut: Record<string, unknown> = { ...settings };

+ 21 - 0
frontend/src/pages/api-docs/endpoints.ts

@@ -453,6 +453,27 @@ export const sections: readonly Section[] = [
         body: 'sni=example.com',
         response: '{\n  "success": true,\n  "obj": {\n    "echKeySet": "...",\n    "echServerKeys": [...],\n    "echConfigList": "..."\n  }\n}',
       },
+      {
+        method: 'POST',
+        path: '/panel/api/server/getCertHash',
+        summary: 'Compute the hex SHA-256 of a certificate (DER) for pinning (pinnedPeerCertSha256). Provide either a server file path or inline PEM/DER content.',
+        params: [
+          { name: 'certFile', in: 'body (form)', type: 'string', desc: 'Path to a certificate file on the server. Takes precedence over certContent.' },
+          { name: 'certContent', in: 'body (form)', type: 'string', desc: 'Inline PEM (or DER) certificate content, used when certFile is empty.' },
+        ],
+        body: 'certFile=/root/cert.crt',
+        response: '{\n  "success": true,\n  "obj": [\n    "e8e2d3..."\n  ]\n}',
+      },
+      {
+        method: 'POST',
+        path: '/panel/api/server/getRemoteCertHash',
+        summary: 'Run `xray tls ping` against a remote server and return its live leaf-certificate SHA-256 hash(es) for pinning (pinnedPeerCertSha256).',
+        params: [
+          { name: 'server', in: 'body (form)', type: 'string', desc: 'Remote server as domain or domain:port (default port 443), e.g. cloudflare-dns.com.' },
+        ],
+        body: 'server=cloudflare-dns.com',
+        response: '{\n  "success": true,\n  "obj": [\n    "e8e2d3..."\n  ]\n}',
+      },
       {
         method: 'GET',
         path: '/panel/api/server/clientIps',

+ 3 - 3
frontend/src/pages/hosts/HostFormModal.tsx

@@ -56,7 +56,7 @@ function defaultsFor(host: HostRecord | null): FormShape {
     overrideSniFromAddress: host?.overrideSniFromAddress ?? false,
     keepSniBlank: host?.keepSniBlank ?? false,
     pinnedPeerCertSha256: host?.pinnedPeerCertSha256 ?? [],
-    verifyPeerCertByName: host?.verifyPeerCertByName ?? false,
+    verifyPeerCertByName: (host?.verifyPeerCertByName as string | undefined) ?? '',
     allowInsecure: host?.allowInsecure ?? false,
     echConfigList: host?.echConfigList ?? '',
     muxParams: asString(host?.muxParams),
@@ -224,8 +224,8 @@ export default function HostFormModal({ open, mode, host, inboundOptions, save,
                       <Form.Item name="pinnedPeerCertSha256" label={t('pages.hosts.fields.pins')}>
                         <Select mode="tags" allowClear tokenSeparators={[',']} />
                       </Form.Item>
-                      <Form.Item name="verifyPeerCertByName" label={t('pages.hosts.fields.verifyPeerCertByName')} valuePropName="checked">
-                        <Switch />
+                      <Form.Item name="verifyPeerCertByName" label={t('pages.hosts.fields.verifyPeerCertByName')} tooltip={t('pages.inbounds.form.verifyPeerCertByNameTip')}>
+                        <Input placeholder="example.com" />
                       </Form.Item>
                       <Form.Item name="allowInsecure" label={t('pages.hosts.fields.allowInsecure')} tooltip={t('pages.hosts.hints.allowInsecure')} valuePropName="checked">
                         <Switch />

+ 110 - 91
frontend/src/pages/inbounds/form/FallbacksCard.tsx

@@ -1,8 +1,7 @@
 import { useTranslation } from 'react-i18next';
-import { Button, Card, Empty, Input, InputNumber, Select, Space } from 'antd';
+import { Button, Card, Col, Empty, Input, InputNumber, Row, Select, Space } from 'antd';
 import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons';
 
-import { InputAddon } from '@/components/ui';
 import type { FallbackRow } from '@/schemas/forms/inbound-form';
 
 interface FallbacksCardProps {
@@ -25,100 +24,120 @@ export default function FallbacksCard({
   addAllFallbacks,
 }: FallbacksCardProps) {
   const { t } = useTranslation();
+
+  const addButtons = (
+    <Space size={8} wrap>
+      <Button type="primary" ghost size="small" icon={<PlusOutlined />} onClick={addFallback}>
+        {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
+      </Button>
+      <Button
+        size="small"
+        onClick={addAllFallbacks}
+        disabled={fallbackChildOptions.length === 0 || fallbacks.length >= fallbackChildOptions.length}
+        title={t('pages.inbounds.form.addAllFallbackTooltip')}
+      >
+        {t('pages.inbounds.form.addAll')}
+      </Button>
+    </Space>
+  );
+
   return (
-    <Card size="small" className="mt-12" title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}>
-      {fallbacks.length === 0 && (
+    <Card
+      size="small"
+      className="mt-12"
+      title={t('pages.inbounds.fallbacks.title') || 'Fallbacks'}
+      extra={addButtons}
+    >
+      {fallbacks.length === 0 ? (
         <Empty
+          image={Empty.PRESENTED_IMAGE_SIMPLE}
+          styles={{ image: { height: 36 } }}
           description={t('pages.inbounds.fallbacks.empty') || 'No fallbacks yet'}
-          styles={{ image: { height: 40 } }}
-          style={{ margin: '8px 0 12px' }}
+          style={{ margin: '4px 0 12px' }}
         />
+      ) : (
+        fallbacks.map((record, idx) => (
+          <Card
+            key={record.rowKey}
+            type="inner"
+            size="small"
+            style={{ marginBottom: 8 }}
+            styles={{ body: { padding: 12 } }}
+          >
+            <Space.Compact block style={{ marginBottom: 8 }}>
+              <Select
+                value={record.childId}
+                options={fallbackChildOptions}
+                placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
+                allowClear
+                showSearch={{
+                  filterOption: (input, option) =>
+                    ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
+                }}
+                style={{ width: '100%' }}
+                onChange={(v) => updateFallback(record.rowKey, { childId: v ?? null })}
+              />
+              <Button
+                disabled={idx === 0}
+                onClick={() => moveFallback(idx, -1)}
+                title={t('pages.inbounds.form.moveUp')}
+                icon={<ArrowUpOutlined />}
+              />
+              <Button
+                disabled={idx === fallbacks.length - 1}
+                onClick={() => moveFallback(idx, 1)}
+                title={t('pages.inbounds.form.moveDown')}
+                icon={<ArrowDownOutlined />}
+              />
+              <Button danger onClick={() => removeFallback(idx)} icon={<DeleteOutlined />} />
+            </Space.Compact>
+            <Row gutter={[8, 8]}>
+              <Col xs={24} sm={12}>
+                <Input
+                  addonBefore="SNI"
+                  placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
+                  value={record.name}
+                  onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
+                />
+              </Col>
+              <Col xs={24} sm={12}>
+                <Input
+                  addonBefore="ALPN"
+                  placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
+                  value={record.alpn}
+                  onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
+                />
+              </Col>
+              <Col xs={24} sm={12}>
+                <Input
+                  addonBefore="Path"
+                  placeholder="/"
+                  value={record.path}
+                  onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
+                />
+              </Col>
+              <Col xs={24} sm={12}>
+                <Input
+                  addonBefore="Dest"
+                  placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
+                  value={record.dest}
+                  onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
+                />
+              </Col>
+              <Col xs={24} sm={12}>
+                <InputNumber
+                  addonBefore="xver"
+                  min={0}
+                  max={2}
+                  style={{ width: '100%' }}
+                  value={record.xver}
+                  onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
+                />
+              </Col>
+            </Row>
+          </Card>
+        ))
       )}
-      {fallbacks.map((record, idx) => (
-        <div
-          key={record.rowKey}
-          style={{ border: '1px solid var(--app-border-tertiary)', borderRadius: 6, padding: '10px 12px', marginBottom: 8 }}
-        >
-          <Space.Compact block style={{ marginBottom: 6 }}>
-            <Select
-              value={record.childId}
-              options={fallbackChildOptions}
-              placeholder={t('pages.inbounds.fallbacks.pickInbound') || 'Pick an inbound'}
-              allowClear
-              showSearch={{
-                filterOption: (input, option) =>
-                  ((option?.label as string) || '').toLowerCase().includes(input.toLowerCase()),
-              }}
-              style={{ width: '100%' }}
-              onChange={(v) => updateFallback(record.rowKey, { childId: v ?? null })}
-            />
-            <Button
-              disabled={idx === 0}
-              onClick={() => moveFallback(idx, -1)}
-              title={t('pages.inbounds.form.moveUp')}
-            >
-              <ArrowUpOutlined />
-            </Button>
-            <Button
-              disabled={idx === fallbacks.length - 1}
-              onClick={() => moveFallback(idx, 1)}
-              title={t('pages.inbounds.form.moveDown')}
-            >
-              <ArrowDownOutlined />
-            </Button>
-            <Button danger onClick={() => removeFallback(idx)}>
-              <DeleteOutlined />
-            </Button>
-          </Space.Compact>
-          <Space.Compact block>
-            <InputAddon>SNI</InputAddon>
-            <Input
-              placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
-              value={record.name}
-              onChange={(e) => updateFallback(record.rowKey, { name: e.target.value })}
-            />
-            <InputAddon>ALPN</InputAddon>
-            <Input
-              placeholder={t('pages.inbounds.fallbacks.matchAny') || 'any'}
-              value={record.alpn}
-              onChange={(e) => updateFallback(record.rowKey, { alpn: e.target.value })}
-            />
-            <InputAddon>Path</InputAddon>
-            <Input
-              placeholder="/"
-              value={record.path}
-              onChange={(e) => updateFallback(record.rowKey, { path: e.target.value })}
-            />
-            <InputAddon>Dest</InputAddon>
-            <Input
-              placeholder={t('pages.inbounds.fallbacks.destPlaceholder') || 'auto'}
-              value={record.dest}
-              onChange={(e) => updateFallback(record.rowKey, { dest: e.target.value })}
-            />
-            <InputAddon>xver</InputAddon>
-            <InputNumber
-              min={0}
-              max={2}
-              value={record.xver}
-              onChange={(v) => updateFallback(record.rowKey, { xver: Number(v) || 0 })}
-            />
-          </Space.Compact>
-        </div>
-      ))}
-      <Space>
-        <Button size="small" onClick={addFallback}>
-          <PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
-        </Button>
-        <Button
-          size="small"
-          onClick={addAllFallbacks}
-          disabled={fallbackChildOptions.length === 0
-            || fallbacks.length >= fallbackChildOptions.length}
-          title={t('pages.inbounds.form.addAllFallbackTooltip')}
-        >
-          {t('pages.inbounds.form.addAll')}
-        </Button>
-      </Space>
     </Card>
   );
 }

+ 4 - 2
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -244,7 +244,8 @@ export default function InboundFormModal({
     randomizeShortIds,
     getNewEchCert,
     clearEchCert,
-    generateRandomPinHash,
+    pinFromCert,
+    pinFromRemote,
     setCertFromPanel,
     clearCertFiles,
     onSecurityChange,
@@ -854,7 +855,8 @@ export default function InboundFormModal({
           saving={saving}
           setCertFromPanel={setCertFromPanel}
           clearCertFiles={clearCertFiles}
-          generateRandomPinHash={generateRandomPinHash}
+          pinFromCert={pinFromCert}
+          pinFromRemote={pinFromRemote}
           getNewEchCert={getNewEchCert}
           clearEchCert={clearEchCert}
         />

+ 49 - 1
frontend/src/pages/inbounds/form/security/reality.tsx

@@ -1,5 +1,5 @@
 import { useTranslation } from 'react-i18next';
-import { Button, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
+import { Button, Collapse, Divider, Form, Input, InputNumber, Select, Space, Switch } from 'antd';
 import { ReloadOutlined } from '@ant-design/icons';
 
 import { UTLS_FINGERPRINT } from '@/schemas/primitives';
@@ -153,6 +153,54 @@ export default function RealityForm({
           <Button danger onClick={clearMldsa65}>{t('clear')}</Button>
         </Space>
       </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'realitySettings', 'masterKeyLog']}
+        label={t('pages.inbounds.form.masterKeyLog')}
+        tooltip={t('pages.inbounds.form.masterKeyLogTip')}
+      >
+        <Input placeholder="/path/to/sslkeylog.txt" />
+      </Form.Item>
+      <Collapse
+        style={{ marginBottom: 14 }}
+        items={[
+          {
+            key: 'limitFallback',
+            label: t('pages.inbounds.form.limitFallback'),
+            children: (
+              <>
+                {(['limitFallbackUpload', 'limitFallbackDownload'] as const).map((dir) => (
+                  <div key={dir}>
+                    <Divider style={{ margin: '0 0 14px 0' }}>
+                      {t(`pages.inbounds.form.${dir}`)}
+                    </Divider>
+                    <Form.Item
+                      name={['streamSettings', 'realitySettings', dir, 'afterBytes']}
+                      label={t('pages.inbounds.form.afterBytes')}
+                      tooltip={t('pages.inbounds.form.afterBytesTip')}
+                    >
+                      <InputNumber min={0} />
+                    </Form.Item>
+                    <Form.Item
+                      name={['streamSettings', 'realitySettings', dir, 'bytesPerSec']}
+                      label={t('pages.inbounds.form.bytesPerSec')}
+                      tooltip={t('pages.inbounds.form.bytesPerSecTip')}
+                    >
+                      <InputNumber min={0} />
+                    </Form.Item>
+                    <Form.Item
+                      name={['streamSettings', 'realitySettings', dir, 'burstBytesPerSec']}
+                      label={t('pages.inbounds.form.burstBytesPerSec')}
+                      tooltip={t('pages.inbounds.form.burstBytesPerSecTip')}
+                    >
+                      <InputNumber min={0} />
+                    </Form.Item>
+                  </div>
+                ))}
+              </>
+            ),
+          },
+        ]}
+      />
     </>
   );
 }

+ 112 - 15
frontend/src/pages/inbounds/form/security/tls.tsx

@@ -1,14 +1,16 @@
 import { useTranslation } from 'react-i18next';
 import { Button, Form, Input, InputNumber, Radio, Select, Space, Switch } from 'antd';
-import { MinusOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
+import { CloudDownloadOutlined, FileProtectOutlined, MinusOutlined, PlusOutlined } from '@ant-design/icons';
 
 import {
   ALPN_OPTION,
+  DOMAIN_STRATEGY_OPTION,
   TLS_CIPHER_OPTION,
   TLS_VERSION_OPTION,
   USAGE_OPTION,
   UTLS_FINGERPRINT,
 } from '@/schemas/primitives';
+import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
 
 const { TextArea } = Input;
 
@@ -16,7 +18,8 @@ interface TlsFormProps {
   saving: boolean;
   setCertFromPanel: (certName: number) => void;
   clearCertFiles: (certName: number) => void;
-  generateRandomPinHash: () => void;
+  pinFromCert: () => void;
+  pinFromRemote: () => void;
   getNewEchCert: () => void;
   clearEchCert: () => void;
 }
@@ -25,7 +28,8 @@ export default function TlsForm({
   saving,
   setCertFromPanel,
   clearCertFiles,
-  generateRandomPinHash,
+  pinFromCert,
+  pinFromRemote,
   getNewEchCert,
   clearEchCert,
 }: TlsFormProps) {
@@ -78,6 +82,21 @@ export default function TlsForm({
           options={Object.values(ALPN_OPTION).map((a) => ({ value: a, label: a }))}
         />
       </Form.Item>
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'curvePreferences']}
+        label={t('pages.inbounds.form.curvePreferences')}
+        tooltip={t('pages.inbounds.form.curvePreferencesTip')}
+      >
+        <Select
+          mode="tags"
+          tokenSeparators={[',', ' ']}
+          style={{ width: '100%' }}
+          options={['X25519MLKEM768', 'X25519', 'P-256', 'P-384', 'P-521'].map((c) => ({
+            value: c,
+            label: c,
+          }))}
+        />
+      </Form.Item>
       <Form.Item
         name={['streamSettings', 'tlsSettings', 'rejectUnknownSni']}
         label={t('pages.inbounds.form.rejectUnknownSni')}
@@ -270,7 +289,71 @@ export default function TlsForm({
           </>
         )}
       </Form.List>
-
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'masterKeyLog']}
+        label={t('pages.inbounds.form.masterKeyLog')}
+        tooltip={t('pages.inbounds.form.masterKeyLogTip')}
+      >
+        <Input placeholder="/path/to/sslkeylog.txt" />
+      </Form.Item>
+      <Form.Item
+        noStyle
+        shouldUpdate={(prev, curr) =>
+          !!(prev.streamSettings as { tlsSettings?: { echSockopt?: unknown } } | undefined)?.tlsSettings?.echSockopt
+          !== !!(curr.streamSettings as { tlsSettings?: { echSockopt?: unknown } } | undefined)?.tlsSettings?.echSockopt
+        }
+      >
+        {({ getFieldValue, setFieldValue }) => {
+          const on = !!getFieldValue(['streamSettings', 'tlsSettings', 'echSockopt']);
+          return (
+            <>
+              <Form.Item label={t('pages.inbounds.form.echSockopt')} tooltip={t('pages.inbounds.form.echSockoptTip')}>
+                <Switch
+                  checked={on}
+                  onChange={(v) =>
+                    setFieldValue(
+                      ['streamSettings', 'tlsSettings', 'echSockopt'],
+                      v ? SockoptStreamSettingsSchema.parse({}) : undefined,
+                    )
+                  }
+                />
+              </Form.Item>
+              {on && (
+                <>
+                  <Form.Item
+                    name={['streamSettings', 'tlsSettings', 'echSockopt', 'dialerProxy']}
+                    label={t('pages.inbounds.form.dialerProxy')}
+                  >
+                    <Input />
+                  </Form.Item>
+                  <Form.Item
+                    name={['streamSettings', 'tlsSettings', 'echSockopt', 'domainStrategy']}
+                    label={t('pages.xray.wireguard.domainStrategy')}
+                  >
+                    <Select
+                      options={Object.values(DOMAIN_STRATEGY_OPTION).map((v) => ({ value: v, label: v }))}
+                    />
+                  </Form.Item>
+                  <Form.Item
+                    name={['streamSettings', 'tlsSettings', 'echSockopt', 'tcpFastOpen']}
+                    label={t('pages.inbounds.form.tcpFastOpen')}
+                    valuePropName="checked"
+                  >
+                    <Switch />
+                  </Form.Item>
+                  <Form.Item
+                    name={['streamSettings', 'tlsSettings', 'echSockopt', 'tcpMptcp']}
+                    label={t('pages.inbounds.form.multipathTcp')}
+                    valuePropName="checked"
+                  >
+                    <Switch />
+                  </Form.Item>
+                </>
+              )}
+            </>
+          );
+        }}
+      </Form.Item>
       <Form.Item name={['streamSettings', 'tlsSettings', 'echServerKeys']} label={t('pages.inbounds.form.echKey')}>
         <Input />
       </Form.Item>
@@ -280,6 +363,14 @@ export default function TlsForm({
       >
         <Input />
       </Form.Item>
+      <Form.Item label=" ">
+        <Space>
+          <Button type="primary" loading={saving} onClick={getNewEchCert}>
+            {t('pages.inbounds.form.getNewEchCert')}
+          </Button>
+          <Button danger onClick={clearEchCert}>{t('clear')}</Button>
+        </Space>
+      </Form.Item>
       <Form.Item
         label={t('pages.inbounds.form.pinnedPeerCertSha256')}
         tooltip={t('pages.inbounds.form.pinnedPeerCertSha256Tip')}
@@ -293,23 +384,29 @@ export default function TlsForm({
               mode="tags"
               tokenSeparators={[',', ' ']}
               placeholder={t('pages.inbounds.form.pinnedPeerCertSha256Placeholder')}
-              style={{ width: 'calc(100% - 32px)' }}
+              style={{ width: 'calc(100% - 64px)' }}
             />
           </Form.Item>
           <Button
-            icon={<ReloadOutlined />}
-            onClick={generateRandomPinHash}
-            title={t('pages.inbounds.form.generateRandomPin')}
+            icon={<FileProtectOutlined />}
+            onClick={pinFromCert}
+            loading={saving}
+            title={t('pages.inbounds.form.pinFromCert')}
+          />
+          <Button
+            icon={<CloudDownloadOutlined />}
+            onClick={pinFromRemote}
+            loading={saving}
+            title={t('pages.inbounds.form.pinFromRemote')}
           />
         </Space.Compact>
       </Form.Item>
-      <Form.Item label=" ">
-        <Space>
-          <Button type="primary" loading={saving} onClick={getNewEchCert}>
-            {t('pages.inbounds.form.getNewEchCert')}
-          </Button>
-          <Button danger onClick={clearEchCert}>{t('clear')}</Button>
-        </Space>
+      <Form.Item
+        name={['streamSettings', 'tlsSettings', 'settings', 'verifyPeerCertByName']}
+        label={t('pages.inbounds.form.verifyPeerCertByName')}
+        tooltip={t('pages.inbounds.form.verifyPeerCertByNameTip')}
+      >
+        <Input placeholder="example.com" />
       </Form.Item>
     </>
   );

+ 62 - 12
frontend/src/pages/inbounds/form/useSecurityActions.ts

@@ -100,17 +100,66 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
     form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'echConfigList'], '');
   };
 
-  const generateRandomPinHash = () => {
-    const bytes = new Uint8Array(32);
-    crypto.getRandomValues(bytes);
-    const hash = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
-    const current = (form.getFieldValue(
-      ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
-    ) as string[] | undefined) ?? [];
-    form.setFieldValue(
-      ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
-      [...current, hash],
-    );
+  // Fill the pinned-cert field from the inbound's own certificate: read the
+  // first configured cert (file path or inline content) and ask the server for
+  // its hex SHA-256, then merge the hash(es) into pinnedPeerCertSha256.
+  const pinFromCert = async () => {
+    const certs = (form.getFieldValue(['streamSettings', 'tlsSettings', 'certificates']) ?? []) as Array<{
+      certificateFile?: string;
+      certificate?: string[];
+    }>;
+    const first = certs[0];
+    const certFile = first?.certificateFile?.trim() ?? '';
+    const certContent = Array.isArray(first?.certificate) ? first.certificate.join('\n').trim() : '';
+    if (!certFile && !certContent) {
+      messageApi.warning(t('pages.inbounds.setDefaultCertEmpty'));
+      return;
+    }
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.post('/panel/api/server/getCertHash', { certFile, certContent });
+      if (!msg?.success) {
+        messageApi.warning(msg?.msg || t('pages.inbounds.setDefaultCertEmpty'));
+        return;
+      }
+      const hashes = (msg.obj as string[] | undefined) ?? [];
+      if (hashes.length === 0) return;
+      const current = (form.getFieldValue(
+        ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
+      ) as string[] | undefined) ?? [];
+      const merged = Array.from(new Set([...current, ...hashes]));
+      form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'], merged);
+    } finally {
+      setSaving(false);
+    }
+  };
+
+  // Fill the pinned-cert field by pinging the configured SNI: fetches the live
+  // remote certificate hash via `xray tls ping`. Useful when the panel doesn't
+  // hold the cert file (a CDN front / external endpoint).
+  const pinFromRemote = async () => {
+    const server = ((form.getFieldValue(['streamSettings', 'tlsSettings', 'serverName']) as string | undefined) ?? '').trim();
+    if (!server) {
+      messageApi.warning(t('pages.inbounds.form.pinFromRemoteNoSni'));
+      return;
+    }
+    setSaving(true);
+    try {
+      const msg = await HttpUtil.post('/panel/api/server/getRemoteCertHash', { server });
+      if (!msg?.success) {
+        messageApi.warning(msg?.msg || t('pages.inbounds.form.pinFromRemoteFailed'));
+        return;
+      }
+      const hashes = (msg.obj as string[] | undefined) ?? [];
+      if (hashes.length === 0) return;
+      const current = (form.getFieldValue(
+        ['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'],
+      ) as string[] | undefined) ?? [];
+      const merged = Array.from(new Set([...current, ...hashes]));
+      form.setFieldValue(['streamSettings', 'tlsSettings', 'settings', 'pinnedPeerCertSha256'], merged);
+    } finally {
+      setSaving(false);
+    }
   };
 
   const setCertFromPanel = async (certName: number) => {
@@ -194,7 +243,8 @@ export function useSecurityActions({ form, setSaving, messageApi, nodeId }: UseS
     randomizeShortIds,
     getNewEchCert,
     clearEchCert,
-    generateRandomPinHash,
+    pinFromCert,
+    pinFromRemote,
     setCertFromPanel,
     clearCertFiles,
     onSecurityChange,

+ 45 - 0
frontend/src/pages/xray/basics/BasicsTab.tsx

@@ -70,6 +70,28 @@ export default function BasicsTab({
     [mutate],
   );
 
+  const metricsCfg = (templateSettings as { metrics?: { tag?: string; listen?: string } } | null)?.metrics;
+
+  const setMetrics = useCallback(
+    (field: 'tag' | 'listen', value: string) => mutate((tt) => {
+      const node = tt as { metrics?: { tag?: string; listen?: string }; stats?: Record<string, unknown> };
+      const m: { tag?: string; listen?: string } = { ...(node.metrics ?? {}) };
+      if (value.trim() === '') {
+        delete m[field];
+      } else {
+        m[field] = value.trim();
+      }
+      if (!m.listen && !m.tag) {
+        delete node.metrics;
+      } else {
+        node.metrics = m;
+        // xray-core's metrics handler needs a stats object to populate.
+        if (!node.stats) node.stats = {};
+      }
+    }),
+    [mutate],
+  );
+
   function confirmResetDefault() {
     modal.confirm({
       title: t('pages.settings.resetDefaultConfig'),
@@ -272,6 +294,29 @@ export default function BasicsTab({
               }
             />
           ))}
+          <SettingListItem
+            title={t('pages.xray.metricsListen')}
+            description={t('pages.xray.metricsListenDesc')}
+            paddings="small"
+            control={
+              <Input
+                value={metricsCfg?.listen ?? ''}
+                onChange={(e) => setMetrics('listen', e.target.value)}
+                placeholder="127.0.0.1:11111"
+              />
+            }
+          />
+          <SettingListItem
+            title={t('pages.xray.metricsTag')}
+            paddings="small"
+            control={
+              <Input
+                value={metricsCfg?.tag ?? ''}
+                onChange={(e) => setMetrics('tag', e.target.value)}
+                placeholder="metrics_out"
+              />
+            }
+          />
         </>
       ),
     },

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

@@ -34,9 +34,11 @@ import {
   CheckCircleOutlined,
   WarningOutlined,
   ExportOutlined,
+  ImportOutlined,
 } from '@ant-design/icons';
 
-import { HttpUtil } from '@/utils';
+import { FileManager, HttpUtil } from '@/utils';
+import PromptModal from '@/components/feedback/PromptModal';
 
 import OutboundFormModal from './OutboundFormModal';
 import { propagateOutboundTagRename } from '../basics/helpers';
@@ -223,6 +225,35 @@ export default function OutboundsTab({
     });
   }
 
+  const [importOpen, setImportOpen] = useState(false);
+
+  function exportOutbounds() {
+    FileManager.downloadTextFile(JSON.stringify(outbounds, null, 2), 'outbounds.json', {
+      type: 'application/json',
+    });
+  }
+
+  function importOutbounds(value: string) {
+    let parsed: unknown;
+    try {
+      parsed = JSON.parse(value);
+    } catch {
+      messageApi.error(t('pages.xray.importInvalidJson'));
+      return;
+    }
+    const obj = parsed as { outbounds?: unknown };
+    const list = Array.isArray(parsed) ? parsed : Array.isArray(obj?.outbounds) ? obj.outbounds : null;
+    if (!list) {
+      messageApi.error(t('pages.xray.importInvalidJson'));
+      return;
+    }
+    mutate((tt) => {
+      if (!Array.isArray(tt.outbounds)) tt.outbounds = [];
+      tt.outbounds.push(...(list as never[]));
+    });
+    setImportOpen(false);
+  }
+
   // --- Subscription management (minimal inline UI) ---
   async function loadSubs() {
     setSubsLoading(true);
@@ -420,6 +451,9 @@ export default function OutboundsTab({
                   items: [
                     { key: 'warp', icon: <CloudOutlined />, label: 'WARP', onClick: onShowWarp },
                     { key: 'nord', icon: <ApiOutlined />, label: 'NordVPN', onClick: onShowNord },
+                    { type: 'divider' },
+                    { key: 'import', icon: <ImportOutlined />, label: t('pages.xray.importOutbounds'), onClick: () => setImportOpen(true) },
+                    { key: 'export', icon: <ExportOutlined />, label: t('pages.xray.exportOutbounds'), disabled: outbounds.length === 0, onClick: exportOutbounds },
                   ],
                 }}
               >
@@ -488,6 +522,15 @@ export default function OutboundsTab({
           onClose={() => setModalOpen(false)}
           onConfirm={onConfirm}
         />
+        <PromptModal
+          open={importOpen}
+          onClose={() => setImportOpen(false)}
+          title={t('pages.xray.importOutbounds')}
+          okText={t('pages.xray.importOutbounds')}
+          type="textarea"
+          json
+          onConfirm={importOutbounds}
+        />
 
         {/* Subscription outbounds (read-only, merged at runtime) */}
         {Array.isArray(subscriptionOutbounds) && subscriptionOutbounds.length > 0 && (

+ 71 - 5
frontend/src/pages/xray/routing/RoutingTab.tsx

@@ -1,9 +1,18 @@
 import { useCallback, useMemo, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Button, Modal, Space, Table, Tabs } from 'antd';
-import { AimOutlined, ControlOutlined, PlusOutlined, UnorderedListOutlined } from '@ant-design/icons';
+import { Button, Modal, Space, Table, Tabs, message } from 'antd';
+import {
+  AimOutlined,
+  ControlOutlined,
+  ExportOutlined,
+  ImportOutlined,
+  PlusOutlined,
+  UnorderedListOutlined,
+} from '@ant-design/icons';
 
 import { catTabLabel } from '@/pages/settings/catTabLabel';
+import { FileManager } from '@/utils';
+import PromptModal from '@/components/feedback/PromptModal';
 import RoutingBasic from './RoutingBasic';
 import RouteTester from './RouteTester';
 import RuleFormModal from './RuleFormModal';
@@ -134,6 +143,42 @@ export default function RoutingTab({
     return out;
   }, [templateSettings?.routing?.balancers]);
 
+  const [importOpen, setImportOpen] = useState(false);
+
+  function exportRules() {
+    FileManager.downloadTextFile(JSON.stringify(rules, null, 2), 'routing-rules.json', {
+      type: 'application/json',
+    });
+  }
+
+  function importRules(value: string) {
+    let parsed: unknown;
+    try {
+      parsed = JSON.parse(value);
+    } catch {
+      message.error(t('pages.xray.importInvalidJson'));
+      return;
+    }
+    const obj = parsed as { rules?: unknown; routing?: { rules?: unknown } };
+    const list = Array.isArray(parsed)
+      ? parsed
+      : Array.isArray(obj?.rules)
+        ? obj.rules
+        : Array.isArray(obj?.routing?.rules)
+          ? obj.routing!.rules
+          : null;
+    if (!list) {
+      message.error(t('pages.xray.importInvalidJson'));
+      return;
+    }
+    mutate((tt) => {
+      if (!tt.routing) tt.routing = { rules: [] };
+      if (!Array.isArray(tt.routing.rules)) tt.routing.rules = [];
+      tt.routing.rules.push(...(list as RuleObject[]));
+    });
+    setImportOpen(false);
+  }
+
   function openAdd() {
     setEditingRule(null);
     setEditingIndex(null);
@@ -284,9 +329,21 @@ export default function RoutingTab({
             label: catTabLabel(<UnorderedListOutlined />, t('pages.xray.Routings'), isMobile),
             children: (
               <Space orientation="vertical" size="middle" style={{ width: '100%' }}>
-                <Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
-                  {t('pages.xray.Routings')}
-                </Button>
+                <Space wrap>
+                  <Button type="primary" icon={<PlusOutlined />} onClick={openAdd}>
+                    {t('pages.xray.Routings')}
+                  </Button>
+                  <Button icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
+                    {t('pages.xray.importRules')}
+                  </Button>
+                  <Button
+                    icon={<ExportOutlined />}
+                    onClick={exportRules}
+                    disabled={rules.length === 0}
+                  >
+                    {t('pages.xray.exportRules')}
+                  </Button>
+                </Space>
 
                 {isMobile ? (
                   <RuleCardList
@@ -339,6 +396,15 @@ export default function RoutingTab({
         onClose={() => setRuleModalOpen(false)}
         onConfirm={onRuleConfirm}
       />
+      <PromptModal
+        open={importOpen}
+        onClose={() => setImportOpen(false)}
+        title={t('pages.xray.importRules')}
+        okText={t('pages.xray.importRules')}
+        type="textarea"
+        json
+        onConfirm={importRules}
+      />
     </>
   );
 }

+ 10 - 2
frontend/src/schemas/api/host.ts

@@ -46,7 +46,12 @@ export const HostFormSchema = z.object({
   overrideSniFromAddress: z.boolean().default(false),
   keepSniBlank: z.boolean().default(false),
   pinnedPeerCertSha256: z.array(z.string()).default([]),
-  verifyPeerCertByName: z.boolean().default(false),
+  // Comma-separated cert names (xray `vcn`). Legacy rows stored a boolean here;
+  // coerce any stray bool to '' so old data loads cleanly.
+  verifyPeerCertByName: z.preprocess(
+    (v) => (typeof v === 'boolean' ? '' : v),
+    z.string().default(''),
+  ),
   allowInsecure: z.boolean().default(false),
   echConfigList: z.string().default(''),
 
@@ -98,7 +103,10 @@ export const HostRecordSchema = z.object({
   overrideSniFromAddress: z.boolean().optional(),
   keepSniBlank: z.boolean().optional(),
   pinnedPeerCertSha256: z.array(z.string()).nullish(),
-  verifyPeerCertByName: z.boolean().optional(),
+  verifyPeerCertByName: z.preprocess(
+    (v) => (typeof v === 'boolean' ? '' : v),
+    z.string().optional(),
+  ),
   allowInsecure: z.boolean().optional(),
   echConfigList: z.string().optional(),
   muxParams: z.unknown().optional(),

+ 16 - 0
frontend/src/schemas/protocols/security/reality.ts

@@ -14,6 +14,17 @@ export const RealityClientSettingsSchema = z.object({
 });
 export type RealityClientSettings = z.infer<typeof RealityClientSettingsSchema>;
 
+// REALITY fallback rate-limit (xray-core reality.LimitFallback): throttles the
+// fallback stream after `afterBytes`, then caps it at `bytesPerSec` with an
+// optional `burstBytesPerSec`. Optional so existing inbounds round-trip
+// unchanged — the object is only emitted once a user sets a non-zero value.
+export const RealityLimitFallbackSchema = z.object({
+  afterBytes: z.number().int().min(0).default(0),
+  bytesPerSec: z.number().int().min(0).default(0),
+  burstBytesPerSec: z.number().int().min(0).default(0),
+});
+export type RealityLimitFallback = z.infer<typeof RealityLimitFallbackSchema>;
+
 // xray-core accepts both `target` and `dest` as the REALITY destination —
 // they are aliases (infra/conf/transport_internet.go: REALITYConfig has
 // `json:"target"` and `json:"dest"`). The panel writes `target`, but configs
@@ -52,6 +63,11 @@ export const RealityStreamSettingsSchema = z.preprocess(
     maxTimediff: z.number().int().min(0).default(0),
     shortIds: z.array(z.string()).default([]),
     mldsa65Seed: z.string().default(''),
+    // Server-side TLS master-key log path (xray-core reality.Config). Optional
+    // so existing inbounds round-trip unchanged.
+    masterKeyLog: z.string().optional(),
+    limitFallbackUpload: RealityLimitFallbackSchema.optional(),
+    limitFallbackDownload: RealityLimitFallbackSchema.optional(),
     settings: RealityClientSettingsSchema.default({
       publicKey: '',
       fingerprint: 'chrome',

+ 14 - 1
frontend/src/schemas/protocols/security/tls.ts

@@ -1,5 +1,7 @@
 import { z } from 'zod';
 
+import { SockoptStreamSettingsSchema } from '@/schemas/protocols/stream/sockopt';
+
 export const TlsVersionSchema = z.enum(['1.0', '1.1', '1.2', '1.3']);
 export type TlsVersion = z.infer<typeof TlsVersionSchema>;
 
@@ -57,6 +59,11 @@ export const TlsClientSettingsSchema = z.object({
   fingerprint: TlsFingerprintSchema.default('chrome'),
   echConfigList: z.string().default(''),
   pinnedPeerCertSha256: z.array(z.string()).default([]),
+  // Panel-only client directive (v2rayN `vcn`): verify the server certificate
+  // against this name instead of the SNI. Comma-separated names. Shipped in
+  // share links / subscriptions; the modern replacement for `allowInsecure`,
+  // which xray-core removed after 2026-06-01.
+  verifyPeerCertByName: z.string().default(''),
 });
 export type TlsClientSettings = z.infer<typeof TlsClientSettingsSchema>;
 
@@ -73,6 +80,12 @@ export const TlsStreamSettingsSchema = z.object({
   certificates: z.array(TlsCertSchema).default([]),
   alpn: z.array(AlpnSchema).default(['h2', 'http/1.1']),
   echServerKeys: z.string().default(''),
-  settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '', pinnedPeerCertSha256: [] }),
+  // Server-side TLS fields (xray-core TLSConfig top-level): survive the
+  // panel-only `settings` strip and reach the runtime config. Optional so
+  // existing inbounds round-trip unchanged.
+  curvePreferences: z.array(z.string()).optional(),
+  masterKeyLog: z.string().optional(),
+  echSockopt: SockoptStreamSettingsSchema.optional(),
+  settings: TlsClientSettingsSchema.default({ fingerprint: 'chrome', echConfigList: '', pinnedPeerCertSha256: [], verifyPeerCertByName: '' }),
 });
 export type TlsStreamSettings = z.infer<typeof TlsStreamSettingsSchema>;

+ 1 - 0
frontend/src/schemas/protocols/stream/external-proxy.ts

@@ -23,6 +23,7 @@ export const ExternalProxyEntrySchema = z.object({
   ),
   alpn: z.array(AlpnSchema).optional(),
   pinnedPeerCertSha256: z.array(z.string()).optional(),
+  verifyPeerCertByName: z.string().optional(),
   echConfigList: z.string().optional(),
 });
 export type ExternalProxyEntry = z.infer<typeof ExternalProxyEntrySchema>;

+ 5 - 0
frontend/src/test/__snapshots__/inbound-form-blocks.test.tsx.snap

@@ -16,6 +16,7 @@ exports[`inbound security forms > RealityForm field structure is stable 1`] = `
   "Private Key",
   "mldsa65 Seed",
   "mldsa65 Verify",
+  "Master Key Log",
 ]
 `;
 
@@ -26,13 +27,17 @@ exports[`inbound security forms > TlsForm field structure is stable 1`] = `
   "Min/Max Version",
   "uTLS",
   "ALPN",
+  "Curve Preferences",
   "Reject Unknown SNI",
   "Disable System Root",
   "Session Resumption",
   "Digital Certificate",
+  "Master Key Log",
+  "ECH Sockopt",
   "ECH key",
   "ECH config",
   "Pinned Peer Cert SHA-256",
+  "Verify Peer Cert By Name",
 ]
 `;
 

+ 5 - 0
frontend/src/test/__snapshots__/inbound-full.test.ts.snap

@@ -74,6 +74,7 @@ exports[`InboundSchema (full) fixtures > parses hysteria-v1-tls byte-stably 1`]
         "echConfigList": "",
         "fingerprint": "chrome",
         "pinnedPeerCertSha256": [],
+        "verifyPeerCertByName": "",
       },
     },
   },
@@ -217,6 +218,7 @@ exports[`InboundSchema (full) fixtures > parses trojan-ws-tls byte-stably 1`] =
         "echConfigList": "",
         "fingerprint": "chrome",
         "pinnedPeerCertSha256": [],
+        "verifyPeerCertByName": "",
       },
     },
     "wsSettings": {
@@ -394,6 +396,7 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls byte-stably 1`] = `
         "echConfigList": "",
         "fingerprint": "chrome",
         "pinnedPeerCertSha256": [],
+        "verifyPeerCertByName": "",
       },
     },
     "wsSettings": {
@@ -488,6 +491,7 @@ exports[`InboundSchema (full) fixtures > parses vless-ws-tls-pinned byte-stably
           "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
           "BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=",
         ],
+        "verifyPeerCertByName": "",
       },
     },
     "wsSettings": {
@@ -583,6 +587,7 @@ exports[`InboundSchema (full) fixtures > parses vmess-tcp-tls byte-stably 1`] =
         "echConfigList": "",
         "fingerprint": "chrome",
         "pinnedPeerCertSha256": [],
+        "verifyPeerCertByName": "",
       },
     },
   },

+ 1 - 0
frontend/src/test/__snapshots__/security.test.ts.snap

@@ -68,6 +68,7 @@ exports[`SecuritySettingsSchema fixtures > parses tls-cert-file byte-stably 1`]
       "echConfigList": "",
       "fingerprint": "chrome",
       "pinnedPeerCertSha256": [],
+      "verifyPeerCertByName": "",
     },
   },
 }

+ 2 - 0
frontend/src/test/host-link.test.ts

@@ -13,6 +13,7 @@ describe('hostToExternalProxyEntry', () => {
     alpn: ['h2'] as ('h2' | 'h3' | 'http/1.1')[],
     fingerprint: 'chrome' as const,
     pinnedPeerCertSha256: ['AAAA'],
+    verifyPeerCertByName: 'verify.example.com',
     echConfigList: 'ECH',
     overrideSniFromAddress: false,
     keepSniBlank: false,
@@ -28,6 +29,7 @@ describe('hostToExternalProxyEntry', () => {
     expect(ep.alpn).toEqual(['h2']);
     expect(ep.fingerprint).toBe('chrome');
     expect(ep.pinnedPeerCertSha256).toEqual(['AAAA']);
+    expect(ep.verifyPeerCertByName).toBe('verify.example.com');
     expect(ep.echConfigList).toBe('ECH');
   });
 

+ 2 - 1
frontend/src/test/inbound-form-blocks.test.tsx

@@ -85,7 +85,8 @@ describe('inbound security forms', () => {
         saving={false}
         setCertFromPanel={noop}
         clearCertFiles={noop}
-        generateRandomPinHash={noop}
+        pinFromCert={noop}
+        pinFromRemote={noop}
         getNewEchCert={noop}
         clearEchCert={noop}
       />

+ 38 - 0
internal/database/db.go

@@ -89,6 +89,9 @@ func initModels() error {
 			return err
 		}
 	}
+	if err := migrateHostVerifyPeerCertByNameColumn(); err != nil {
+		return err
+	}
 	if err := dropLegacyForeignKeys(); err != nil {
 		return err
 	}
@@ -121,6 +124,41 @@ func dropLegacyForeignKeys() error {
 	return nil
 }
 
+// migrateHostVerifyPeerCertByNameColumn converts hosts.verify_peer_cert_by_name
+// from its original boolean shape to the comma-separated string xray-core's
+// verifyPeerCertByName (vcn) actually expects. The legacy boolean was dead
+// (never emitted into links), so its value carries no meaning and is discarded.
+// Idempotent by construction (no HistoryOfSeeders row — writing one here would
+// flip the fresh-DB detection in runSeeders). Runs right after AutoMigrate,
+// before anything reads or writes Host rows (critical on Postgres, where the
+// column stays boolean-typed until the ALTER below).
+func migrateHostVerifyPeerCertByNameColumn() error {
+	if !db.Migrator().HasColumn(&model.Host{}, "verify_peer_cert_by_name") {
+		return nil
+	}
+	if IsPostgres() {
+		// Only convert a still-boolean column; once it is text this is a no-op,
+		// so a user-set name is never wiped on a later restart.
+		var dataType string
+		if err := db.Raw(
+			`SELECT data_type FROM information_schema.columns WHERE table_name = 'hosts' AND column_name = 'verify_peer_cert_by_name'`,
+		).Scan(&dataType).Error; err != nil {
+			return err
+		}
+		if dataType != "boolean" {
+			return nil
+		}
+		if err := db.Exec(`ALTER TABLE hosts ALTER COLUMN verify_peer_cert_by_name DROP DEFAULT`).Error; err != nil {
+			return err
+		}
+		return db.Exec(`ALTER TABLE hosts ALTER COLUMN verify_peer_cert_by_name TYPE text USING ''`).Error
+	}
+	// SQLite keeps the original numeric-affinity column; blank any legacy
+	// integer/null value so it doesn't read back as "0"/"1". After conversion
+	// every value is text, so re-running touches nothing.
+	return db.Exec(`UPDATE hosts SET verify_peer_cert_by_name = '' WHERE verify_peer_cert_by_name IS NULL OR typeof(verify_peer_cert_by_name) <> 'text'`).Error
+}
+
 // seedHostsFromExternalProxy is a one-time, self-gated migration that creates a
 // Host row for every legacy externalProxy entry on every inbound. Additive: the
 // externalProxy arrays are left intact in StreamSettings.

+ 1 - 1
internal/database/model/model.go

@@ -744,7 +744,7 @@ type Host struct {
 	OverrideSniFromAddress bool     `json:"overrideSniFromAddress" form:"overrideSniFromAddress" gorm:"column:override_sni_from_address"`
 	KeepSniBlank           bool     `json:"keepSniBlank" form:"keepSniBlank" gorm:"column:keep_sni_blank"`
 	PinnedPeerCertSha256   []string `json:"pinnedPeerCertSha256" form:"pinnedPeerCertSha256" gorm:"serializer:json;column:pinned_peer_cert_sha256"`
-	VerifyPeerCertByName   bool     `json:"verifyPeerCertByName" form:"verifyPeerCertByName" gorm:"column:verify_peer_cert_by_name"`
+	VerifyPeerCertByName   string   `json:"verifyPeerCertByName" form:"verifyPeerCertByName" gorm:"column:verify_peer_cert_by_name"`
 	AllowInsecure          bool     `json:"allowInsecure" form:"allowInsecure" gorm:"column:allow_insecure"`
 	EchConfigList          string   `json:"echConfigList" form:"echConfigList" gorm:"column:ech_config_list"`
 

+ 3 - 0
internal/sub/host_sub.go

@@ -80,6 +80,9 @@ func hostToExternalProxyMap(h *model.Host, defaultDest string, defaultPort int)
 	if h.EchConfigList != "" {
 		ep["echConfigList"] = h.EchConfigList
 	}
+	if h.VerifyPeerCertByName != "" {
+		ep["verifyPeerCertByName"] = h.VerifyPeerCertByName
+	}
 	if h.AllowInsecure {
 		ep["allowInsecure"] = true
 	}

+ 3 - 0
internal/sub/json_service.go

@@ -309,6 +309,9 @@ func (s *SubJsonService) tlsData(tData map[string]any) map[string]any {
 	if ech, ok := tlsClientSettings["echConfigList"].(string); ok && ech != "" {
 		tlsData["echConfigList"] = ech
 	}
+	if vcn, ok := verifyPeerCertByNameValue(tlsClientSettings); ok {
+		tlsData["verifyPeerCertByName"] = vcn
+	}
 	// xray-core now parses pinnedPeerCertSha256 as a comma-separated string, not
 	// an array; emit the joined form so v2ray clients can import the config (#5401).
 	if pins, ok := pinnedSha256List(tlsClientSettings); ok {

+ 42 - 0
internal/sub/service.go

@@ -813,6 +813,9 @@ func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) strin
 				params["ech"] = ech
 			}
 		}
+		if vcn, ok := verifyPeerCertByNameValue(tlsSettings); ok {
+			params["vcn"] = vcn
+		}
 		if pins, ok := pinnedSha256List(tlsSettings); ok {
 			for i, p := range pins {
 				pins[i] = hysteriaPinHex(p)
@@ -1120,6 +1123,9 @@ func applyShareTLSParams(stream map[string]any, params map[string]string) {
 				params["ech"] = ech
 			}
 		}
+		if vcn, ok := verifyPeerCertByNameValue(tlsSettings); ok {
+			params["vcn"] = vcn
+		}
 		if pins, ok := pinnedSha256List(tlsSettings); ok {
 			params["pcs"] = strings.Join(pins, ",")
 		}
@@ -1150,12 +1156,34 @@ func applyVmessTLSParams(stream map[string]any, obj map[string]any) {
 				obj["ech"] = ech
 			}
 		}
+		if vcn, ok := verifyPeerCertByNameValue(tlsSettings); ok {
+			obj["vcn"] = vcn
+		}
 		if pins, ok := pinnedSha256List(tlsSettings); ok {
 			obj["pcs"] = strings.Join(pins, ",")
 		}
 	}
 }
 
+// verifyPeerCertByNameValue extracts tlsSettings.settings.verifyPeerCertByName
+// (the v2rayN `vcn` param) as a trimmed string. Like pinnedPeerCertSha256 it is
+// panel-only and flows into share links so clients verify the server
+// certificate by this name — the replacement for the removed allowInsecure.
+func verifyPeerCertByNameValue(tlsClientSettings any) (string, bool) {
+	raw, ok := searchKey(tlsClientSettings, "verifyPeerCertByName")
+	if !ok {
+		return "", false
+	}
+	s, ok := raw.(string)
+	if !ok {
+		return "", false
+	}
+	if s = strings.TrimSpace(s); s == "" {
+		return "", false
+	}
+	return s, true
+}
+
 // pinnedSha256List extracts tlsSettings.settings.pinnedPeerCertSha256 as a
 // []string. The field is panel-only (stripped before the run-config reaches
 // xray-core via internal/web/service/xray.go) but flows into share links so clients
@@ -1274,6 +1302,9 @@ func applyExternalProxyTLSObj(ep map[string]any, obj map[string]any, security st
 	if pins, ok := externalProxyPins(ep["pinnedPeerCertSha256"]); ok {
 		obj["pcs"] = joinAnyStrings(pins)
 	}
+	if vcn, ok := ep["verifyPeerCertByName"].(string); ok && vcn != "" {
+		obj["vcn"] = vcn
+	}
 	if ech, ok := ep["echConfigList"].(string); ok && ech != "" {
 		obj["ech"] = ech
 	}
@@ -1295,6 +1326,9 @@ func applyExternalProxyTLSParams(ep map[string]any, params map[string]string, se
 	if pins, ok := externalProxyPins(ep["pinnedPeerCertSha256"]); ok {
 		params["pcs"] = joinAnyStrings(pins)
 	}
+	if vcn, ok := ep["verifyPeerCertByName"].(string); ok && vcn != "" {
+		params["vcn"] = vcn
+	}
 	if ech, ok := ep["echConfigList"].(string); ok && ech != "" {
 		params["ech"] = ech
 	}
@@ -1378,6 +1412,14 @@ func applyExternalProxyTLSToStream(ep map[string]any, stream map[string]any, sec
 		}
 		settings["echConfigList"] = ech
 	}
+	if vcn, ok := ep["verifyPeerCertByName"].(string); ok && vcn != "" {
+		settings, _ := tlsSettings["settings"].(map[string]any)
+		if settings == nil {
+			settings = map[string]any{}
+			tlsSettings["settings"] = settings
+		}
+		settings["verifyPeerCertByName"] = vcn
+	}
 	if ai, ok := ep["allowInsecure"].(bool); ok && ai {
 		settings, _ := tlsSettings["settings"].(map[string]any)
 		if settings == nil {

+ 24 - 0
internal/web/controller/server.go

@@ -75,6 +75,8 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
 	g.POST("/xraylogs/:count", a.getXrayLogs)
 	g.POST("/importDB", a.importDB)
 	g.POST("/getNewEchCert", a.getNewEchCert)
+	g.POST("/getCertHash", a.getCertHash)
+	g.POST("/getRemoteCertHash", a.getRemoteCertHash)
 	g.POST("/clientIps", a.setClientIps)
 }
 
@@ -395,6 +397,28 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
 	jsonObj(c, cert, nil)
 }
 
+// getCertHash returns the hex SHA-256 of the given certificate (file path or
+// inline content) so the panel can fill the pinned-cert field.
+func (a *ServerController) getCertHash(c *gin.Context) {
+	hashes, err := a.serverService.GetCertHash(c.PostForm("certFile"), c.PostForm("certContent"))
+	if err != nil {
+		jsonMsg(c, "get cert hash", err)
+		return
+	}
+	jsonObj(c, hashes, nil)
+}
+
+// getRemoteCertHash runs `xray tls ping` against the given server and returns
+// its live certificate SHA-256 hash(es) for pinning.
+func (a *ServerController) getRemoteCertHash(c *gin.Context) {
+	hashes, err := a.serverService.GetRemoteCertHash(c.PostForm("server"))
+	if err != nil {
+		jsonMsg(c, "get remote cert hash", err)
+		return
+	}
+	jsonObj(c, hashes, nil)
+}
+
 // getNewVlessEnc generates a new VLESS encryption key.
 func (a *ServerController) getNewVlessEnc(c *gin.Context) {
 	out, err := a.serverService.GetNewVlessEnc()

+ 99 - 0
internal/web/service/server.go

@@ -4,9 +4,12 @@ import (
 	"archive/zip"
 	"bufio"
 	"bytes"
+	"context"
 	"crypto/sha256"
+	"crypto/x509"
 	"encoding/hex"
 	"encoding/json"
+	"encoding/pem"
 	"fmt"
 	"io"
 	"mime/multipart"
@@ -1721,6 +1724,102 @@ func (s *ServerService) GetNewmldsa65() (any, error) {
 	return keyPair, nil
 }
 
+// GetCertHash parses a certificate (from a file path or inline PEM/DER content)
+// and returns the hex-encoded SHA-256 over each certificate's raw DER — the
+// value xray-core's pinnedPeerCertSha256 (pcs) expects. Lets the panel fill the
+// pinned-cert field from the inbound's own certificate without the user
+// computing the hash by hand.
+func (s *ServerService) GetCertHash(certFile string, certContent string) ([]string, error) {
+	var certBytes []byte
+	if path := strings.TrimSpace(certFile); path != "" {
+		b, err := os.ReadFile(path)
+		if err != nil {
+			return nil, err
+		}
+		certBytes = b
+	} else if strings.TrimSpace(certContent) != "" {
+		certBytes = []byte(certContent)
+	} else {
+		return nil, common.NewError("no certificate provided")
+	}
+
+	var certs []*x509.Certificate
+	if bytes.Contains(certBytes, []byte("BEGIN")) {
+		rest := certBytes
+		for {
+			block, remain := pem.Decode(rest)
+			if block == nil {
+				break
+			}
+			cert, err := x509.ParseCertificate(block.Bytes)
+			if err != nil {
+				return nil, common.NewError("unable to decode certificate: ", err)
+			}
+			certs = append(certs, cert)
+			rest = remain
+		}
+	} else {
+		parsed, err := x509.ParseCertificates(certBytes)
+		if err != nil {
+			return nil, common.NewError("unable to parse certificates: ", err)
+		}
+		certs = parsed
+	}
+
+	if len(certs) == 0 {
+		return nil, common.NewError("no certificates found")
+	}
+
+	hashes := make([]string, 0, len(certs))
+	for _, cert := range certs {
+		sum := sha256.Sum256(cert.Raw)
+		hashes = append(hashes, hex.EncodeToString(sum[:]))
+	}
+	return hashes, nil
+}
+
+// GetRemoteCertHash runs `xray tls ping <server>` to fetch the live certificate
+// SHA-256 of a remote endpoint — the value to put in pinnedPeerCertSha256 (pcs)
+// when pinning a server whose certificate file you don't hold (a CDN front, a
+// REALITY dest, an external proxy). Returns the unique leaf-certificate hashes.
+func (s *ServerService) GetRemoteCertHash(server string) ([]string, error) {
+	server = strings.TrimSpace(server)
+	if server == "" {
+		return nil, common.NewError("no server provided")
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
+	defer cancel()
+	cmd := exec.CommandContext(ctx, xray.GetBinaryPath(), "tls", "ping", server)
+	var out bytes.Buffer
+	cmd.Stdout = &out
+	cmd.Stderr = &out
+	if err := cmd.Run(); err != nil && out.Len() == 0 {
+		return nil, err
+	}
+
+	hexRe := regexp.MustCompile(`[0-9a-fA-F]{64}`)
+	seen := make(map[string]struct{})
+	var leaves []string
+	for _, line := range strings.Split(out.String(), "\n") {
+		if !strings.Contains(line, "leaf SHA256") {
+			continue
+		}
+		hash := strings.ToLower(hexRe.FindString(line))
+		if hash == "" {
+			continue
+		}
+		if _, ok := seen[hash]; !ok {
+			seen[hash] = struct{}{}
+			leaves = append(leaves, hash)
+		}
+	}
+	if len(leaves) == 0 {
+		return nil, common.NewError("no certificate hash found for ", server)
+	}
+	return leaves, nil
+}
+
 func (s *ServerService) GetNewEchCert(sni string) (any, error) {
 	// Run the command
 	cmd := exec.Command(xray.GetBinaryPath(), "tls", "ech", "--serverName", sni)

+ 33 - 5
internal/web/translation/ar-EG.json

@@ -591,7 +591,6 @@
         "pinnedPeerCertSha256": "SHA-256 لشهادة النظير المثبَّتة",
         "pinnedPeerCertSha256Tip": "تجزئات SHA-256 لشهادة النظير كسلسلة سداسية عشرية (مثل e8e2d3…)، مفصولة بفواصل. للوحة فقط — لا تُكتب في إعدادات xray على الخادم، لكنها تُضمَّن في روابط المشاركة ليتمكَّن العملاء من تثبيت الشهادة.",
         "pinnedPeerCertSha256Placeholder": "تجزئة (تجزئات) سداسية عشرية، مفصولة بفواصل",
-        "generateRandomPin": "إنشاء تجزئة عشوائية",
         "getNewEchCert": "احصل على شهادة ECH جديدة",
         "show": "عرض",
         "xver": "Xver",
@@ -620,7 +619,28 @@
           "node": "عنوان العقدة",
           "listen": "عنوان استماع الوارد",
           "custom": "مخصص"
-        }
+        },
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "خيارات السوكيت للاتصال اللي Xray بتستخدمه عشان تجيب قائمة إعدادات ECH (مثلاً توجيه الاستعلام عبر صادر dialerProxy). سيبه متعطّل عشان تستخدم الإعدادات الافتراضية.",
+        "curvePreferences": "تفضيلات المنحنى",
+        "curvePreferencesTip": "بتقصر منحنيات تبادل مفاتيح TLS اللي السيرفر بيعرضها، بترتيب الأفضلية (مثلاً X25519MLKEM768, X25519). سيبه فاضي عشان تستخدم الإعدادات الافتراضية لـ Xray-core.",
+        "masterKeyLog": "سجل المفتاح الرئيسي",
+        "masterKeyLogTip": "المسار اللي هتتكتب فيه مفاتيح TLS الرئيسية (بصيغة SSLKEYLOGFILE) عشان التصحيح باستخدام Wireshark. سيبه فاضي في الإنتاج — لإنه بيخلي أي حد عنده الملف يقدر يفك تشفير المرور.",
+        "verifyPeerCertByName": "التحقق من شهادة النظير بالاسم",
+        "verifyPeerCertByNameTip": "بيقول للعملاء يتحققوا من شهادة السيرفر مقابل الاسم ده بدل الـ SNI. الأسماء بتتفصل بفاصلة. للوحة بس — بيتضاف في روابط المشاركة (vcn). ده البديل الحديث لـ allowInsecure اللي شالته Xray بعد 2026-06-01.",
+        "pinFromCert": "املأ من شهادة الوارد ده",
+        "pinFromRemote": "اجلب الهاش عبر ping على الـ SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "حدّد الـ SNI (serverName) الأول عشان تعمل ping للشهادة البعيدة.",
+        "pinFromRemoteFailed": "تعذّر جلب هاش الشهادة البعيدة.",
+        "limitFallback": "تحديد الـ Fallback",
+        "limitFallbackUpload": "تحديد رفع الـ Fallback",
+        "limitFallbackDownload": "تحديد تنزيل الـ Fallback",
+        "afterBytes": "بعد البايتات",
+        "afterBytesTip": "خلي الـ fallback يشتغل بالسرعة الكاملة لعدد البايتات ده، وبعدين يبدأ التحجيم. 0 = حجّم من أول بايت.",
+        "bytesPerSec": "بايت في الثانية",
+        "bytesPerSecTip": "حد السرعة (بايت/ثانية) المطبّق على مرور الـ fallback بعد العتبة، عشان الفحوصات ماتقدرش تستخدم سيرفرك كنطاق ترددي مجاني للهدف. 0 = بلا حد (بيعطّل الاتجاه ده).",
+        "burstBytesPerSec": "بايت في الثانية للدفقة",
+        "burstBytesPerSecTip": "السماح بدفقات قصيرة فوق المعدل الثابت (حجم token-bucket). لو أقل من بايت في الثانية بيترفع ليطابقه."
       },
       "info": {
         "mode": "الوضع",
@@ -1340,6 +1360,14 @@
       "Outbounds": "الصادرات",
       "OutboundSubscriptions": "اشتراكات الصادرات",
       "OutboundSubscriptionsDesc": "استورد الصادرات من روابط اشتراك بعيدة (vmess/vless/trojan/ss/...). الوسوم بتفضل ثابتة عشان تستخدمها في موازنات التحميل وقواعد التوجيه. التحديثات بتتم تلقائياً.",
+      "importRules": "استيراد القواعد",
+      "exportRules": "تصدير القواعد",
+      "importOutbounds": "استيراد الصادرات",
+      "exportOutbounds": "تصدير الصادرات",
+      "importInvalidJson": "JSON غير صالح — المتوقع مصفوفة أو كائن بمفتاح مطابق.",
+      "metricsListen": "نقطة نهاية المقاييس",
+      "metricsListenDesc": "بتعرض مقاييس Xray بنمط Prometheus على العنوان:المنفذ ده (مثلاً 127.0.0.1:11111). سيبه فاضي عشان تعطّله. اربطه بـ localhost وحطّ قدامه reverse-proxy — لإنه من غير مصادقة.",
+      "metricsTag": "وسم المقاييس",
       "Balancers": "موازنات التحميل",
       "balancerTagRequired": "الوسم مطلوب",
       "balancerSelectorRequired": "اختر صادراً واحداً على الأقل",
@@ -1729,7 +1757,6 @@
         "alpn": "ALPN",
         "fingerprint": "بصمة",
         "pins": "SHA-256 للشهادة المثبّتة",
-        "verifyPeerCertByName": "التحقق من شهادة النظير بالاسم",
         "allowInsecure": "السماح بغير الآمن",
         "echConfigList": "قائمة إعدادات ECH",
         "muxParams": "Mux",
@@ -1741,7 +1768,8 @@
         "shuffleHost": "خلط المضيف",
         "tags": "وسوم",
         "nodeGuids": "النودز",
-        "excludeFromSubTypes": "استبعاد من الصيغ"
+        "excludeFromSubTypes": "استبعاد من الصيغ",
+        "verifyPeerCertByName": "التحقق من شهادة النظير بالاسم"
       },
       "hints": {
         "address": "اتركه فارغاً ليرث عنوان الوارد نفسه.",
@@ -2015,4 +2043,4 @@
     "statusDown": "غير متصل",
     "statusUp": "متصل"
   }
-}
+}

+ 29 - 1
internal/web/translation/en-US.json

@@ -588,10 +588,21 @@
         "buildChain": "Build Chain",
         "echKey": "ECH key",
         "echConfig": "ECH config",
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Socket options for the connection Xray uses to fetch the ECH config list (e.g. route the lookup through a dialerProxy outbound). Leave disabled to use defaults.",
+        "curvePreferences": "Curve Preferences",
+        "curvePreferencesTip": "Restrict the TLS key-exchange curves the server offers, in preference order (e.g. X25519MLKEM768, X25519). Leave empty to use Xray-core defaults.",
+        "masterKeyLog": "Master Key Log",
+        "masterKeyLogTip": "Path to write TLS master keys (SSLKEYLOGFILE format) for debugging with Wireshark. Leave empty in production — it lets anyone with the file decrypt traffic.",
+        "verifyPeerCertByName": "Verify Peer Cert By Name",
+        "verifyPeerCertByNameTip": "Tell clients to verify the server certificate against this name instead of the SNI. Comma-separated names. Panel-only — included in share links (vcn). The modern replacement for allowInsecure, which Xray removed after 2026-06-01.",
         "pinnedPeerCertSha256": "Pinned Peer Cert SHA-256",
         "pinnedPeerCertSha256Tip": "SHA-256 hash(es) of the peer certificate as a hex string (e.g. e8e2d3…), comma-separated. Panel-only — not written to the server's xray config, but included in share links so clients can pin the certificate.",
         "pinnedPeerCertSha256Placeholder": "hex hash(es), comma-separated",
-        "generateRandomPin": "Generate random hash",
+        "pinFromCert": "Fill from this inbound's certificate",
+        "pinFromRemote": "Fetch the hash by pinging the SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "Set the SNI (serverName) first to ping the remote certificate.",
+        "pinFromRemoteFailed": "Could not fetch the remote certificate hash.",
         "getNewEchCert": "Get New ECH Cert",
         "show": "Show",
         "xver": "Xver",
@@ -609,6 +620,15 @@
         "mldsa65Seed": "mldsa65 Seed",
         "mldsa65Verify": "mldsa65 Verify",
         "getNewSeed": "Get New Seed",
+        "limitFallback": "Limit Fallback",
+        "limitFallbackUpload": "Limit Fallback Upload",
+        "limitFallbackDownload": "Limit Fallback Download",
+        "afterBytes": "After Bytes",
+        "afterBytesTip": "Let the fallback run at full speed for this many bytes, then start throttling. 0 = throttle from the first byte.",
+        "bytesPerSec": "Bytes Per Sec",
+        "bytesPerSecTip": "Speed cap (bytes/sec) applied to fallback traffic after the threshold, so probes can't use your server as free bandwidth to the target. 0 = no limit (disables this direction).",
+        "burstBytesPerSec": "Burst Bytes Per Sec",
+        "burstBytesPerSecTip": "Allowance for short bursts above the steady rate (token-bucket size). If lower than Bytes Per Sec it is raised to match.",
         "listenHelp": "You can also enter a Unix socket path (e.g. /run/xray/in.sock), or an abstract socket name prefixed with @ (e.g. @xray/in.sock), to listen on a socket instead of a TCP port — set Port to 0 in that case.",
         "shareAddrStrategy": "Share address strategy",
         "shareAddrStrategyHelp": "Controls which address is written into exported share links, QR codes, and subscription output.",
@@ -1469,6 +1489,11 @@
       "OutboundsDesc": "Set the outgoing traffic pathway.",
       "Routings": "Routing Rules",
       "RoutingsDesc": "The priority of each rule is important!",
+      "importRules": "Import Rules",
+      "exportRules": "Export Rules",
+      "importOutbounds": "Import Outbounds",
+      "exportOutbounds": "Export Outbounds",
+      "importInvalidJson": "Invalid JSON — expected an array or an object with a matching key.",
       "completeTemplate": "All",
       "logLevel": "Log Level",
       "logLevelDesc": "The log level for error logs, indicating the information that needs to be recorded.",
@@ -1489,6 +1514,9 @@
       "statsOutboundUplinkDesc": "Enables the statistics collection for upstream traffic of all outbound proxies.",
       "statsOutboundDownlink": "Outbound Download Statistics",
       "statsOutboundDownlinkDesc": "Enables the statistics collection for downstream traffic of all outbound proxies.",
+      "metricsListen": "Metrics Endpoint",
+      "metricsListenDesc": "Expose Xray's Prometheus-style metrics on this address:port (e.g. 127.0.0.1:11111). Leave empty to disable. Bind to localhost and reverse-proxy it — it is unauthenticated.",
+      "metricsTag": "Metrics Tag",
       "connectionLimits": "Connection Limits",
       "connectionLimitsDesc": "Connection-level policies for user level 0. Leave a field empty to use Xray's default.",
       "connIdle": "Idle Timeout",

+ 29 - 1
internal/web/translation/es-ES.json

@@ -591,8 +591,28 @@
         "pinnedPeerCertSha256": "SHA-256 del cert. del par fijado",
         "pinnedPeerCertSha256Tip": "Hashes SHA-256 del certificado del par como cadena hexadecimal (p. ej. e8e2d3…), separados por comas. Solo en el panel — no se escribe en la config xray del servidor, pero se incluye en los enlaces para que los clientes puedan fijar el certificado.",
         "pinnedPeerCertSha256Placeholder": "hash(es) hexadecimal, separados por comas",
-        "generateRandomPin": "Generar hash aleatorio",
         "getNewEchCert": "Obtener nuevo cert ECH",
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Opciones de socket para la conexión que Xray usa al obtener la lista de configuración ECH (por ejemplo, enrutar la consulta a través de una salida dialerProxy). Déjalo deshabilitado para usar los valores por defecto.",
+        "curvePreferences": "Preferencias de curvas",
+        "curvePreferencesTip": "Restringe las curvas de intercambio de claves TLS que ofrece el servidor, por orden de preferencia (por ejemplo, X25519MLKEM768, X25519). Déjalo vacío para usar los valores por defecto de Xray-core.",
+        "masterKeyLog": "Registro de clave maestra",
+        "masterKeyLogTip": "Ruta donde escribir las claves maestras TLS (formato SSLKEYLOGFILE) para depurar con Wireshark. Déjalo vacío en producción — permite que cualquiera con el archivo descifre el tráfico.",
+        "verifyPeerCertByName": "Verificar cert. del par por nombre",
+        "verifyPeerCertByNameTip": "Indica a los clientes que verifiquen el certificado del servidor con este nombre en lugar del SNI. Nombres separados por comas. Solo en el panel — se incluye en los enlaces (vcn). El reemplazo moderno de allowInsecure, que Xray eliminó después del 2026-06-01.",
+        "pinFromCert": "Rellenar desde el certificado de este inbound",
+        "pinFromRemote": "Obtener el hash haciendo ping al SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "Establece primero el SNI (serverName) para hacer ping al certificado remoto.",
+        "pinFromRemoteFailed": "No se pudo obtener el hash del certificado remoto.",
+        "limitFallback": "Limitar fallback",
+        "limitFallbackUpload": "Limitar subida del fallback",
+        "limitFallbackDownload": "Limitar bajada del fallback",
+        "afterBytes": "Tras bytes",
+        "afterBytesTip": "Deja que el fallback funcione a máxima velocidad durante esta cantidad de bytes y luego empieza a limitar. 0 = limitar desde el primer byte.",
+        "bytesPerSec": "Bytes por seg.",
+        "bytesPerSecTip": "Límite de velocidad (bytes/seg.) aplicado al tráfico del fallback tras el umbral, para que las sondas no puedan usar tu servidor como ancho de banda gratuito hacia el destino. 0 = sin límite (deshabilita esta dirección).",
+        "burstBytesPerSec": "Ráfaga de bytes por seg.",
+        "burstBytesPerSecTip": "Margen para ráfagas breves por encima de la tasa constante (tamaño del token-bucket). Si es menor que Bytes por seg., se eleva para igualarlo.",
         "show": "Mostrar",
         "xver": "Xver",
         "target": "Objetivo",
@@ -1389,6 +1409,14 @@
       "bufferSizeDesc": "Tamaño del búfer interno por conexión en KB. Ponlo en 0 para minimizar el uso de memoria en servidores con poca RAM (el valor predeterminado de Xray depende de la plataforma).",
       "bufferSizePlaceholder": "automático",
       "seconds": "segundos",
+      "importRules": "Importar reglas",
+      "exportRules": "Exportar reglas",
+      "importOutbounds": "Importar salidas",
+      "exportOutbounds": "Exportar salidas",
+      "importInvalidJson": "JSON no válido — se esperaba un array o un objeto con una clave coincidente.",
+      "metricsListen": "Punto de métricas",
+      "metricsListenDesc": "Expone las métricas estilo Prometheus de Xray en esta dirección:puerto (por ejemplo, 127.0.0.1:11111). Déjalo vacío para deshabilitarlo. Vincúlalo a localhost y ponlo tras un proxy inverso — no está autenticado.",
+      "metricsTag": "Etiqueta de métricas",
       "rules": {
         "first": "Primero",
         "last": "Último",

+ 29 - 1
internal/web/translation/fa-IR.json

@@ -588,10 +588,21 @@
         "buildChain": "ساخت زنجیره",
         "echKey": "کلید ECH",
         "echConfig": "پیکربندی ECH",
+        "echSockopt": "Sockopt مربوط به ECH",
+        "echSockoptTip": "تنظیمات سوکت برای اتصالی که Xray برای دریافت لیست پیکربندی ECH استفاده می‌کند (مثلاً عبور این درخواست از یک dialerProxy). برای استفاده از پیش‌فرض غیرفعال بگذارید.",
+        "curvePreferences": "ترجیح منحنی‌ها",
+        "curvePreferencesTip": "منحنی‌های تبادل کلید TLS که سرور ارائه می‌دهد را به‌ترتیب اولویت محدود می‌کند (مثلاً X25519MLKEM768، X25519). خالی بگذارید تا پیش‌فرض Xray استفاده شود.",
+        "masterKeyLog": "لاگ کلید اصلی",
+        "masterKeyLogTip": "مسیر نوشتن کلیدهای اصلی TLS (قالب SSLKEYLOGFILE) برای دیباگ با Wireshark. در محیط عملیاتی خالی بگذارید — هرکس به این فایل دسترسی داشته باشد می‌تواند ترافیک را رمزگشایی کند.",
+        "pinFromCert": "پرکردن از گواهی همین ورودی",
+        "pinFromRemote": "گرفتن هش با پینگ‌کردن SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "ابتدا SNI (serverName) را تنظیم کنید تا گواهی راه‌دور پینگ شود.",
+        "pinFromRemoteFailed": "گرفتن هشِ گواهی راه‌دور ممکن نشد.",
+        "verifyPeerCertByName": "تأیید گواهی همتا با نام",
+        "verifyPeerCertByNameTip": "به کلاینت‌ها می‌گوید گواهی سرور را به‌جای SNI با این نام تأیید کنند. نام‌ها با کاما جدا شوند. فقط در پنل — در لینک‌های اشتراک‌گذاری گنجانده می‌شود (vcn). جایگزین مدرن allowInsecure که xray بعد از ۲۰۲۶-۰۶-۰۱ حذفش کرد.",
         "pinnedPeerCertSha256": "SHA-256 پین‌شدهٔ گواهی همتا",
         "pinnedPeerCertSha256Tip": "هش‌های SHA-256 گواهی همتا به‌صورت رشتهٔ هگزادسیمال (مثل e8e2d3…)، با کاما جدا شوند. فقط در پنل — در پیکربندی xray سرور نوشته نمی‌شود، اما در لینک‌های اشتراک‌گذاری گنجانده می‌شود تا کلاینت‌ها بتوانند گواهی را پین کنند.",
         "pinnedPeerCertSha256Placeholder": "هش(های) هگزادسیمال، با کاما جدا شوند",
-        "generateRandomPin": "تولید هش تصادفی",
         "getNewEchCert": "دریافت گواهی ECH جدید",
         "show": "نمایش",
         "xver": "Xver",
@@ -609,6 +620,15 @@
         "mldsa65Seed": "mldsa65 Seed",
         "mldsa65Verify": "mldsa65 Verify",
         "getNewSeed": "دریافت Seed جدید",
+        "limitFallback": "محدودیت فالبک",
+        "limitFallbackUpload": "محدودیت فالبک آپلود",
+        "limitFallbackDownload": "محدودیت فالبک دانلود",
+        "afterBytes": "پس از (بایت)",
+        "afterBytesTip": "اجازه بده فالبک برای این تعداد بایت با سرعت کامل اجرا شود، سپس محدودسازی شروع شود. مقدار ۰ یعنی از همان بایت اول محدود شود.",
+        "bytesPerSec": "بایت بر ثانیه",
+        "bytesPerSecTip": "سقف سرعت (بایت بر ثانیه) که بعد از آستانه روی ترافیک فالبک اعمال می‌شود تا پروب‌ها نتوانند سرورت را به‌عنوان پهنای‌باند رایگان به مقصد استفاده کنند. مقدار ۰ یعنی بدون محدودیت (این جهت غیرفعال می‌شود).",
+        "burstBytesPerSec": "بایت انفجاری بر ثانیه",
+        "burstBytesPerSecTip": "سهمیه برای انفجارهای کوتاه بالاتر از نرخ ثابت (اندازهٔ token-bucket). اگر کمتر از «بایت بر ثانیه» باشد، به همان مقدار افزایش می‌یابد.",
         "listenHelp": "می‌توانید به‌جای پورت TCP یک مسیر سوکت یونیکس وارد کنید (مثلاً /run/xray/in.sock)، یا یک نام سوکت انتزاعی با پیشوند @ (مثلاً @xray/in.sock)، تا روی سوکت گوش داده شود — در این حالت پورت را روی ۰ بگذارید.",
         "shareAddrStrategy": "راهبرد آدرس اشتراک‌گذاری",
         "shareAddrStrategyHelp": "مشخص می‌کند کدام آدرس در لینک‌های اشتراک‌گذاری خروجی، کدهای QR و خروجی اشتراک نوشته شود.",
@@ -1361,6 +1381,11 @@
       "OutboundsDesc": "مسیر ترافیک خروجی را تنظیم کنید",
       "Routings": "قوانین مسیریابی",
       "RoutingsDesc": "اولویت هر قانون مهم است",
+      "importRules": "ورود قوانین",
+      "exportRules": "خروج قوانین",
+      "importOutbounds": "ورود خروجی‌ها",
+      "exportOutbounds": "خروج خروجی‌ها",
+      "importInvalidJson": "JSON نامعتبر — یک آرایه یا یک شیء با کلید متناظر انتظار می‌رود.",
       "completeTemplate": "همه",
       "logLevel": "سطح گزارش",
       "logLevelDesc": "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند.",
@@ -1381,6 +1406,9 @@
       "statsOutboundUplinkDesc": "جمع‌آوری آمار برای ترافیک بالارو (آپلود) تمام پروکسی‌های خروجی را فعال می‌کند.",
       "statsOutboundDownlink": "آمار دانلود خروجی",
       "statsOutboundDownlinkDesc": "جمع‌آوری آمار برای ترافیک پایین‌رو (دانلود) تمام پروکسی‌های خروجی را فعال می‌کند.",
+      "metricsListen": "نقطه پایانی متریک",
+      "metricsListenDesc": "متریک‌های Xray (سبک Prometheus) را روی این آدرس:پورت در دسترس قرار می‌دهد (مثلاً 127.0.0.1:11111). برای غیرفعال‌کردن خالی بگذارید. روی localhost ببندید و با ریورس‌پروکسی ارائه دهید — احراز هویت ندارد.",
+      "metricsTag": "تگ متریک",
       "connectionLimits": "محدودیت اتصال",
       "connectionLimitsDesc": "سیاست‌های سطح اتصال برای کاربرانِ سطح ۰. هر فیلد را خالی بگذارید تا مقدار پیش‌فرض Xray استفاده شود.",
       "connIdle": "مهلت بی‌کاری",

+ 33 - 5
internal/web/translation/id-ID.json

@@ -591,7 +591,6 @@
         "pinnedPeerCertSha256": "SHA-256 Sertifikat Peer Tersemat",
         "pinnedPeerCertSha256Tip": "Hash SHA-256 dari sertifikat peer sebagai string heksadesimal (mis. e8e2d3…), dipisah koma. Hanya panel — tidak ditulis ke konfig xray server, tetapi disertakan dalam link berbagi agar klien dapat menyematkan sertifikat.",
         "pinnedPeerCertSha256Placeholder": "hash heksadesimal, dipisah koma",
-        "generateRandomPin": "Hasilkan hash acak",
         "getNewEchCert": "Dapatkan sertifikat ECH baru",
         "show": "Tampilkan",
         "xver": "Xver",
@@ -620,7 +619,28 @@
           "node": "Alamat node",
           "listen": "Alamat listen inbound",
           "custom": "Kustom"
-        }
+        },
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Opsi socket untuk koneksi yang dipakai Xray guna mengambil daftar konfig ECH (mis. mengarahkan pencarian melalui outbound dialerProxy). Biarkan nonaktif untuk memakai bawaan.",
+        "curvePreferences": "Preferensi kurva",
+        "curvePreferencesTip": "Batasi kurva pertukaran kunci TLS yang ditawarkan server, sesuai urutan preferensi (mis. X25519MLKEM768, X25519). Biarkan kosong untuk memakai bawaan Xray-core.",
+        "masterKeyLog": "Log master key",
+        "masterKeyLogTip": "Path untuk menulis master key TLS (format SSLKEYLOGFILE) untuk debug dengan Wireshark. Biarkan kosong di produksi — ini memungkinkan siapa pun yang memiliki file tersebut mendekripsi lalu lintas.",
+        "verifyPeerCertByName": "Verifikasi sertifikat peer berdasarkan nama",
+        "verifyPeerCertByNameTip": "Minta klien memverifikasi sertifikat server terhadap nama ini, bukan SNI. Nama dipisahkan koma. Hanya panel — disertakan dalam tautan berbagi (vcn). Pengganti modern untuk allowInsecure, yang dihapus Xray setelah 2026-06-01.",
+        "pinFromCert": "Isi dari sertifikat inbound ini",
+        "pinFromRemote": "Ambil hash dengan melakukan ping ke SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "Atur SNI (serverName) terlebih dahulu untuk melakukan ping ke sertifikat jarak jauh.",
+        "pinFromRemoteFailed": "Tidak dapat mengambil hash sertifikat jarak jauh.",
+        "limitFallback": "Batasi fallback",
+        "limitFallbackUpload": "Batasi unggah fallback",
+        "limitFallbackDownload": "Batasi unduh fallback",
+        "afterBytes": "Setelah byte",
+        "afterBytesTip": "Biarkan fallback berjalan pada kecepatan penuh untuk sejumlah byte ini, lalu mulai membatasi. 0 = batasi sejak byte pertama.",
+        "bytesPerSec": "Byte per detik",
+        "bytesPerSecTip": "Batas kecepatan (byte/detik) yang diterapkan pada lalu lintas fallback setelah ambang batas, agar probe tidak dapat memakai server Anda sebagai bandwidth gratis menuju target. 0 = tanpa batas (menonaktifkan arah ini).",
+        "burstBytesPerSec": "Byte per detik burst",
+        "burstBytesPerSecTip": "Kelonggaran untuk burst singkat di atas laju tetap (ukuran token-bucket). Jika lebih kecil dari Byte per detik, nilainya dinaikkan agar sama."
       },
       "info": {
         "mode": "Mode",
@@ -1338,6 +1358,14 @@
       "Inbounds": "Inbound",
       "InboundsDesc": "Menerima klien tertentu.",
       "Outbounds": "Outbound",
+      "importRules": "Impor aturan",
+      "exportRules": "Ekspor aturan",
+      "importOutbounds": "Impor outbound",
+      "exportOutbounds": "Ekspor outbound",
+      "importInvalidJson": "JSON tidak valid — diharapkan berupa array atau objek dengan kunci yang sesuai.",
+      "metricsListen": "Endpoint metrik",
+      "metricsListenDesc": "Tampilkan metrik gaya Prometheus dari Xray pada alamat:port ini (mis. 127.0.0.1:11111). Biarkan kosong untuk menonaktifkan. Ikat ke localhost dan reverse-proxy — endpoint ini tanpa autentikasi.",
+      "metricsTag": "Tag metrik",
       "OutboundSubscriptions": "Langganan Outbound",
       "OutboundSubscriptionsDesc": "Impor outbound dari URL langganan jarak jauh (vmess/vless/trojan/ss/...). Tag dijaga tetap stabil untuk digunakan pada penyeimbang dan aturan routing. Pembaruan berjalan otomatis.",
       "Balancers": "Penyeimbang",
@@ -1729,7 +1757,6 @@
         "alpn": "ALPN",
         "fingerprint": "Fingerprint",
         "pins": "SHA-256 sertifikat tersemat",
-        "verifyPeerCertByName": "Verifikasi sertifikat peer berdasarkan nama",
         "allowInsecure": "Izinkan tidak aman",
         "echConfigList": "Daftar konfig ECH",
         "muxParams": "Mux",
@@ -1741,7 +1768,8 @@
         "shuffleHost": "Acak host",
         "tags": "Tag",
         "nodeGuids": "Node",
-        "excludeFromSubTypes": "Kecualikan dari format"
+        "excludeFromSubTypes": "Kecualikan dari format",
+        "verifyPeerCertByName": "Verifikasi sertifikat peer berdasarkan nama"
       },
       "hints": {
         "address": "Biarkan kosong untuk mewarisi alamat inbound itu sendiri.",
@@ -2015,4 +2043,4 @@
     "statusDown": "MATI",
     "statusUp": "AKTIF"
   }
-}
+}

+ 29 - 1
internal/web/translation/ja-JP.json

@@ -463,6 +463,27 @@
         "moreIssues": "{message}  (他 {count} 件)"
       },
       "form": {
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Xray が ECH config リストを取得するために使用する接続のソケットオプション(例: ルックアップを dialerProxy アウトバウンド経由にする)。既定値を使う場合は無効のままにします。",
+        "curvePreferences": "曲線の優先設定",
+        "curvePreferencesTip": "サーバーが提供する TLS 鍵交換の曲線を優先順位順に制限します(例: X25519MLKEM768, X25519)。空欄にすると Xray-core の既定値を使用します。",
+        "masterKeyLog": "マスターキーログ",
+        "masterKeyLogTip": "Wireshark でのデバッグ用に TLS マスターキー(SSLKEYLOGFILE 形式)を書き出すパス。本番環境では空欄にしてください — このファイルを持つ者は誰でも通信を復号できます。",
+        "verifyPeerCertByName": "名前でピア証明書を検証",
+        "verifyPeerCertByNameTip": "クライアントに対し、SNI ではなくこの名前でサーバー証明書を検証するよう指示します。名前はカンマ区切り。パネル専用 — 共有リンクに含まれます(vcn)。2026-06-01 以降 Xray が削除した allowInsecure の最新の代替手段です。",
+        "pinFromCert": "このインバウンドの証明書から入力",
+        "pinFromRemote": "SNI に ping して(xray tls ping)ハッシュを取得",
+        "pinFromRemoteNoSni": "まず SNI (serverName) を設定してリモート証明書に ping してください。",
+        "pinFromRemoteFailed": "リモート証明書のハッシュを取得できませんでした。",
+        "limitFallback": "Fallback を制限",
+        "limitFallbackUpload": "Fallback アップロードを制限",
+        "limitFallbackDownload": "Fallback ダウンロードを制限",
+        "afterBytes": "閾値バイト",
+        "afterBytesTip": "このバイト数までは fallback を全速で実行し、その後でスロットリングを開始します。0 = 最初のバイトからスロットリングします。",
+        "bytesPerSec": "バイト/秒",
+        "bytesPerSecTip": "閾値を超えた fallback トラフィックに適用される速度上限(バイト/秒)。プローブがサーバーを対象先への無料帯域として悪用できないようにします。0 = 無制限(この方向を無効化します)。",
+        "burstBytesPerSec": "バースト バイト/秒",
+        "burstBytesPerSecTip": "定常レートを超える短時間のバーストの許容量(token-bucket のサイズ)。バイト/秒より小さい場合は同じ値まで引き上げられます。",
         "moveUp": "上へ",
         "moveDown": "下へ",
         "addAll": "すべて追加",
@@ -591,7 +612,6 @@
         "pinnedPeerCertSha256": "ピン留めピア証明書 SHA-256",
         "pinnedPeerCertSha256Tip": "ピア証明書の SHA-256 ハッシュ(16進数文字列、例: e8e2d3…)、カンマ区切り。パネルのみ — サーバーの xray 設定には書き込まれませんが、共有リンクには含まれ、クライアントが証明書をピン留めできます。",
         "pinnedPeerCertSha256Placeholder": "16進ハッシュ、カンマ区切り",
-        "generateRandomPin": "ランダムハッシュを生成",
         "getNewEchCert": "新しい ECH 証明書を取得",
         "show": "表示",
         "xver": "Xver",
@@ -1294,6 +1314,14 @@
     },
     "xray": {
       "title": "Xray 設定",
+      "importRules": "ルールをインポート",
+      "exportRules": "ルールをエクスポート",
+      "importOutbounds": "アウトバウンドをインポート",
+      "exportOutbounds": "アウトバウンドをエクスポート",
+      "importInvalidJson": "無効な JSON — 配列、または一致するキーを持つオブジェクトが必要です。",
+      "metricsListen": "メトリクスエンドポイント",
+      "metricsListenDesc": "この アドレス:ポート で Xray の Prometheus 形式メトリクスを公開します(例: 127.0.0.1:11111)。空欄にすると無効になります。認証されないため、localhost にバインドしてリバースプロキシ経由で公開してください。",
+      "metricsTag": "メトリクスタグ",
       "save": "保存",
       "restart": "Xray を再起動",
       "restartSuccess": "Xrayの再起動に成功しました",

+ 29 - 1
internal/web/translation/pt-BR.json

@@ -463,6 +463,27 @@
         "moreIssues": "{message}  (+{count} mais)"
       },
       "form": {
+        "echSockopt": "Sockopt ECH",
+        "echSockoptTip": "Opções de socket para a conexão que o Xray usa ao buscar a lista de configurações ECH (por exemplo, rotear a consulta por um outbound dialerProxy). Deixe desativado para usar os padrões.",
+        "curvePreferences": "Preferências de curva",
+        "curvePreferencesTip": "Restringe as curvas de troca de chaves TLS que o servidor oferece, em ordem de preferência (por exemplo, X25519MLKEM768, X25519). Deixe vazio para usar os padrões do Xray-core.",
+        "masterKeyLog": "Log da chave mestra",
+        "masterKeyLogTip": "Caminho para gravar as chaves mestras TLS (formato SSLKEYLOGFILE) para depuração com o Wireshark. Deixe vazio em produção — isso permite que qualquer pessoa com o arquivo descriptografe o tráfego.",
+        "verifyPeerCertByName": "Verificar certificado do par pelo nome",
+        "verifyPeerCertByNameTip": "Instrui os clientes a verificar o certificado do servidor em relação a este nome em vez do SNI. Nomes separados por vírgula. Apenas do painel — incluído nos links de compartilhamento (vcn). A substituição moderna para allowInsecure, que o Xray removeu após 2026-06-01.",
+        "pinFromCert": "Preencher a partir do certificado desta entrada",
+        "pinFromRemote": "Obter o hash via ping ao SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "Defina primeiro o SNI (serverName) para fazer o ping no certificado remoto.",
+        "pinFromRemoteFailed": "Não foi possível obter o hash do certificado remoto.",
+        "limitFallback": "Limitar fallback",
+        "limitFallbackUpload": "Limitar upload do fallback",
+        "limitFallbackDownload": "Limitar download do fallback",
+        "afterBytes": "Após bytes",
+        "afterBytesTip": "Permite que o fallback opere em velocidade máxima por esta quantidade de bytes e depois começa a limitar. 0 = limitar a partir do primeiro byte.",
+        "bytesPerSec": "Bytes por seg",
+        "bytesPerSecTip": "Limite de velocidade (bytes/seg) aplicado ao tráfego de fallback após o limiar, para que sondagens não usem seu servidor como banda gratuita até o destino. 0 = sem limite (desativa esta direção).",
+        "burstBytesPerSec": "Bytes por seg em rajada",
+        "burstBytesPerSecTip": "Margem para rajadas curtas acima da taxa estável (tamanho do token-bucket). Se for menor que Bytes por seg, é elevado para corresponder.",
         "moveUp": "Mover para cima",
         "moveDown": "Mover para baixo",
         "addAll": "Adicionar todos",
@@ -591,7 +612,6 @@
         "pinnedPeerCertSha256": "SHA-256 do cert. do par fixado",
         "pinnedPeerCertSha256Tip": "Hashes SHA-256 do certificado do par como string hexadecimal (ex. e8e2d3…), separados por vírgula. Apenas no painel — não é gravado na config xray do servidor, mas é incluído nos links de compartilhamento para que clientes possam fixar o certificado.",
         "pinnedPeerCertSha256Placeholder": "hash(es) hexadecimal, separados por vírgula",
-        "generateRandomPin": "Gerar hash aleatório",
         "getNewEchCert": "Obter novo certificado ECH",
         "show": "Mostrar",
         "xver": "Xver",
@@ -1294,6 +1314,14 @@
     },
     "xray": {
       "title": "Configurações Xray",
+      "importRules": "Importar regras",
+      "exportRules": "Exportar regras",
+      "importOutbounds": "Importar saídas",
+      "exportOutbounds": "Exportar saídas",
+      "importInvalidJson": "JSON inválido — esperava-se um array ou um objeto com uma chave correspondente.",
+      "metricsListen": "Endpoint de métricas",
+      "metricsListenDesc": "Expõe as métricas no estilo Prometheus do Xray neste endereço:porta (por exemplo, 127.0.0.1:11111). Deixe vazio para desativar. Vincule ao localhost e use um proxy reverso — ele não é autenticado.",
+      "metricsTag": "Tag de métricas",
       "save": "Salvar",
       "restart": "Reiniciar Xray",
       "restartSuccess": "Xray foi reiniciado com sucesso",

+ 29 - 1
internal/web/translation/ru-RU.json

@@ -463,6 +463,27 @@
         "moreIssues": "{message}  (+{count} ещё)"
       },
       "form": {
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Параметры сокета для соединения, через которое Xray получает список конфигураций ECH (например, можно направить запрос через исходящее dialerProxy). Оставьте отключённым для значений по умолчанию.",
+        "curvePreferences": "Предпочтения кривых",
+        "curvePreferencesTip": "Ограничьте кривые обмена ключами TLS, которые предлагает сервер, в порядке предпочтения (например, X25519MLKEM768, X25519). Оставьте пустым для значений по умолчанию Xray-core.",
+        "masterKeyLog": "Лог мастер-ключей",
+        "masterKeyLogTip": "Путь для записи мастер-ключей TLS (формат SSLKEYLOGFILE) для отладки с помощью Wireshark. В рабочей среде оставляйте пустым — любой, у кого есть этот файл, сможет расшифровать трафик.",
+        "verifyPeerCertByName": "Проверять сертификат пира по имени",
+        "verifyPeerCertByNameTip": "Указывает клиентам проверять сертификат сервера по этому имени вместо SNI. Имена через запятую. Только для панели — включается в ссылки для обмена (vcn). Современная замена allowInsecure, который Xray удалил после 2026-06-01.",
+        "pinFromCert": "Заполнить из сертификата этого входящего",
+        "pinFromRemote": "Получить хеш, пингуя SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "Сначала задайте SNI (serverName), чтобы пинговать удалённый сертификат.",
+        "pinFromRemoteFailed": "Не удалось получить хеш удалённого сертификата.",
+        "limitFallback": "Ограничение fallback",
+        "limitFallbackUpload": "Ограничение загрузки fallback",
+        "limitFallbackDownload": "Ограничение скачивания fallback",
+        "afterBytes": "После байтов",
+        "afterBytesTip": "Позволяет fallback работать на полной скорости в течение этого количества байтов, после чего начинается ограничение. 0 = ограничивать с первого байта.",
+        "bytesPerSec": "Байт в секунду",
+        "bytesPerSecTip": "Ограничение скорости (байт/с) для трафика fallback после порога, чтобы пробы не могли использовать ваш сервер как бесплатный канал к цели. 0 = без ограничения (отключает это направление).",
+        "burstBytesPerSec": "Пиковые байты в секунду",
+        "burstBytesPerSecTip": "Допуск для коротких всплесков выше постоянной скорости (размер token-bucket). Если меньше, чем «Байт в секунду», повышается до этого значения.",
         "moveUp": "Вверх",
         "moveDown": "Вниз",
         "addAll": "Добавить все",
@@ -591,7 +612,6 @@
         "pinnedPeerCertSha256": "SHA-256 сертификата пира",
         "pinnedPeerCertSha256Tip": "SHA-256-хеши сертификата пира в виде шестнадцатеричной строки (напр. e8e2d3…), через запятую. Только для панели — не записывается в конфиг xray сервера, но включается в ссылки-приглашения, чтобы клиенты могли закрепить сертификат.",
         "pinnedPeerCertSha256Placeholder": "шестнадцатеричный хеш(и), через запятую",
-        "generateRandomPin": "Сгенерировать случайный хеш",
         "getNewEchCert": "Получить новый ECH-сертификат",
         "show": "Показать",
         "xver": "Xver",
@@ -1293,6 +1313,14 @@
       "remarkTemplateDesc": "Если задан, заменяет модель примечания для каждой ссылки подписки — задайте собственный формат с помощью токенов переменных (используйте кнопку для их вставки). Оставьте пустым, чтобы использовать модель выше."
     },
     "xray": {
+      "importRules": "Импорт правил",
+      "exportRules": "Экспорт правил",
+      "importOutbounds": "Импорт исходящих",
+      "exportOutbounds": "Экспорт исходящих",
+      "importInvalidJson": "Некорректный JSON — ожидался массив или объект с подходящим ключом.",
+      "metricsListen": "Эндпоинт метрик",
+      "metricsListenDesc": "Публикует метрики Xray в стиле Prometheus по этому адресу:порту (например, 127.0.0.1:11111). Оставьте пустым, чтобы отключить. Привяжите к localhost и проксируйте через reverse-proxy — он без аутентификации.",
+      "metricsTag": "Тег метрик",
       "title": "Настройки Xray",
       "save": "Сохранить",
       "restart": "Перезапуск Xray",

+ 33 - 5
internal/web/translation/tr-TR.json

@@ -591,7 +591,6 @@
         "pinnedPeerCertSha256": "Sabitlenmiş Peer Sertifikası SHA-256",
         "pinnedPeerCertSha256Tip": "Peer sertifikasının SHA-256 hash'leri onaltılık (hex) dizge olarak (örn. e8e2d3…), virgülle ayrılmış. Sadece panel — sunucunun Xray yapılandırmasına yazılmaz, ancak kullanıcıların sertifikayı sabitleyebilmesi için paylaşım bağlantılarına eklenir.",
         "pinnedPeerCertSha256Placeholder": "onaltılık (hex) hash(ler), virgülle ayrılmış",
-        "generateRandomPin": "Rastgele Hash Üret",
         "getNewEchCert": "Yeni ECH Sertifikası Al",
         "show": "Göster",
         "xver": "Xver",
@@ -620,7 +619,28 @@
           "node": "Düğüm adresi",
           "listen": "Inbound dinleme adresi",
           "custom": "Özel"
-        }
+        },
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Xray'in ECH yapılandırma listesini almak için kullandığı bağlantının soket seçenekleri (ör. aramayı bir dialerProxy giden bağlantısı üzerinden yönlendirme). Varsayılanları kullanmak için devre dışı bırakın.",
+        "curvePreferences": "Eğri Tercihleri",
+        "curvePreferencesTip": "Sunucunun sunduğu TLS anahtar değişim eğrilerini tercih sırasına göre kısıtlayın (ör. X25519MLKEM768, X25519). Xray-core varsayılanlarını kullanmak için boş bırakın.",
+        "masterKeyLog": "Ana Anahtar Günlüğü",
+        "masterKeyLogTip": "Wireshark ile hata ayıklama için TLS ana anahtarlarının yazılacağı yol (SSLKEYLOGFILE biçimi). Üretimde boş bırakın — dosyaya erişimi olan herkesin trafiği çözmesine olanak tanır.",
+        "verifyPeerCertByName": "Peer sertifikasını ada göre doğrula",
+        "verifyPeerCertByNameTip": "İstemcilere sunucu sertifikasını SNI yerine bu ada göre doğrulamalarını söyler. Adları virgülle ayırın. Yalnızca panele özgü — paylaşım bağlantılarına eklenir (vcn). Xray'in 2026-06-01 sonrası kaldırdığı allowInsecure'un modern karşılığıdır.",
+        "pinFromCert": "Bu gelen bağlantının sertifikasından doldur",
+        "pinFromRemote": "SNI'ye ping atarak hash'i al (xray tls ping)",
+        "pinFromRemoteNoSni": "Uzak sertifikaya ping atmak için önce SNI'yi (serverName) ayarlayın.",
+        "pinFromRemoteFailed": "Uzak sertifika hash'i alınamadı.",
+        "limitFallback": "Fallback Sınırı",
+        "limitFallbackUpload": "Fallback Yükleme Sınırı",
+        "limitFallbackDownload": "Fallback İndirme Sınırı",
+        "afterBytes": "Bayttan Sonra",
+        "afterBytesTip": "Fallback'in bu kadar bayt boyunca tam hızda çalışmasına izin verin, ardından kısıtlamaya başlayın. 0 = ilk bayttan itibaren kısıtla.",
+        "bytesPerSec": "Saniye Başına Bayt",
+        "bytesPerSecTip": "Eşik aşıldıktan sonra fallback trafiğine uygulanan hız sınırı (bayt/sn); böylece sondalar sunucunuzu hedefe ücretsiz bant genişliği olarak kullanamaz. 0 = sınır yok (bu yönü devre dışı bırakır).",
+        "burstBytesPerSec": "Saniye Başına Patlama Baytı",
+        "burstBytesPerSecTip": "Sabit hızın üzerindeki kısa patlamalar için pay (token-bucket boyutu). Saniye Başına Bayt değerinden düşükse ona eşitlenecek şekilde yükseltilir."
       },
       "info": {
         "mode": "Mod",
@@ -1334,6 +1354,14 @@
       "RoutingStrategyDesc": "Tüm istekleri çözmek için genel trafik yönlendirme stratejisini ayarlayın.",
       "outboundTestUrl": "Giden Bağlantı Test URL'si",
       "outboundTestUrlDesc": "Giden bağlantı bağlantı testinde kullanılan URL.",
+      "importRules": "Kuralları İçe Aktar",
+      "exportRules": "Kuralları Dışa Aktar",
+      "importOutbounds": "Giden Bağlantıları İçe Aktar",
+      "exportOutbounds": "Giden Bağlantıları Dışa Aktar",
+      "importInvalidJson": "Geçersiz JSON — bir dizi veya eşleşen anahtara sahip bir nesne bekleniyordu.",
+      "metricsListen": "Metrik Uç Noktası",
+      "metricsListenDesc": "Xray'in Prometheus tarzı metriklerini bu adres:port üzerinde sunar (ör. 127.0.0.1:11111). Devre dışı bırakmak için boş bırakın. localhost'a bağlayın ve ters proxy ile sunun — kimlik doğrulaması yoktur.",
+      "metricsTag": "Metrik Etiketi",
       "Torrent": "BitTorrent Protokolünü Engelle",
       "Inbounds": "Gelen Bağlantılar",
       "InboundsDesc": "Belirtilen istemcileri (clients) kabul eder.",
@@ -1729,7 +1757,6 @@
         "alpn": "ALPN",
         "fingerprint": "Fingerprint",
         "pins": "Sabitlenmiş sertifika SHA-256",
-        "verifyPeerCertByName": "Peer sertifikasını ada göre doğrula",
         "allowInsecure": "Güvensize izin ver",
         "echConfigList": "ECH yapılandırma listesi",
         "muxParams": "Mux",
@@ -1741,7 +1768,8 @@
         "shuffleHost": "Host'u karıştır",
         "tags": "Etiketler",
         "nodeGuids": "Düğümler",
-        "excludeFromSubTypes": "Formatlardan hariç tut"
+        "excludeFromSubTypes": "Formatlardan hariç tut",
+        "verifyPeerCertByName": "Peer sertifikasını ada göre doğrula"
       },
       "hints": {
         "address": "Gelen bağlantının kendi adresini devralmak için boş bırakın.",
@@ -2015,4 +2043,4 @@
     "statusDown": "ÇEVRİMDIŞI",
     "statusUp": "ÇEVRİMİÇİ"
   }
-}
+}

+ 29 - 1
internal/web/translation/uk-UA.json

@@ -591,7 +591,6 @@
         "pinnedPeerCertSha256": "Закріплений SHA-256 сертифіката пира",
         "pinnedPeerCertSha256Tip": "SHA-256-хеші сертифіката пира у вигляді шістнадцяткового рядка (напр. e8e2d3…), через кому. Лише панель — не записується в конфіг xray сервера, але додається до посилань спільного доступу, щоб клієнти могли закріпити сертифікат.",
         "pinnedPeerCertSha256Placeholder": "шістнадцятковий хеш(і), через кому",
-        "generateRandomPin": "Згенерувати випадковий хеш",
         "getNewEchCert": "Отримати новий ECH-сертифікат",
         "show": "Показати",
         "xver": "Xver",
@@ -616,6 +615,27 @@
         "shareAddrHelp": "Використовується лише коли стратегія адреси поширення — користувацька. Введіть хост або IP без схеми та порту.",
         "subSortIndex": "Порядок у підписці",
         "subSortIndexHelp": "Позиція посилань цього вхідного у виводі підписки (сторінка підписки та клієнтські застосунки). Менші значення йдуть першими; за однакових значень зберігається порядок створення. Не впливає на список вхідних у панелі.",
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Параметри сокета для з'єднання, яке Xray використовує для отримання списку конфігурацій ECH (наприклад, спрямувати запит через вихідний dialerProxy). Залиште вимкненим, щоб використовувати типові значення.",
+        "curvePreferences": "Налаштування кривих",
+        "curvePreferencesTip": "Обмежує криві обміну ключами TLS, які пропонує сервер, у порядку переваги (наприклад, X25519MLKEM768, X25519). Залиште порожнім, щоб використовувати типові значення Xray-core.",
+        "masterKeyLog": "Журнал майстер-ключів",
+        "masterKeyLogTip": "Шлях для запису майстер-ключів TLS (формат SSLKEYLOGFILE) для налагодження за допомогою Wireshark. Залиште порожнім у продакшені — це дозволяє будь-кому з доступом до файлу розшифрувати трафік.",
+        "verifyPeerCertByName": "Перевіряти сертифікат пира за іменем",
+        "verifyPeerCertByNameTip": "Вказує клієнтам перевіряти сертифікат сервера за цим іменем замість SNI. Імена через кому. Лише для панелі — включається до посилань спільного доступу (vcn). Сучасна заміна allowInsecure, який Xray видалив після 2026-06-01.",
+        "pinFromCert": "Заповнити з сертифіката цього вхідного",
+        "pinFromRemote": "Отримати хеш пінгуванням SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "Спочатку вкажіть SNI (serverName), щоб пінгувати віддалений сертифікат.",
+        "pinFromRemoteFailed": "Не вдалося отримати хеш віддаленого сертифіката.",
+        "limitFallback": "Обмеження fallback",
+        "limitFallbackUpload": "Обмеження вивантаження fallback",
+        "limitFallbackDownload": "Обмеження завантаження fallback",
+        "afterBytes": "Після байтів",
+        "afterBytesTip": "Дозволяє fallback працювати на повній швидкості протягом цієї кількості байтів, а потім починає обмежувати. 0 = обмежувати з першого байта.",
+        "bytesPerSec": "Байтів за секунду",
+        "bytesPerSecTip": "Обмеження швидкості (байтів/сек) для трафіку fallback після перевищення порогу, щоб зонди не могли використовувати ваш сервер як безкоштовний канал до цілі. 0 = без обмеження (вимикає цей напрямок).",
+        "burstBytesPerSec": "Пікових байтів за секунду",
+        "burstBytesPerSecTip": "Допуск для коротких сплесків понад сталу швидкість (розмір token-bucket). Якщо менше за «Байтів за секунду», піднімається до цього значення.",
         "shareAddrStrategyOptions": {
           "node": "Адреса вузла",
           "listen": "Адреса прослуховування inbound",
@@ -1361,6 +1381,14 @@
       "OutboundsDesc": "Встановити шлях вихідного трафіку.",
       "Routings": "Правила маршрутизації",
       "RoutingsDesc": "Пріоритет кожного правила важливий!",
+      "importRules": "Імпортувати правила",
+      "exportRules": "Експортувати правила",
+      "importOutbounds": "Імпортувати вихідні",
+      "exportOutbounds": "Експортувати вихідні",
+      "importInvalidJson": "Недійсний JSON — очікувався масив або об'єкт із відповідним ключем.",
+      "metricsListen": "Точка доступу метрик",
+      "metricsListenDesc": "Надає метрики Xray у стилі Prometheus за цією адресою:порт (наприклад, 127.0.0.1:11111). Залиште порожнім, щоб вимкнути. Прив'яжіть до localhost і використовуйте зворотний проксі — доступ без автентифікації.",
+      "metricsTag": "Тег метрик",
       "completeTemplate": "Усі",
       "logLevel": "Рівень журналу",
       "logLevelDesc": "Рівень журналу для журналів помилок із зазначенням інформації, яку потрібно записати.",

+ 29 - 1
internal/web/translation/vi-VN.json

@@ -463,6 +463,27 @@
         "moreIssues": "{message}  (+{count} lỗi khác)"
       },
       "form": {
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Tùy chọn socket cho kết nối mà Xray dùng để lấy danh sách cấu hình ECH (ví dụ định tuyến truy vấn qua một outbound dialerProxy). Để tắt để dùng giá trị mặc định.",
+        "curvePreferences": "Ưu tiên đường cong",
+        "curvePreferencesTip": "Giới hạn các đường cong trao đổi khóa TLS mà máy chủ cung cấp, theo thứ tự ưu tiên (ví dụ X25519MLKEM768, X25519). Để trống để dùng giá trị mặc định của Xray-core.",
+        "masterKeyLog": "Nhật ký khóa chính",
+        "masterKeyLogTip": "Đường dẫn để ghi các khóa chính TLS (định dạng SSLKEYLOGFILE) phục vụ gỡ lỗi bằng Wireshark. Để trống khi chạy thực tế — vì ai có tệp này đều có thể giải mã lưu lượng.",
+        "verifyPeerCertByName": "Xác minh chứng chỉ peer theo tên",
+        "verifyPeerCertByNameTip": "Yêu cầu máy khách xác minh chứng chỉ máy chủ theo tên này thay vì theo SNI. Các tên cách nhau bằng dấu phẩy. Chỉ dùng trong bảng điều khiển — được đính kèm trong link chia sẻ (vcn). Đây là giải pháp thay thế hiện đại cho allowInsecure, vốn đã bị Xray gỡ bỏ sau 2026-06-01.",
+        "pinFromCert": "Điền từ chứng chỉ của inbound này",
+        "pinFromRemote": "Lấy hash bằng cách ping SNI (xray tls ping)",
+        "pinFromRemoteNoSni": "Hãy đặt SNI (serverName) trước để ping chứng chỉ từ xa.",
+        "pinFromRemoteFailed": "Không thể lấy hash chứng chỉ từ xa.",
+        "limitFallback": "Giới hạn fallback",
+        "limitFallbackUpload": "Giới hạn fallback tải lên",
+        "limitFallbackDownload": "Giới hạn fallback tải xuống",
+        "afterBytes": "Sau số byte",
+        "afterBytesTip": "Cho phép fallback chạy hết tốc độ trong bấy nhiêu byte này, sau đó mới bắt đầu giới hạn. 0 = giới hạn ngay từ byte đầu tiên.",
+        "bytesPerSec": "Byte mỗi giây",
+        "bytesPerSecTip": "Giới hạn tốc độ (byte/giây) áp dụng cho lưu lượng fallback sau ngưỡng, để các đầu dò không thể dùng máy chủ của bạn làm băng thông miễn phí tới đích. 0 = không giới hạn (tắt hướng này).",
+        "burstBytesPerSec": "Byte bùng phát mỗi giây",
+        "burstBytesPerSecTip": "Mức cho phép các đợt bùng phát ngắn vượt tốc độ ổn định (kích thước token-bucket). Nếu thấp hơn Byte mỗi giây thì sẽ được nâng lên cho khớp.",
         "moveUp": "Lên",
         "moveDown": "Xuống",
         "addAll": "Thêm tất cả",
@@ -591,7 +612,6 @@
         "pinnedPeerCertSha256": "SHA-256 chứng chỉ peer đã ghim",
         "pinnedPeerCertSha256Tip": "Hash SHA-256 của chứng chỉ peer dưới dạng chuỗi thập lục phân (vd. e8e2d3…), phân tách bằng dấu phẩy. Chỉ panel — không ghi vào cấu hình xray máy chủ, nhưng được đưa vào liên kết chia sẻ để client có thể ghim chứng chỉ.",
         "pinnedPeerCertSha256Placeholder": "hash thập lục phân, phân tách bằng dấu phẩy",
-        "generateRandomPin": "Tạo hash ngẫu nhiên",
         "getNewEchCert": "Lấy chứng chỉ ECH mới",
         "show": "Hiện",
         "xver": "Xver",
@@ -1294,6 +1314,14 @@
     },
     "xray": {
       "title": "Cài đặt Xray",
+      "importRules": "Nhập quy tắc",
+      "exportRules": "Xuất quy tắc",
+      "importOutbounds": "Nhập outbound",
+      "exportOutbounds": "Xuất outbound",
+      "importInvalidJson": "JSON không hợp lệ — cần một mảng hoặc một đối tượng có khóa khớp.",
+      "metricsListen": "Điểm cuối Metrics",
+      "metricsListenDesc": "Hiển thị các chỉ số kiểu Prometheus của Xray tại địa chỉ:cổng này (ví dụ 127.0.0.1:11111). Để trống để tắt. Hãy gắn vào localhost và reverse-proxy nó — vì nó không có xác thực.",
+      "metricsTag": "Metrics Tag",
       "save": "Lưu cài đặt",
       "restart": "Khởi động lại Xray",
       "restartSuccess": "Đã khởi động lại Xray thành công",

+ 31 - 3
internal/web/translation/zh-CN.json

@@ -463,6 +463,26 @@
         "moreIssues": "{message}  (另有 {count} 项)"
       },
       "form": {
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Xray 获取 ECH 配置列表时所用连接的 Socket 选项(例如让该查询通过 dialerProxy 出站)。保持禁用则使用默认值。",
+        "curvePreferences": "曲线偏好",
+        "curvePreferencesTip": "限制服务器提供的 TLS 密钥交换曲线,并按偏好顺序排列(例如 X25519MLKEM768、X25519)。留空则使用 Xray-core 默认值。",
+        "masterKeyLog": "主密钥日志",
+        "masterKeyLogTip": "写入 TLS 主密钥的路径(SSLKEYLOGFILE 格式),用于配合 Wireshark 调试。生产环境请留空——任何拥有该文件的人都能解密流量。",
+        "verifyPeerCertByNameTip": "让客户端以此名称(而非 SNI)验证服务器证书。多个名称用逗号分隔。仅面板使用——会包含在分享链接中(vcn)。这是 allowInsecure 的现代替代方案,Xray 已在 2026-06-01 之后将其移除。",
+        "pinFromCert": "从此入站的证书填充",
+        "pinFromRemote": "通过 ping SNI 获取哈希(xray tls ping)",
+        "pinFromRemoteNoSni": "请先设置 SNI(serverName)才能 ping 远端证书。",
+        "pinFromRemoteFailed": "无法获取远端证书哈希。",
+        "limitFallback": "限制 Fallback",
+        "limitFallbackUpload": "限制 Fallback 上传",
+        "limitFallbackDownload": "限制 Fallback 下载",
+        "afterBytes": "起始字节数",
+        "afterBytesTip": "允许 fallback 以全速运行此字节数,之后开始限速。0 = 从第一个字节起就限速。",
+        "bytesPerSec": "每秒字节数",
+        "bytesPerSecTip": "在达到阈值后对 fallback 流量施加的速度上限(字节/秒),以防探测者把你的服务器当作通往目标的免费带宽。0 = 无限制(禁用此方向)。",
+        "burstBytesPerSec": "突发每秒字节数",
+        "burstBytesPerSecTip": "允许在稳定速率之上的短时突发额度(token-bucket 容量)。若低于“每秒字节数”,则会被提升至与之相同。",
         "moveUp": "上移",
         "moveDown": "下移",
         "addAll": "全部添加",
@@ -591,7 +611,6 @@
         "pinnedPeerCertSha256": "固定对端证书 SHA-256",
         "pinnedPeerCertSha256Tip": "对端证书的 SHA-256 哈希(十六进制字符串,如 e8e2d3…),逗号分隔。仅面板使用 — 不写入服务器的 xray 配置,但会包含在分享链接中,以便客户端固定证书。",
         "pinnedPeerCertSha256Placeholder": "十六进制哈希,逗号分隔",
-        "generateRandomPin": "生成随机哈希",
         "getNewEchCert": "获取新 ECH 证书",
         "show": "显示",
         "xver": "Xver",
@@ -620,7 +639,8 @@
           "node": "节点地址",
           "listen": "入站监听地址",
           "custom": "自定义"
-        }
+        },
+        "verifyPeerCertByName": "按名称验证对端证书"
       },
       "info": {
         "mode": "模式",
@@ -1293,6 +1313,14 @@
       "remarkTemplateDesc": "设置后,将替换每个订阅链接的备注模型 — 使用变量标记编写您自己的格式(用按钮插入它们)。留空则使用上方的模型。"
     },
     "xray": {
+      "importRules": "导入规则",
+      "exportRules": "导出规则",
+      "importOutbounds": "导入出站",
+      "exportOutbounds": "导出出站",
+      "importInvalidJson": "无效的 JSON——应为数组或包含匹配键的对象。",
+      "metricsListen": "指标端点",
+      "metricsListenDesc": "在此 address:port 上暴露 Xray 的 Prometheus 风格指标(例如 127.0.0.1:11111)。留空则禁用。请绑定到本地回环并通过反向代理转发——它没有身份验证。",
+      "metricsTag": "指标标签",
       "title": "Xray 配置",
       "save": "保存",
       "restart": "重启 Xray",
@@ -2015,4 +2043,4 @@
     "statusDown": "断开",
     "statusUp": "恢复"
   }
-}
+}

+ 29 - 1
internal/web/translation/zh-TW.json

@@ -591,7 +591,6 @@
         "pinnedPeerCertSha256": "釘選對端憑證 SHA-256",
         "pinnedPeerCertSha256Tip": "對端憑證的 SHA-256 雜湊(十六進位字串,如 e8e2d3…),以逗號分隔。僅面板使用 — 不寫入伺服器的 xray 設定,但會包含在分享連結中,以便用戶端釘選憑證。",
         "pinnedPeerCertSha256Placeholder": "十六進位雜湊,以逗號分隔",
-        "generateRandomPin": "產生隨機雜湊",
         "getNewEchCert": "取得新 ECH 憑證",
         "show": "顯示",
         "xver": "Xver",
@@ -609,6 +608,27 @@
         "mldsa65Seed": "mldsa65 Seed",
         "mldsa65Verify": "mldsa65 Verify",
         "getNewSeed": "取得新 Seed",
+        "echSockopt": "ECH Sockopt",
+        "echSockoptTip": "Xray 用來取得 ECH 設定清單的連線之通訊端選項(例如讓查詢透過 dialerProxy 出站)。停用則使用預設值。",
+        "curvePreferences": "曲線偏好",
+        "curvePreferencesTip": "依偏好順序限制伺服器提供的 TLS 金鑰交換曲線(例如 X25519MLKEM768、X25519)。留空則使用 Xray-core 預設值。",
+        "masterKeyLog": "主金鑰日誌",
+        "masterKeyLogTip": "寫入 TLS 主金鑰的路徑(SSLKEYLOGFILE 格式),用於以 Wireshark 除錯。正式環境請留空——擁有該檔案的任何人都能解密流量。",
+        "verifyPeerCertByName": "依名稱驗證對端憑證",
+        "verifyPeerCertByNameTip": "要求客戶端以此名稱(而非 SNI)驗證伺服器憑證。以逗號分隔多個名稱。僅面板使用——會包含在分享連結中(vcn)。這是 allowInsecure 的新式替代方案,Xray 已於 2026-06-01 之後移除 allowInsecure。",
+        "pinFromCert": "從此入站的憑證填入",
+        "pinFromRemote": "透過 ping SNI 取得雜湊值(xray tls ping)",
+        "pinFromRemoteNoSni": "請先設定 SNI(serverName)才能 ping 遠端憑證。",
+        "pinFromRemoteFailed": "無法取得遠端憑證雜湊值。",
+        "limitFallback": "限制 Fallback",
+        "limitFallbackUpload": "限制 Fallback 上傳",
+        "limitFallbackDownload": "限制 Fallback 下載",
+        "afterBytes": "起算位元組",
+        "afterBytesTip": "讓 fallback 以全速傳輸此數量的位元組後,再開始限速。0 = 從第一個位元組起即限速。",
+        "bytesPerSec": "每秒位元組",
+        "bytesPerSecTip": "在達到門檻後套用於 fallback 流量的速度上限(位元組/秒),使探測無法將你的伺服器當成通往目標的免費頻寬。0 = 不限制(停用此方向)。",
+        "burstBytesPerSec": "每秒突發位元組",
+        "burstBytesPerSecTip": "允許短暫超出穩定速率的額度(token-bucket 大小)。若低於「每秒位元組」,則會提升至與其相同。",
         "listenHelp": "也可以填寫 Unix socket 路徑(例如 /run/xray/in.sock),或以 @ 為前綴的抽象通訊端名稱(例如 @xray/in.sock),以使用通訊端而非 TCP 連接埠監聽——此時請將連接埠設為 0。",
         "shareAddrStrategy": "分享地址策略",
         "shareAddrStrategyHelp": "控制匯出分享連結、QR Code 和訂閱輸出時寫入哪個地址。",
@@ -1303,6 +1323,14 @@
       "stopSuccess": "Xray 已成功停止",
       "restartError": "重新啟動Xray時發生錯誤。",
       "stopError": "停止Xray時發生錯誤。",
+      "importRules": "匯入規則",
+      "exportRules": "匯出規則",
+      "importOutbounds": "匯入出站",
+      "exportOutbounds": "匯出出站",
+      "importInvalidJson": "無效的 JSON——預期為陣列或含有相符鍵的物件。",
+      "metricsListen": "Metrics 端點",
+      "metricsListenDesc": "在此 address:port 上公開 Xray 的 Prometheus 風格 metrics(例如 127.0.0.1:11111)。留空則停用。請綁定 localhost 並以反向代理轉發——此端點未經驗證。",
+      "metricsTag": "Metrics 標籤",
       "basicTemplate": "基礎配置",
       "advancedTemplate": "高階配置",
       "generalConfigs": "常規配置",