4 Angajamente ec0a57fc4f ... c7d89ceac2

Autor SHA1 Permisiunea de a trimite mesaje. Dacă este dezactivată, utilizatorul nu va putea trimite nici un fel de mesaj Data
  MHSanaei c7d89ceac2 feat(outbounds): TLS for XDRIVE outbounds 9 ore în urmă
  MHSanaei ef4bda4e06 feat(inbounds): TUN adapter type and auto-system options 9 ore în urmă
  MHSanaei b6c859f5b9 feat(xray): edit Lua DNS and routing scripts 9 ore în urmă
  MHSanaei 1c050f872a feat(finalmask): header-custom variables, sudoku UDP mask and noise reset ranges 9 ore în urmă
44 a modificat fișierele cu 1141 adăugiri și 106 ștergeri
  1. 3 1
      docs/content/docs/en/config/transports.mdx
  2. 14 0
      docs/content/docs/en/operations/outbounds-routing.mdx
  3. 3 1
      docs/content/docs/fa/config/transports.mdx
  4. 14 0
      docs/content/docs/fa/operations/outbounds-routing.mdx
  5. 2 0
      docs/content/docs/ru/config/transports.mdx
  6. 14 0
      docs/content/docs/ru/operations/outbounds-routing.mdx
  7. 2 1
      docs/content/docs/zh/config/transports.mdx
  8. 12 0
      docs/content/docs/zh/operations/outbounds-routing.mdx
  9. 2 2
      frontend/package.json
  10. 228 51
      frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx
  11. 9 0
      frontend/src/lib/xray/protocol-capabilities.ts
  12. 34 1
      frontend/src/pages/inbounds/form/protocols/tun.tsx
  13. 20 0
      frontend/src/pages/xray/basics/BasicsTab.tsx
  14. 7 0
      frontend/src/pages/xray/dns/DnsServerModal.tsx
  15. 12 0
      frontend/src/pages/xray/dns/DnsTab.tsx
  16. 2 2
      frontend/src/pages/xray/outbounds/OutboundFormModal.tsx
  17. 2 2
      frontend/src/pages/xray/outbounds/outbound-form-helpers.ts
  18. 10 1
      frontend/src/pages/xray/routing/RoutingTab.tsx
  19. 2 0
      frontend/src/schemas/protocols/inbound/tun.ts
  20. 1 1
      frontend/src/schemas/protocols/stream/finalmask.ts
  21. 1 0
      frontend/src/schemas/xray.ts
  22. 101 0
      frontend/src/test/__snapshots__/finalmask.test.ts.snap
  23. 16 1
      frontend/src/test/dns-server-modal.test.tsx
  24. 26 2
      frontend/src/test/dns-tab.test.tsx
  25. 54 0
      frontend/src/test/golden/fixtures/finalmask/header-custom-variables.json
  26. 43 0
      frontend/src/test/inbound-form-modal.test.tsx
  27. 3 1
      frontend/src/test/inbound-from-db.test.ts
  28. 206 0
      frontend/src/test/outbound-form-modal.test.tsx
  29. 75 0
      frontend/src/test/routing-script.test.tsx
  30. 1 0
      internal/sub/service.go
  31. 14 0
      internal/sub/service_test.go
  32. 16 3
      internal/web/translation/ar-EG.json
  33. 16 3
      internal/web/translation/en-US.json
  34. 16 3
      internal/web/translation/es-ES.json
  35. 16 3
      internal/web/translation/fa-IR.json
  36. 16 3
      internal/web/translation/id-ID.json
  37. 16 3
      internal/web/translation/ja-JP.json
  38. 16 3
      internal/web/translation/pt-BR.json
  39. 16 3
      internal/web/translation/ru-RU.json
  40. 16 3
      internal/web/translation/tr-TR.json
  41. 16 3
      internal/web/translation/uk-UA.json
  42. 16 3
      internal/web/translation/vi-VN.json
  43. 16 3
      internal/web/translation/zh-CN.json
  44. 16 3
      internal/web/translation/zh-TW.json

+ 3 - 1
docs/content/docs/en/config/transports.mdx

@@ -135,7 +135,9 @@ client; XDRIVE inbounds get no share link or Clash entry.
 
 The optional **Front address/port** (the stream-level `address`/`port`) is a domain
 front the storage API is dialed through; TLS still names the API host. XDRIVE has
-no TLS/REALITY layer of its own — its traffic is the storage API's HTTPS.
+no TLS/REALITY layer of its own — its traffic is the storage API's HTTPS. On an
+outbound, **TLS** security shapes that HTTPS connection: its SNI, uTLS fingerprint
+and ALPN are what the storage API sees.
 
 ## FinalMask — late-layer obfuscation
 

+ 14 - 0
docs/content/docs/en/operations/outbounds-routing.mdx

@@ -151,6 +151,20 @@ put specific rules above general ones.
 }
 ```
 
+### Lua scripts
+
+Xray 26.10.10 can hand routing and DNS to Lua. **Basics → Routing Script** takes the
+path of a `.lua` file that defines `HandleRoute` (absolute, or relative to the Xray
+`bin` folder); **DNS → Lua Script** takes one that defines `HandleDNSQuery`, and a
+DNS server's **ID** is the name such a script picks it by.
+
+<Callout type="warn">
+  A routing script replaces **every** routing rule, the panel's own `api` rule
+  included. Send the `api` inbound to the `api` outbound in your script, or traffic
+  statistics and live client changes stop. Xray does not start if the file is missing
+  or fails to load.
+</Callout>
+
 ## Balancers
 
 A **balancer** groups outbounds by a **selector** (tag prefixes, including the

+ 3 - 1
docs/content/docs/fa/config/transports.mdx

@@ -136,7 +136,9 @@ XDRIVE جریان را به‌صورت فایل‌هایی در پوشه‌ای
 
 **آدرس/پورت Front** اختیاری (کلیدهای `address`/`port` در سطح stream) یک domain front
 است که API ذخیره‌ساز از طریق آن وصل می‌شود؛ نام TLS همچنان میزبان API است. XDRIVE لایهٔ
-TLS/REALITY جداگانه‌ای ندارد — ترافیکش همان HTTPS خود API ذخیره‌ساز است.
+TLS/REALITY جداگانه‌ای ندارد — ترافیکش همان HTTPS خود API ذخیره‌ساز است. در اوت‌باند،
+امنیت **TLS** همین اتصال HTTPS را شکل می‌دهد: API ذخیره‌ساز همان SNI، اثرانگشت uTLS و
+ALPN آن را می‌بیند.
 
 ## FinalMask — مبهم‌سازی لایه پایانی
 

+ 14 - 0
docs/content/docs/fa/operations/outbounds-routing.mdx

@@ -152,6 +152,20 @@ Reset یا حذف، peer مربوط به WireGuard را در حساب PIA باط
 }
 ```
 
+### اسکریپت‌های Lua
+
+Xray 26.10.10 می‌تواند مسیریابی و DNS را به Lua بسپارد. **پایه ← اسکریپت مسیریابی**
+مسیر یک فایل `.lua` را می‌گیرد که `HandleRoute` را تعریف می‌کند (مطلق یا نسبت به
+پوشه‌ی `bin` در Xray)؛ **DNS ← اسکریپت Lua** فایلی را می‌گیرد که `HandleDNSQuery` را
+تعریف می‌کند، و **شناسه**‌ی هر سرور DNS نامی است که اسکریپت با آن سرور را انتخاب می‌کند.
+
+<Callout type="warn">
+  اسکریپت مسیریابی جای **همه‌ی** قوانین مسیریابی را می‌گیرد، از جمله قانون `api` خود
+  پنل. در اسکریپت، اینباند `api` را به اوت‌باند `api` بفرستید، وگرنه آمار ترافیک و
+  اعمال زنده‌ی تغییرات کلاینت‌ها متوقف می‌شود. اگر فایل وجود نداشته باشد یا بارگذاری
+  نشود، Xray اجرا نمی‌شود.
+</Callout>
+
 ## متعادل‌کننده‌ها
 
 یک **متعادل‌کننده** خروجی‌ها را با یک **انتخاب‌گر** (پیشوندهای برچسب، از جمله مجموعه‌های

+ 2 - 0
docs/content/docs/ru/config/transports.mdx

@@ -139,6 +139,8 @@ XDRIVE передаёт поток файлами в папке, доступн
 Необязательные **адрес/порт фронтинга** (`address`/`port` на уровне stream) — домен-фронт,
 через который идёт подключение к API хранилища; в TLS по-прежнему указывается хост API.
 У XDRIVE нет собственного уровня TLS/REALITY — его трафик это HTTPS самого API хранилища.
+На исходящем безопасность **TLS** задаёт это HTTPS-подключение: API хранилища видит его
+SNI, отпечаток uTLS и ALPN.
 
 ## FinalMask — обфускация на позднем уровне
 

+ 14 - 0
docs/content/docs/ru/operations/outbounds-routing.mdx

@@ -165,6 +165,20 @@ hostname получает уникальный тег `nord-<hostname>` и не
 }
 ```
 
+### Lua-скрипты
+
+Xray 26.10.10 умеет передавать маршрутизацию и DNS в Lua. **Основное → Скрипт
+маршрутизации** принимает путь к файлу `.lua` с функцией `HandleRoute` (абсолютный
+или относительно папки `bin` Xray); **DNS → Lua-скрипт** — файл с функцией
+`HandleDNSQuery`, а **ID** DNS-сервера — имя, по которому такой скрипт его выбирает.
+
+<Callout type="warn">
+  Скрипт маршрутизации заменяет **все** правила маршрутизации, включая собственное
+  правило `api` панели. Направляйте в скрипте входящий `api` на исходящий `api`,
+  иначе остановятся статистика трафика и применение изменений клиентов на лету. Xray не
+  запустится, если файл отсутствует или не загружается.
+</Callout>
+
 ## Балансировщики
 
 **Балансировщик** группирует исходящие соединения по **селектору** (префиксы

+ 2 - 1
docs/content/docs/zh/config/transports.mdx

@@ -128,7 +128,8 @@ XDRIVE 把数据流作为文件存放在双方都能访问的文件夹中:服
 | 调优参数       | 内核默认值     | `segmentBytes`(512 KiB)、`flushIntervalMs`(20)、`pollIntervalMs`/`maxPollIntervalMs`(50/500)、`eagerWindowMs`(2000)、`holeTimeoutMs`(30000)、`sessionTtlSeconds`(300)、`concurrency`(8)。 |
 
 可选的**前置地址/端口**(stream 级的 `address`/`port`)是连接存储 API 时使用的域前置;TLS 中仍使用 API 主机名。
-XDRIVE 没有自己的 TLS/REALITY 层——其流量就是存储 API 自身的 HTTPS。
+XDRIVE 没有自己的 TLS/REALITY 层——其流量就是存储 API 自身的 HTTPS。在出站上,**TLS**
+安全设置决定这条 HTTPS 连接:存储 API 看到的就是其中的 SNI、uTLS 指纹和 ALPN。
 
 ## FinalMask — 末层混淆
 

+ 12 - 0
docs/content/docs/zh/operations/outbounds-routing.mdx

@@ -140,6 +140,18 @@ Xray 配置中——而不会改动你已保存的模板。这是订阅一*池*
 }
 ```
 
+### Lua 脚本
+
+Xray 26.10.10 可以把路由和 DNS 交给 Lua 处理。**基础配置 → 路由脚本** 填写定义了
+`HandleRoute` 的 `.lua` 文件路径(绝对路径或相对于 Xray `bin` 目录);**DNS → Lua
+脚本** 填写定义了 `HandleDNSQuery` 的文件,DNS 服务器的 **ID** 就是脚本选择它时使用的名称。
+
+<Callout type="warn">
+  路由脚本会取代**全部**路由规则,包括面板自带的 `api` 规则。请在脚本中把 `api` 入站
+  发往 `api` 出站,否则流量统计和客户端变更的实时生效都会停止。文件不存在或加载失败时
+  Xray 无法启动。
+</Callout>
+
 ## 均衡器
 
 **均衡器**通过一个**选择器**(标签前缀,包括来自出站订阅的通配符池)将出站分组,

+ 2 - 2
frontend/package.json

@@ -29,8 +29,8 @@
   },
   "lint-staged": {
     "src/**/*.{ts,tsx}": [
-      "oxfmt --no-error-on-unmatched-pattern",
-      "oxlint --fix --no-error-on-unmatched-pattern"
+      "oxfmt",
+      "oxlint --fix"
     ]
   },
   "dependencies": {

+ 228 - 51
frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useRef } from 'react';
+import { useEffect, useRef, useState } from 'react';
 import {
   AutoComplete,
   Button,
@@ -9,6 +9,7 @@ import {
   Select,
   Space,
   Switch,
+  Typography,
 } from 'antd';
 import { DeleteOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
 import { useTranslation } from 'react-i18next';
@@ -98,14 +99,7 @@ function defaultTcpMaskSettings(type: string): Record<string, unknown> {
       // a single length entry reproduces the legacy single-range behavior.
       return { packets: '1-3', lengths: ['100-200'], delays: [], maxSplit: '' };
     case 'sudoku':
-      return {
-        password: '',
-        ascii: '',
-        customTable: '',
-        customTables: [],
-        paddingMin: 0,
-        paddingMax: 0,
-      };
+      return defaultSudokuSettings();
     case 'header-custom':
       return { clients: [], servers: [] };
     case 'xmc':
@@ -119,6 +113,17 @@ function defaultTcpMaskSettings(type: string): Record<string, unknown> {
   }
 }
 
+function defaultSudokuSettings(): Record<string, unknown> {
+  return {
+    password: '',
+    ascii: '',
+    customTable: '',
+    customTables: [],
+    paddingMin: 0,
+    paddingMax: 0,
+  };
+}
+
 function defaultXmcProfile(): Record<string, unknown> {
   return { username: '', uuid: '', texturesValue: '', texturesSignature: '' };
 }
@@ -198,6 +203,8 @@ function defaultUdpMaskSettings(type: string): Record<string, unknown> {
       return { client: [], server: [] };
     case 'noise':
       return { reset: 0, noise: [] };
+    case 'sudoku':
+      return defaultSudokuSettings();
     case 'udphop':
       return { mode: 'intervalRemote', interval: '5-10', remotePorts: '' };
     default:
@@ -471,30 +478,7 @@ function TcpMaskItem({
               </>
             );
           }
-          if (type === 'sudoku') {
-            return (
-              <>
-                <Form.Item label="Password" name={[fieldName, 'settings', 'password']}>
-                  <Input />
-                </Form.Item>
-                <Form.Item label="ASCII" name={[fieldName, 'settings', 'ascii']}>
-                  <Input />
-                </Form.Item>
-                <Form.Item label="Custom Table" name={[fieldName, 'settings', 'customTable']}>
-                  <Input />
-                </Form.Item>
-                <Form.Item label="Custom Tables" name={[fieldName, 'settings', 'customTables']}>
-                  <Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
-                </Form.Item>
-                <Form.Item label="Padding Min" name={[fieldName, 'settings', 'paddingMin']}>
-                  <InputNumber min={0} />
-                </Form.Item>
-                <Form.Item label="Padding Max" name={[fieldName, 'settings', 'paddingMax']}>
-                  <InputNumber min={0} />
-                </Form.Item>
-              </>
-            );
-          }
+          if (type === 'sudoku') return <SudokuSettings fieldName={fieldName} />;
           if (type === 'header-custom') {
             return (
               <HeaderCustomGroups
@@ -545,6 +529,31 @@ function TcpMaskItem({
   );
 }
 
+function SudokuSettings({ fieldName }: { fieldName: number }) {
+  return (
+    <>
+      <Form.Item label="Password" name={[fieldName, 'settings', 'password']}>
+        <Input />
+      </Form.Item>
+      <Form.Item label="ASCII" name={[fieldName, 'settings', 'ascii']}>
+        <Input />
+      </Form.Item>
+      <Form.Item label="Custom Table" name={[fieldName, 'settings', 'customTable']}>
+        <Input />
+      </Form.Item>
+      <Form.Item label="Custom Tables" name={[fieldName, 'settings', 'customTables']}>
+        <Select mode="tags" style={{ width: '100%' }} tokenSeparators={[',']} />
+      </Form.Item>
+      <Form.Item label="Padding Min" name={[fieldName, 'settings', 'paddingMin']}>
+        <InputNumber min={0} />
+      </Form.Item>
+      <Form.Item label="Padding Max" name={[fieldName, 'settings', 'paddingMax']}>
+        <InputNumber min={0} />
+      </Form.Item>
+    </>
+  );
+}
+
 // xray's fragment `packets` accepts "tlshello" or an arbitrary packet-number
 // range like "1-3" (#5075 — presets only covered the common cases).
 function validateFragmentPackets(_rule: unknown, value: unknown): Promise<void> {
@@ -764,6 +773,16 @@ function XmcProfilesList({ tcpFieldName }: { tcpFieldName: number }) {
   );
 }
 
+const HEADER_CUSTOM_TCP_GROUPS = [
+  { key: 'clients', label: 'Clients', tooltip: undefined },
+  { key: 'servers', label: 'Servers', tooltip: undefined },
+  {
+    key: 'errors',
+    label: 'Errors',
+    tooltip: 'Sent instead when the matching client group fails to match, e.g. an HTTP 400',
+  },
+] as const;
+
 function HeaderCustomGroups({
   tcpFieldName,
   form,
@@ -776,11 +795,11 @@ function HeaderCustomGroups({
   const { t } = useTranslation();
   return (
     <>
-      {(['clients', 'servers'] as const).map((groupKey) => (
+      {HEADER_CUSTOM_TCP_GROUPS.map(({ key: groupKey, label, tooltip }) => (
         <Form.List key={groupKey} name={[tcpFieldName, 'settings', groupKey]}>
           {(groups, { add: addGroup, remove: removeGroup }) => (
             <>
-              <Form.Item label={groupKey === 'clients' ? 'Clients' : 'Servers'}>
+              <Form.Item label={label} tooltip={tooltip}>
                 <Button
                   type="primary"
                   size="small"
@@ -792,7 +811,7 @@ function HeaderCustomGroups({
               {groups.map((group, gi) => (
                 <div key={group.key}>
                   <Divider style={{ margin: 0 }}>
-                    {groupKey === 'clients' ? 'Clients' : 'Servers'} Group {gi + 1}
+                    {label} Group {gi + 1}
                     <DeleteOutlined
                       className="danger-icon"
                       role="button"
@@ -825,6 +844,7 @@ function HeaderCustomGroups({
                               item.name,
                             ]}
                             delayMode="number"
+                            allowVariables
                             onRemove={() => removeItem(item.name)}
                           />
                         ))}
@@ -938,6 +958,7 @@ function UdpMaskItem({
         { value: 'realm', label: 'Realm' },
         { value: 'header-custom', label: 'Header Custom' },
         { value: 'noise', label: 'Noise' },
+        { value: 'sudoku', label: 'Sudoku' },
         ...udpHopOption,
       ];
 
@@ -1109,6 +1130,7 @@ function UdpMaskItem({
               />
             );
           }
+          if (type === 'sudoku') return <SudokuSettings fieldName={fieldName} />;
           if (type === 'noise') {
             return (
               <NoiseItems
@@ -1253,6 +1275,20 @@ function UdpHeaderCustom({
   const { t } = useTranslation();
   return (
     <>
+      <Form.Item
+        label="Mode"
+        name={[udpFieldName, 'settings', 'mode']}
+        tooltip="Prefix puts the header on every packet; Standalone exchanges it as separate handshake packets first"
+      >
+        <Select
+          allowClear
+          placeholder="Prefix"
+          options={[
+            { value: 'prefix', label: 'Prefix' },
+            { value: 'standalone', label: 'Standalone' },
+          ]}
+        />
+      </Form.Item>
       {(['client', 'server'] as const).map((groupKey) => (
         <Form.List key={groupKey} name={[udpFieldName, 'settings', groupKey]}>
           {(items, { add, remove }) => (
@@ -1283,6 +1319,7 @@ function UdpHeaderCustom({
                     fieldName={item.name}
                     form={form}
                     absoluteItemPath={[...absoluteSettingsPath, groupKey, item.name]}
+                    allowVariables
                     onRemove={() => remove(item.name)}
                   />
                 </div>
@@ -1463,6 +1500,19 @@ function UdpHopSettings({ udpFieldName }: { udpFieldName: number }) {
   );
 }
 
+// xray-core reads reset as an Int32Range: a plain number or a "min-max" string.
+function validateNoiseReset(_: unknown, value: unknown) {
+  if (value == null || value === '') return Promise.resolve();
+  const bounds = String(value)
+    .split('-')
+    .map((v) => Number(v.trim()));
+  const valid =
+    bounds.length <= 2 &&
+    bounds.every((v) => Number.isInteger(v) && v >= 0) &&
+    bounds[0] <= bounds[bounds.length - 1];
+  return valid ? Promise.resolve() : Promise.reject(new Error('seconds, e.g. 0 or 60-120'));
+}
+
 function NoiseItems({
   udpFieldName,
   form,
@@ -1475,8 +1525,13 @@ function NoiseItems({
   const { t } = useTranslation();
   return (
     <>
-      <Form.Item label="Reset" name={[udpFieldName, 'settings', 'reset']}>
-        <InputNumber min={0} />
+      <Form.Item
+        label="Reset"
+        name={[udpFieldName, 'settings', 'reset']}
+        tooltip="Seconds before the noise is sent to a destination again; 0 sends it once"
+        rules={[{ validator: validateNoiseReset }]}
+      >
+        <Input placeholder="0 or 60-120" />
       </Form.Item>
       <Form.List name={[udpFieldName, 'settings', 'noise']}>
         {(items, { add, remove }) => (
@@ -1519,20 +1574,98 @@ function NoiseItems({
   );
 }
 
+// xray-core 26.6 header-custom: an item is exactly one of raw bytes, a reused variable
+// or a transform ("exactly one item kind must be set"); capture works with any of them.
+type ItemKind = 'bytes' | 'reuse' | 'transform';
+
+const ITEM_KIND_OPTIONS: { value: ItemKind; label: string }[] = [
+  { value: 'bytes', label: 'Bytes' },
+  { value: 'reuse', label: 'Reuse variable' },
+  { value: 'transform', label: 'Transform' },
+];
+
+function itemKindOf(item: unknown): ItemKind {
+  const fields = (item ?? {}) as Record<string, unknown>;
+  if (typeof fields.reuse === 'string') return 'reuse';
+  if (fields.transform != null) return 'transform';
+  return 'bytes';
+}
+
+function validateCustomVarName(_: unknown, value: unknown) {
+  if (value == null || value === '') return Promise.resolve();
+  return /^[A-Za-z_][A-Za-z0-9_]*$/.test(String(value))
+    ? Promise.resolve()
+    : Promise.reject(new Error('letters, digits and _, not starting with a digit'));
+}
+
+// Hands the form only a parsed JSON object, so half-typed text never reaches the config.
+function TransformEditor({
+  value,
+  onChange,
+}: {
+  value?: Record<string, unknown>;
+  onChange?: (next: Record<string, unknown> | undefined) => void;
+}) {
+  const [text, setText] = useState(() => (value ? JSON.stringify(value) : ''));
+  const [invalid, setInvalid] = useState(false);
+  return (
+    <>
+      <Input.TextArea
+        value={text}
+        autoSize={{ minRows: 2, maxRows: 8 }}
+        placeholder='{"op": "concat", "args": [{"reuse": "nonce"}]}'
+        onChange={(e) => {
+          const next = e.target.value;
+          setText(next);
+          if (next.trim() === '') {
+            setInvalid(false);
+            onChange?.(undefined);
+            return;
+          }
+          try {
+            const parsed: unknown = JSON.parse(next);
+            const isObject = !!parsed && typeof parsed === 'object' && !Array.isArray(parsed);
+            setInvalid(!isObject);
+            if (isObject) onChange?.(parsed as Record<string, unknown>);
+          } catch {
+            setInvalid(true);
+          }
+        }}
+      />
+      {invalid && <Typography.Text type="danger">Must be a JSON object</Typography.Text>}
+    </>
+  );
+}
+
 function ItemEditor({
   fieldName,
   form,
   absoluteItemPath,
   delayMode,
+  allowVariables = false,
   onRemove: _onRemove,
 }: {
   fieldName: number;
   form: FormInstance;
   absoluteItemPath: (string | number)[];
   delayMode?: 'number' | 'string';
+  allowVariables?: boolean;
   onRemove?: () => void;
 }) {
   const { t } = useTranslation();
+  const [kind, setKind] = useState<ItemKind>(() =>
+    allowVariables ? itemKindOf(form.getFieldValue(absoluteItemPath)) : 'bytes',
+  );
+  const onKindChange = (next: ItemKind) => {
+    setKind(next);
+    for (const key of ['type', 'packet', 'rand', 'randRange', 'reuse', 'transform']) {
+      form.setFieldValue([...absoluteItemPath, key], undefined);
+    }
+    if (next === 'bytes') {
+      form.setFieldValue([...absoluteItemPath, 'type'], 'array');
+      form.setFieldValue([...absoluteItemPath, 'rand'], 0);
+    }
+  };
   /**
    * Switching to `array` clears the packet instead of emptying it to `[]`:
    * that branch is rand-driven, and xray-core counts even an empty array as a
@@ -1552,19 +1685,26 @@ function ItemEditor({
 
   return (
     <>
-      <Form.Item label="Type" name={[fieldName, 'type']}>
-        <Select
-          onChange={onTypeChange}
-          options={[
-            { value: 'array', label: 'Array' },
-            { value: 'str', label: 'String' },
-            { value: 'hex', label: 'Hex' },
-            { value: 'base64', label: 'Base64' },
-            // Only the noise mask parses tag expressions (xray-core 26.9.30, #6862).
-            ...(delayMode === 'string' ? [{ value: 'exp', label: 'Expression' }] : []),
-          ]}
-        />
-      </Form.Item>
+      {allowVariables && (
+        <Form.Item label="Kind">
+          <Select value={kind} onChange={onKindChange} options={ITEM_KIND_OPTIONS} />
+        </Form.Item>
+      )}
+      {kind === 'bytes' && (
+        <Form.Item label="Type" name={[fieldName, 'type']}>
+          <Select
+            onChange={onTypeChange}
+            options={[
+              { value: 'array', label: 'Array' },
+              { value: 'str', label: 'String' },
+              { value: 'hex', label: 'Hex' },
+              { value: 'base64', label: 'Base64' },
+              // Only the noise mask parses tag expressions (xray-core 26.9.30, #6862).
+              ...(delayMode === 'string' ? [{ value: 'exp', label: 'Expression' }] : []),
+            ]}
+          />
+        </Form.Item>
+      )}
 
       {delayMode === 'number' && (
         <Form.Item label="Delay (ms)" name={[fieldName, 'delay']}>
@@ -1577,6 +1717,30 @@ function ItemEditor({
         </Form.Item>
       )}
 
+      {kind === 'reuse' && (
+        <Form.Item
+          label="Reuse"
+          name={[fieldName, 'reuse']}
+          tooltip="Writes the bytes an earlier item captured under this name"
+          rules={[
+            { required: true, message: 'name the captured variable' },
+            { validator: validateCustomVarName },
+          ]}
+        >
+          <Input placeholder="variable name" />
+        </Form.Item>
+      )}
+      {kind === 'transform' && (
+        <Form.Item
+          label="Transform"
+          name={[fieldName, 'transform']}
+          tooltip='An expression tree, e.g. {"op": "concat", "args": [{"type": "str", "bytes": "OK "}, {"reuse": "nonce"}]}'
+          rules={[{ required: true, message: 'enter the transform expression' }]}
+        >
+          <TransformEditor />
+        </Form.Item>
+      )}
+
       <Form.Item
         noStyle
         shouldUpdate={(prev, curr) =>
@@ -1585,6 +1749,7 @@ function ItemEditor({
         }
       >
         {({ getFieldValue }) => {
+          if (kind !== 'bytes') return null;
           const type = getFieldValue([...absoluteItemPath, 'type']) as string | undefined;
           if (type === 'array') {
             return (
@@ -1635,6 +1800,18 @@ function ItemEditor({
           );
         }}
       </Form.Item>
+
+      {allowVariables && (
+        <Form.Item
+          label="Capture"
+          name={[fieldName, 'capture']}
+          tooltip="Saves the bytes this item writes or reads under a name later items can reuse"
+          normalize={(v) => (v === '' ? undefined : v)}
+          rules={[{ validator: validateCustomVarName }]}
+        >
+          <Input placeholder="variable name" />
+        </Form.Item>
+      )}
     </>
   );
 }

+ 9 - 0
frontend/src/lib/xray/protocol-capabilities.ts

@@ -43,6 +43,15 @@ export function canEnableTls(values: CapabilityProtocolSlice): boolean {
   return TLS_NETWORKS.includes(values.streamSettings?.network ?? '');
 }
 
+// An XDRIVE outbound's tlsSettings shape only its HTTPS calls to the storage API (SNI, uTLS
+// fingerprint); xray-core 26.10.10 reads them on the dial side, never on a listener.
+export function canEnableOutboundTls(values: CapabilityProtocolSlice): boolean {
+  if (canEnableTls(values)) return true;
+  return (
+    TLS_ELIGIBLE_PROTOCOLS.includes(values.protocol) && values.streamSettings?.network === 'xdrive'
+  );
+}
+
 export function canEnableReality(values: CapabilityProtocolSlice): boolean {
   if (!REALITY_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
   return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');

+ 34 - 1
frontend/src/pages/inbounds/form/protocols/tun.tsx

@@ -1,6 +1,6 @@
 import type { ReactNode } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Button, Form, Input, InputNumber, Space, Tooltip } from 'antd';
+import { Button, Form, Input, InputNumber, Select, Space, Switch, Tooltip } from 'antd';
 import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
 import { useFieldArray, useFormContext } from 'react-hook-form';
 
@@ -35,6 +35,12 @@ function StringList({ name, label, placeholder }: StringListProps) {
   );
 }
 
+// xray-core 26.9.30 accepts exactly these autoSystemWfpBlockLeak values.
+const WFP_BLOCK_LEAK_OPTIONS = [
+  { value: 'dns', label: 'dns' },
+  { value: 'misconfigtun', label: 'misconfigtun' },
+];
+
 export default function TunFields() {
   const { t } = useTranslation();
   return (
@@ -42,6 +48,13 @@ export default function TunFields() {
       <FormField name={['settings', 'name']} label={t('pages.inbounds.info.interfaceName')}>
         <Input placeholder="xray0" />
       </FormField>
+      <FormField
+        name={['settings', 'desc']}
+        label={t('pages.inbounds.form.tunDesc')}
+        tooltip={t('pages.inbounds.form.tunDescTooltip')}
+      >
+        <Input id="tun-desc" placeholder="Wintun" />
+      </FormField>
       <FormField name={['settings', 'mtu']} label="MTU">
         <InputNumber min={0} />
       </FormField>
@@ -77,6 +90,26 @@ export default function TunFields() {
       >
         <Input placeholder="auto" />
       </FormField>
+      <FormField
+        name={['settings', 'autoSystemDnsToGateway']}
+        label={t('pages.inbounds.form.autoSystemDnsToGateway')}
+        tooltip={t('pages.inbounds.form.autoSystemDnsToGatewayTooltip')}
+        valueProp="checked"
+      >
+        <Switch id="tun-auto-dns-gateway" />
+      </FormField>
+      <FormField
+        name={['settings', 'autoSystemWfpBlockLeak']}
+        label={t('pages.inbounds.form.autoSystemWfpBlockLeak')}
+        tooltip={t('pages.inbounds.form.autoSystemWfpBlockLeakTooltip')}
+      >
+        <Select
+          id="tun-wfp-block-leak"
+          mode="multiple"
+          allowClear
+          options={WFP_BLOCK_LEAK_OPTIONS}
+        />
+      </FormField>
     </>
   );
 }

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

@@ -160,6 +160,7 @@ export default function BasicsTab({
   );
 
   const routingStrategy = templateSettings?.routing?.domainStrategy ?? 'AsIs';
+  const routingScript = templateSettings?.routing?.script ?? '';
   const log = (templateSettings?.log || {}) as Record<string, unknown>;
   const policy = (templateSettings?.policy?.system || {}) as Record<string, boolean>;
   const level0 = (templateSettings?.policy?.levels?.['0'] || {}) as Record<string, unknown>;
@@ -259,6 +260,25 @@ export default function BasicsTab({
               />
             }
           />
+          <SettingListItem
+            title={t('pages.xray.routingScript')}
+            description={t('pages.xray.routingScriptDesc')}
+            paddings="small"
+            control={
+              <Input
+                value={routingScript}
+                placeholder="routing.lua"
+                onChange={(e) => {
+                  const next = e.target.value;
+                  mutate((tt) => {
+                    if (!tt.routing) return;
+                    if (next.trim() === '') delete tt.routing.script;
+                    else tt.routing.script = next;
+                  });
+                }}
+              />
+            }
+          />
           <SettingListItem
             title={t('pages.xray.outboundTestUrl')}
             description={t('pages.xray.outboundTestUrlDesc')}

+ 7 - 0
frontend/src/pages/xray/dns/DnsServerModal.tsx

@@ -188,6 +188,13 @@ export default function DnsServerModal({
               <InputNumber min={1} max={65535} />
             </FormField>
           )}
+          <FormField
+            label={t('pages.xray.dns.serverId')}
+            tooltip={t('pages.xray.dns.serverIdDesc')}
+            name="id"
+          >
+            <Input />
+          </FormField>
           <FormField label={t('pages.xray.dns.tag')} name="tag">
             <Input />
           </FormField>

+ 12 - 0
frontend/src/pages/xray/dns/DnsTab.tsx

@@ -297,6 +297,18 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab
                     />
                   }
                 />
+                <SettingListItem
+                  paddings="small"
+                  title={t('pages.xray.dns.script')}
+                  description={t('pages.xray.dns.scriptDesc')}
+                  control={
+                    <Input
+                      value={dns?.script ?? ''}
+                      placeholder="dns.lua"
+                      onChange={(e) => setDnsField('script', e.target.value, true)}
+                    />
+                  }
+                />
                 {(
                   [
                     [

+ 2 - 2
frontend/src/pages/xray/outbounds/OutboundFormModal.tsx

@@ -14,7 +14,7 @@ import { OutboundFormBaseSchema, type OutboundFormValues } from '@/schemas/forms
 import {
   canEnableReality,
   canEnableStream,
-  canEnableTls,
+  canEnableOutboundTls,
   canEnableTlsFlow,
 } from '@/lib/xray/protocol-capabilities';
 
@@ -111,7 +111,7 @@ export default function OutboundFormModal({
     | undefined;
 
   const streamAllowed = canEnableStream({ protocol });
-  const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
+  const tlsAllowed = canEnableOutboundTls({ protocol, streamSettings: { network, security } });
   const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
   const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
 

+ 2 - 2
frontend/src/pages/xray/outbounds/outbound-form-helpers.ts

@@ -1,5 +1,5 @@
 import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
-import { canEnableReality, canEnableTls } from '@/lib/xray/protocol-capabilities';
+import { canEnableOutboundTls, canEnableReality } from '@/lib/xray/protocol-capabilities';
 import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
 import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
 
@@ -112,7 +112,7 @@ export function applyNetworkChange(
   if (next === 'masque') return masqueStreamSlice();
   const stream = prevStream ?? {};
   const currentSecurity = (stream.security as string) ?? 'none';
-  const stillTls = canEnableTls({
+  const stillTls = canEnableOutboundTls({
     protocol,
     streamSettings: { network: next, security: currentSecurity },
   });

+ 10 - 1
frontend/src/pages/xray/routing/RoutingTab.tsx

@@ -1,6 +1,6 @@
 import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
 import { useTranslation } from 'react-i18next';
-import { Button, Dropdown, Modal, Space, Table, Tabs, message } from 'antd';
+import { Alert, Button, Dropdown, Modal, Space, Table, Tabs, message } from 'antd';
 import {
   AimOutlined,
   ControlOutlined,
@@ -64,6 +64,7 @@ export default function RoutingTab({
     moved: false,
   });
 
+  const routingScript = templateSettings?.routing?.script ?? '';
   const rules = useMemo(
     () => (templateSettings?.routing?.rules || []) as RoutingRule[],
     [templateSettings?.routing?.rules],
@@ -349,6 +350,14 @@ export default function RoutingTab({
     <>
       {modalContextHolder}
       {messageContextHolder}
+      {routingScript.trim() !== '' && (
+        <Alert
+          type="warning"
+          showIcon
+          style={{ marginBottom: 12 }}
+          title={t('pages.xray.routingScriptActive', { section: t('pages.xray.basicTemplate') })}
+        />
+      )}
       <Tabs
         defaultActiveKey="basic"
         items={[

+ 2 - 0
frontend/src/schemas/protocols/inbound/tun.ts

@@ -2,6 +2,8 @@ import { z } from 'zod';
 
 export const TunInboundSettingsSchema = z.object({
   name: z.string().default('xray0'),
+  // Windows: the tunnel type a new Wintun adapter is created with (core default Wintun).
+  desc: z.string().optional(),
   mtu: z.number().int().min(0).default(1500),
   gateway: z.array(z.string()).default([]),
   dns: z.array(z.string()).default([]),

+ 1 - 1
frontend/src/schemas/protocols/stream/finalmask.ts

@@ -20,7 +20,7 @@ export const TcpMaskSchema = z.object({
 export type TcpMask = z.infer<typeof TcpMaskSchema>;
 
 // 'udphop' is client-only in xray-core (it refuses to wrap a server socket),
-// so it round-trips here but is deliberately absent from the mask dropdown.
+// so only the client-side mask editors offer it.
 export const UdpMaskTypeSchema = z.enum([
   'salamander',
   'mkcp-legacy',

+ 1 - 0
frontend/src/schemas/xray.ts

@@ -27,6 +27,7 @@ export const XraySettingsValueSchema = z
         rules: z.array(RuleObjectSchema).optional(),
         balancers: z.array(BalancerObjectSchema).optional(),
         domainStrategy: z.string().optional(),
+        script: z.string().optional(),
       })
       .loose()
       .optional(),

+ 101 - 0
frontend/src/test/__snapshots__/finalmask.test.ts.snap

@@ -38,6 +38,107 @@ exports[`FinalMaskStreamSettingsSchema fixtures > parses combined byte-stably 1`
 }
 `;
 
+exports[`FinalMaskStreamSettingsSchema fixtures > parses header-custom-variables byte-stably 1`] = `
+{
+  "tcp": [
+    {
+      "settings": {
+        "clients": [
+          [
+            {
+              "capture": "nonce",
+              "delay": 0,
+              "rand": 8,
+              "type": "array",
+            },
+            {
+              "delay": 0,
+              "packet": "HELLO",
+              "type": "str",
+            },
+          ],
+        ],
+        "errors": [
+          [
+            {
+              "delay": 0,
+              "packet": "DENIED",
+              "type": "str",
+            },
+          ],
+        ],
+        "servers": [
+          [
+            {
+              "delay": 0,
+              "reuse": "nonce",
+            },
+            {
+              "delay": 0,
+              "transform": {
+                "args": [
+                  {
+                    "bytes": "OK ",
+                    "type": "str",
+                  },
+                  {
+                    "reuse": "nonce",
+                  },
+                ],
+                "op": "concat",
+              },
+            },
+          ],
+        ],
+      },
+      "type": "header-custom",
+    },
+  ],
+  "udp": [
+    {
+      "settings": {
+        "client": [
+          {
+            "capture": "token",
+            "rand": 8,
+            "type": "array",
+          },
+        ],
+        "mode": "standalone",
+        "server": [
+          {
+            "reuse": "token",
+          },
+        ],
+      },
+      "type": "header-custom",
+    },
+    {
+      "settings": {
+        "ascii": "prefer_entropy",
+        "paddingMax": 7,
+        "paddingMin": 2,
+        "password": "s3cr3t",
+      },
+      "type": "sudoku",
+    },
+    {
+      "settings": {
+        "noise": [
+          {
+            "delay": "10-16",
+            "rand": "10-20",
+            "type": "array",
+          },
+        ],
+        "reset": "60-120",
+      },
+      "type": "noise",
+    },
+  ],
+}
+`;
+
 exports[`FinalMaskStreamSettingsSchema fixtures > parses quic-params byte-stably 1`] = `
 {
   "quicParams": {

+ 16 - 1
frontend/src/test/dns-server-modal.test.tsx

@@ -1,5 +1,5 @@
 import { describe, expect, it, vi } from 'vitest';
-import { act, fireEvent } from '@testing-library/react';
+import { act, fireEvent, screen } from '@testing-library/react';
 
 import DnsServerModal from '@/pages/xray/dns/DnsServerModal';
 import { renderWithProviders } from './test-utils';
@@ -27,4 +27,19 @@ describe('DnsServerModal', () => {
     expect(onConfirm).toHaveBeenCalledTimes(1);
     expect(onConfirm.mock.calls[0][0]).toMatchObject({ address: '1.1.1.1', id: 'cf' });
   });
+
+  it('saves an id typed for a plain server', async () => {
+    const onConfirm = vi.fn();
+    renderWithProviders(
+      <DnsServerModal open server="1.1.1.1" isEdit onClose={() => {}} onConfirm={onConfirm} />,
+    );
+
+    fireEvent.change(screen.getByLabelText('ID'), { target: { value: 'cf' } });
+    const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
+    await act(async () => {
+      fireEvent.click(ok);
+    });
+
+    expect(onConfirm.mock.calls[0][0]).toMatchObject({ address: '1.1.1.1', id: 'cf' });
+  });
 });

+ 26 - 2
frontend/src/test/dns-tab.test.tsx

@@ -1,5 +1,5 @@
-import { useState } from 'react';
-import { describe, expect, it } from 'vitest';
+import { useEffect, useState } from 'react';
+import { describe, expect, it, vi } from 'vitest';
 import { fireEvent, screen } from '@testing-library/react';
 
 import DnsTab from '@/pages/xray/dns/DnsTab';
@@ -127,4 +127,28 @@ describe('DnsTab', () => {
 
     expect(screen.queryByLabelText('Domain (e.g. domain:example.com)')).toBeNull();
   });
+
+  // xray-core 26.10.10 dns.script is a file path; an empty one fails the core build.
+  it('writes the Lua script path and drops the key once cleared', () => {
+    const seen = vi.fn();
+    const latest = () => seen.mock.lastCall?.[0] as { dns: Record<string, unknown> };
+    function Harness() {
+      const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>(
+        withHosts({}),
+      );
+      useEffect(() => seen(templateSettings), [templateSettings]);
+      const updateTemplate: SetTemplate = (next) => {
+        setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
+      };
+      return <DnsTab templateSettings={templateSettings} setTemplateSettings={updateTemplate} />;
+    }
+
+    renderWithProviders(<Harness />);
+    const script = screen.getByRole('textbox', { name: 'Lua Script' });
+    fireEvent.change(script, { target: { value: '/etc/xray/dns.lua' } });
+    expect(latest().dns.script).toBe('/etc/xray/dns.lua');
+
+    fireEvent.change(script, { target: { value: '' } });
+    expect(latest().dns).not.toHaveProperty('script');
+  });
 });

+ 54 - 0
frontend/src/test/golden/fixtures/finalmask/header-custom-variables.json

@@ -0,0 +1,54 @@
+{
+  "tcp": [
+    {
+      "type": "header-custom",
+      "settings": {
+        "clients": [
+          [
+            { "delay": 0, "type": "array", "rand": 8, "capture": "nonce" },
+            { "delay": 0, "type": "str", "packet": "HELLO" }
+          ]
+        ],
+        "servers": [
+          [
+            { "delay": 0, "reuse": "nonce" },
+            {
+              "delay": 0,
+              "transform": {
+                "op": "concat",
+                "args": [{ "type": "str", "bytes": "OK " }, { "reuse": "nonce" }]
+              }
+            }
+          ]
+        ],
+        "errors": [[{ "delay": 0, "type": "str", "packet": "DENIED" }]]
+      }
+    }
+  ],
+  "udp": [
+    {
+      "type": "header-custom",
+      "settings": {
+        "mode": "standalone",
+        "client": [{ "type": "array", "rand": 8, "capture": "token" }],
+        "server": [{ "reuse": "token" }]
+      }
+    },
+    {
+      "type": "sudoku",
+      "settings": {
+        "password": "s3cr3t",
+        "ascii": "prefer_entropy",
+        "paddingMin": 2,
+        "paddingMax": 7
+      }
+    },
+    {
+      "type": "noise",
+      "settings": {
+        "reset": "60-120",
+        "noise": [{ "type": "array", "rand": "10-20", "delay": "10-16" }]
+      }
+    }
+  ]
+}

+ 43 - 0
frontend/src/test/inbound-form-modal.test.tsx

@@ -467,6 +467,49 @@ describe('InboundFormModal', () => {
     expect(JSON.stringify(finalmask.udp ?? [])).not.toContain('udphop');
   });
 
+  // xray-core 26.9: the Wintun adapter type (desc) and the TUN auto-system leak guards.
+  it('saves the TUN adapter type and auto-system options set in the form', async () => {
+    const post = vi.mocked(HttpUtil.post);
+    post.mockClear();
+    const tun = new DBInbound({
+      id: 44,
+      port: 0,
+      listen: '',
+      protocol: 'tun',
+      remark: 'tun',
+      enable: true,
+      settings: {
+        name: 'xray0',
+        mtu: 1500,
+        gateway: ['10.0.0.1/16'],
+        dns: ['1.1.1.1'],
+        userLevel: 0,
+        autoSystemRoutingTable: ['0.0.0.0/0'],
+        autoOutboundsInterface: 'auto',
+      },
+      streamSettings: {},
+      sniffing: { enabled: false },
+      nodeId: null,
+      shareAddrStrategy: 'listen',
+      shareAddr: '',
+    });
+    renderCloneLikeEdit(tun);
+
+    fireEvent.change(document.getElementById('tun-desc')!, { target: { value: 'MyTun' } });
+    fireEvent.click(document.getElementById('tun-auto-dns-gateway')!);
+    chooseSelectOption('tun-wfp-block-leak', 'dns');
+    fireEvent.click(primaryButton());
+
+    const isUpdate = ([url]: unknown[]) => url === '/panel/api/inbounds/update/44';
+    await waitFor(() => expect(post.mock.calls.some(isUpdate)).toBe(true));
+    const payload = post.mock.calls.find(isUpdate)![1] as { settings: string };
+    expect(JSON.parse(payload.settings)).toMatchObject({
+      desc: 'MyTun',
+      autoSystemDnsToGateway: true,
+      autoSystemWfpBlockLeak: ['dns'],
+    });
+  });
+
   // Clients and enable change through their own endpoints; the server keeps the
   // stored ones, so the edit form must neither send nor validate its stale copy.
   it('edit save neither sends nor validates the clients it loaded', async () => {

+ 3 - 1
frontend/src/test/inbound-from-db.test.ts

@@ -60,12 +60,13 @@ describe('inboundFromDb', () => {
 
   // The settings parse strips keys the schema does not list, so a TUN option xray-core
   // 26.9.30 added and an admin entered as JSON would vanish on the next form save.
-  it('keeps the TUN options xray-core 26.9.30 added', () => {
+  it('keeps the TUN options xray-core 26.9 added', () => {
     const inbound = inboundFromDb({
       ...BASE_DB_FIELDS,
       protocol: 'tun',
       settings: {
         name: 'xray0',
+        desc: 'MyTun',
         gateway: ['10.0.0.1/16'],
         autoSystemDnsToGateway: true,
         autoSystemWfpBlockLeak: ['dns', 'misconfigtun'],
@@ -73,6 +74,7 @@ describe('inboundFromDb', () => {
       streamSettings: '',
     });
     expect(inbound.settings).toMatchObject({
+      desc: 'MyTun',
       autoSystemDnsToGateway: true,
       autoSystemWfpBlockLeak: ['dns', 'misconfigtun'],
     });

+ 206 - 0
frontend/src/test/outbound-form-modal.test.tsx

@@ -379,6 +379,80 @@ describe('OutboundFormModal', () => {
   });
 });
 
+// xray-core 26.10.10 dials the XDRIVE storage API with the outbound's tlsSettings (SNI,
+// uTLS fingerprint), so switching a TLS outbound to XDRIVE must keep them.
+describe('OutboundFormModal XDRIVE TLS', () => {
+  it('keeps TLS when a TLS outbound moves to XDRIVE', async () => {
+    const onConfirm = vi.fn();
+    const queryClient = makeTestQueryClient();
+    const outbound = {
+      protocol: 'vless',
+      tag: 'drive-out',
+      settings: {
+        vnext: [
+          {
+            address: 'www.google.com',
+            port: 443,
+            users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
+          },
+        ],
+      },
+      streamSettings: {
+        network: 'tcp',
+        security: 'tls',
+        tlsSettings: { serverName: 'www.google.com', fingerprint: 'chrome' },
+      },
+    };
+    const tree = (open: boolean) => (
+      <QueryClientProvider client={queryClient}>
+        <ThemeProvider>
+          <OutboundFormModal
+            open={open}
+            outbound={outbound}
+            existingTags={[]}
+            onClose={() => {}}
+            onConfirm={onConfirm}
+          />
+        </ThemeProvider>
+      </QueryClientProvider>
+    );
+    const { rerender } = render(tree(false));
+    rerender(tree(true));
+    await act(async () => {
+      await new Promise((r) => setTimeout(r, 0));
+    });
+
+    chooseOptionByLabel('Transmission', 'XDRIVE');
+    await act(async () => {
+      await new Promise((r) => setTimeout(r, 0));
+    });
+    expect((screen.getByRole('radio', { name: 'TLS' }) as HTMLInputElement).checked).toBe(true);
+    for (const [label, value] of [
+      ['Remote folder', 'xray-tunnel'],
+      ['Client ID', 'id.apps.googleusercontent.com'],
+      ['Client secret', 'client-secret'],
+      ['Refresh token', 'refresh-token'],
+    ]) {
+      fireEvent.change(screen.getByLabelText(label), { target: { value } });
+    }
+    const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
+    await act(async () => {
+      fireEvent.click(ok);
+    });
+    await act(async () => {
+      await new Promise((r) => setTimeout(r, 0));
+    });
+
+    expect(onConfirm).toHaveBeenCalledTimes(1);
+    const payload = onConfirm.mock.calls[0][0] as { streamSettings: Record<string, unknown> };
+    expect(payload.streamSettings).toMatchObject({
+      network: 'xdrive',
+      security: 'tls',
+      tlsSettings: { serverName: 'www.google.com', fingerprint: 'chrome' },
+    });
+  });
+});
+
 describe('OutboundFormModal MASQUE', () => {
   // xray-core refuses user/pass next to warp, and builds the WARP client certificate
   // from privateKey; a save that kept stale credentials or dropped a key never connects.
@@ -516,3 +590,135 @@ describe('OutboundFormModal UDP hop', () => {
     expect(payload.streamSettings.finalmask.quicParams?.congestion).toBe('bbr');
   });
 });
+
+describe('OutboundFormModal finalmask variables', () => {
+  function xhttpOutbound(finalmask: Record<string, unknown>) {
+    return {
+      protocol: 'vless',
+      tag: 'masked',
+      settings: {
+        vnext: [
+          {
+            address: 'example.com',
+            port: 443,
+            users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
+          },
+        ],
+      },
+      streamSettings: {
+        network: 'xhttp',
+        security: 'none',
+        xhttpSettings: { path: '/', mode: 'auto' },
+        finalmask,
+      },
+    };
+  }
+
+  async function saveFinalmask(
+    finalmask: Record<string, unknown>,
+    edit: () => void = () => {},
+  ): Promise<Record<string, unknown>> {
+    const onConfirm = vi.fn();
+    const queryClient = makeTestQueryClient();
+    const tree = (open: boolean) => (
+      <QueryClientProvider client={queryClient}>
+        <ThemeProvider>
+          <OutboundFormModal
+            open={open}
+            outbound={xhttpOutbound(finalmask)}
+            existingTags={[]}
+            onClose={() => {}}
+            onConfirm={onConfirm}
+          />
+        </ThemeProvider>
+      </QueryClientProvider>
+    );
+    const { rerender } = render(tree(false));
+    rerender(tree(true));
+    await act(async () => {
+      await new Promise((r) => setTimeout(r, 0));
+    });
+    edit();
+    await act(async () => {
+      await new Promise((r) => setTimeout(r, 0));
+    });
+
+    const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
+    await act(async () => {
+      fireEvent.click(ok);
+    });
+    await act(async () => {
+      await new Promise((r) => setTimeout(r, 0));
+    });
+    expect(onConfirm).toHaveBeenCalledTimes(1);
+    return (
+      onConfirm.mock.calls[0][0] as { streamSettings: { finalmask: Record<string, unknown> } }
+    ).streamSettings.finalmask;
+  }
+
+  function typeInto(label: string, value: string) {
+    const input = formItemByLabel(label)?.querySelector('input, textarea');
+    if (!input) throw new Error(`${label} input not found`);
+    fireEvent.change(input, { target: { value } });
+  }
+
+  // xray-core 26.6 header-custom variables, error replies and standalone UDP headers, plus
+  // the sudoku UDP mask; the same fixture is built by Go's TestGoldenStreamFixturesBuildInXray.
+  it('keeps every header-custom variable, sudoku UDP mask and noise reset range on save', async () => {
+    const fixture = (await import('./golden/fixtures/finalmask/header-custom-variables.json'))
+      .default as Record<string, unknown>;
+
+    expect(await saveFinalmask(structuredClone(fixture))).toEqual(fixture);
+  });
+
+  // The core rejects an item carrying two kinds ("exactly one item kind must be set").
+  it('drops the bytes of an item switched to reuse a variable', async () => {
+    const finalmask = await saveFinalmask(
+      {
+        udp: [
+          {
+            type: 'header-custom',
+            settings: { client: [{ type: 'array', rand: 4, randRange: '0-255' }], server: [] },
+          },
+        ],
+      },
+      () => {
+        chooseOptionByLabel('Kind', 'Reuse variable');
+        typeInto('Reuse', 'nonce');
+      },
+    );
+
+    expect(finalmask.udp).toEqual([
+      { type: 'header-custom', settings: { client: [{ reuse: 'nonce' }], server: [] } },
+    ]);
+  });
+
+  // A string transform fails the whole core config, so half-typed JSON must never be saved.
+  it.each([
+    ['{"op": "be16", "args": [{"u64": 7}]}', [{ transform: { op: 'be16', args: [{ u64: 7 }] } }]],
+    ['{"op": "be16"', [{}]],
+  ])('saves the typed transform %s only once it parses', async (typed, client) => {
+    const finalmask = await saveFinalmask(
+      {
+        udp: [
+          { type: 'header-custom', settings: { client: [{ type: 'array', rand: 4 }], server: [] } },
+        ],
+      },
+      () => {
+        chooseOptionByLabel('Kind', 'Transform');
+        typeInto('Transform', typed);
+      },
+    );
+
+    expect(finalmask.udp).toEqual([{ type: 'header-custom', settings: { client, server: [] } }]);
+  });
+
+  it('saves a noise reset typed as a range', async () => {
+    const finalmask = await saveFinalmask(
+      { udp: [{ type: 'noise', settings: { reset: 0, noise: [] } }] },
+      () => typeInto('Reset', '60-120'),
+    );
+
+    expect(finalmask.udp).toEqual([{ type: 'noise', settings: { reset: '60-120', noise: [] } }]);
+  });
+});

+ 75 - 0
frontend/src/test/routing-script.test.tsx

@@ -0,0 +1,75 @@
+import { useEffect, useState } from 'react';
+import { describe, expect, it, vi } from 'vitest';
+import { fireEvent, screen } from '@testing-library/react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+import BasicsTab from '@/pages/xray/basics/BasicsTab';
+import RoutingTab from '@/pages/xray/routing/RoutingTab';
+import type { SetTemplate, XraySettingsValue } from '@/hooks/useXraySetting';
+import { renderWithProviders } from './test-utils';
+
+const SCRIPT_BANNER = 'A routing script is set under Basics, so Xray ignores these rules.';
+
+function renderRoutingRules(routing: Record<string, unknown>) {
+  const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+  renderWithProviders(
+    <QueryClientProvider client={queryClient}>
+      <RoutingTab
+        templateSettings={{ routing } as unknown as XraySettingsValue}
+        setTemplateSettings={vi.fn()}
+        inboundTags={[]}
+        clientReverseTags={[]}
+        isMobile={false}
+      />
+    </QueryClientProvider>,
+  );
+  fireEvent.click(screen.getByRole('tab', { name: /Routing Rules/ }));
+}
+
+// xray-core 26.10.10 routes only through routing.script once it is set, skipping
+// every rule, the panel's api rule included.
+describe('routing script', () => {
+  it('writes the script path from Basics and drops the key once cleared', () => {
+    const seen = vi.fn();
+    const latest = () => seen.mock.lastCall?.[0] as { routing: Record<string, unknown> };
+    function Harness() {
+      const [templateSettings, setTemplateSettings] = useState<XraySettingsValue | null>({
+        routing: { domainStrategy: 'AsIs', rules: [] },
+        outbounds: [],
+      } as unknown as XraySettingsValue);
+      useEffect(() => seen(templateSettings), [templateSettings]);
+      const updateTemplate: SetTemplate = (next) => {
+        setTemplateSettings((current) => (typeof next === 'function' ? next(current) : next));
+      };
+      return (
+        <BasicsTab
+          templateSettings={templateSettings}
+          setTemplateSettings={updateTemplate}
+          outboundTestUrl=""
+          onChangeOutboundTestUrl={vi.fn()}
+          onResetDefault={vi.fn()}
+        />
+      );
+    }
+
+    renderWithProviders(<Harness />);
+    const script = screen.getByRole('textbox', { name: 'Routing Script' });
+    fireEvent.change(script, { target: { value: 'route.lua' } });
+    expect(latest().routing.script).toBe('route.lua');
+
+    fireEvent.change(script, { target: { value: '  ' } });
+    expect(latest().routing).not.toHaveProperty('script');
+  });
+
+  it('warns above the rules while a script is set', () => {
+    renderRoutingRules({ rules: [], script: 'route.lua' });
+
+    expect(screen.getByText(SCRIPT_BANNER)).toBeTruthy();
+  });
+
+  it('shows no warning without a script', () => {
+    renderRoutingRules({ rules: [] });
+
+    expect(screen.queryByText(SCRIPT_BANNER)).toBeNull();
+  });
+});

+ 1 - 0
internal/sub/service.go

@@ -2700,6 +2700,7 @@ var validFinalMaskUDPTypes = map[string]struct{}{
 	"header-custom": {},
 	"realm":         {},
 	"udphop":        {},
+	"sudoku":        {},
 }
 
 var validFinalMaskTCPTypes = map[string]struct{}{

+ 14 - 0
internal/sub/service_test.go

@@ -1181,6 +1181,20 @@ func TestMarshalFinalMask_KeepsUdpHopMask(t *testing.T) {
 	}
 }
 
+// xray-core 26.6 accepts sudoku as a UDP mask too; the link must not drop it from fm=.
+func TestMarshalFinalMask_KeepsSudokuUdpMask(t *testing.T) {
+	fm := map[string]any{
+		"udp": []any{map[string]any{"type": "sudoku", "settings": map[string]any{"password": "s3cr3t"}}},
+	}
+	out, ok := marshalFinalMask(fm)
+	if !ok {
+		t.Fatal("expected ok=true for a sudoku udp mask")
+	}
+	if want := `{"udp":[{"settings":{"password":"s3cr3t"},"type":"sudoku"}]}`; out != want {
+		t.Fatalf("marshalFinalMask = %s, want %s", out, want)
+	}
+}
+
 func TestHasFinalMaskContent(t *testing.T) {
 	if hasFinalMaskContent(nil) {
 		t.Fatal("nil should not count as content")

+ 16 - 3
internal/web/translation/ar-EG.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "مجمّع العناوين",
           "addressPoolDesc": "تُخصَّص عناوين النفق من هذه البادئات — بحد أقصى IPv4 واحد و IPv6 واحد."
-        }
+        },
+        "tunDesc": "نوع المحوّل",
+        "tunDescTooltip": "على ويندوز بس: نوع النفق اللي بيتعمل بيه محوّل Wintun جديد. لو فاضي يبقى Wintun.",
+        "autoSystemDnsToGateway": "DNS النظام إلى البوابة",
+        "autoSystemDnsToGatewayTooltip": "على لينكس بس: بيوجّه systemd-resolved لبوابة TUN عشان استعلامات النظام تعدّي من Xray. محتاج بوابة، والـTUN مش هيشتغل لو ده فشل.",
+        "autoSystemWfpBlockLeak": "منع التسريب (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "على ويندوز بس، مع Auto system routes: ‏dns بيخلّي DNS جوّه الـTUN (محتاج سيرفرات DNS)، وmisconfigtun بيمنع نسخة IP مفيش مسار بيوديها للـTUN."
       },
       "info": {
         "mode": "الوضع",
@@ -2275,7 +2281,11 @@
         "clearAll": "حذف الكل",
         "clearAllTitle": "حذف جميع خوادم DNS؟",
         "clearAllConfirm": "سيؤدي هذا إلى إزالة جميع خوادم DNS من القائمة. لا يمكن التراجع عن هذا الإجراء.",
-        "dnsLeakWarning": "قد يتسرب DNS عبر localhost أو UDP/TCP غير المشفر أو وضع DoH/DoQ المحلي أو استعلامات الرجوع أو EDNS client IP. استخدم DoH عبر التوجيه، وثبت أسماء المحللات في hosts، وعطل الرجوع عندما تكون الخصوصية مهمة."
+        "dnsLeakWarning": "قد يتسرب DNS عبر localhost أو UDP/TCP غير المشفر أو وضع DoH/DoQ المحلي أو استعلامات الرجوع أو EDNS client IP. استخدم DoH عبر التوجيه، وثبت أسماء المحللات في hosts، وعطل الرجوع عندما تكون الخصوصية مهمة.",
+        "script": "سكريبت Lua",
+        "scriptDesc": "مسار ملف ‎.lua‎ بيعرّف HandleDNSQuery، مطلق أو نسبةً لمجلد bin بتاع Xray. بعدها السكريبت هو اللي بيرد على كل الاستعلامات وما بيوصلش للسيرفرات غير من خلال الكود بتاعه. Xray مش هيشتغل لو الملف مش موجود أو مش صالح.",
+        "serverId": "المعرّف",
+        "serverIdDesc": "الاسم اللي سكريبت DNS بلغة Lua بيستخدمه عشان يختار السيرفر ده"
       },
       "fakedns": {
         "add": "أضف Fake DNS",
@@ -2283,7 +2293,10 @@
         "poolSize": "حجم المجموعة"
       },
       "defaultOutbound": "الصادر الافتراضي",
-      "defaultOutboundDesc": "الحركة التي لا تطابق أي قاعدة توجيه تستخدم هذا الصادر (الأول في القائمة)."
+      "defaultOutboundDesc": "الحركة التي لا تطابق أي قاعدة توجيه تستخدم هذا الصادر (الأول في القائمة).",
+      "routingScript": "سكريبت التوجيه",
+      "routingScriptDesc": "مسار ملف ‎.lua‎ بيعرّف HandleRoute، مطلق أو نسبةً لمجلد bin بتاع Xray. بعدها السكريبت هو اللي بيختار كل مسار وXray بيتجاهل كل قواعد التوجيه، ومنها قاعدة api بتاعة البانل: لازم توجّه الإدخال api للإخراج api وإلا إحصائيات الترافيك وتطبيق تغييرات العملاء مباشرةً هيقفوا. Xray مش هيشتغل لو الملف مش موجود أو مش صالح.",
+      "routingScriptActive": "فيه سكريبت توجيه متظبط في {section}، عشان كده Xray بيتجاهل القواعد دي."
     },
     "hosts": {
       "addHost": "إضافة مضيف",

+ 16 - 3
internal/web/translation/en-US.json

@@ -723,7 +723,13 @@
         "masque": {
           "addressPool": "Address pool",
           "addressPoolDesc": "Tunnel addresses are leased from these prefixes — at most one IPv4 and one IPv6."
-        }
+        },
+        "tunDesc": "Adapter type",
+        "tunDescTooltip": "Windows only: the tunnel type a new Wintun adapter is created with. Empty means Wintun.",
+        "autoSystemDnsToGateway": "System DNS to gateway",
+        "autoSystemDnsToGatewayTooltip": "Linux only: points systemd-resolved at the TUN gateway so system lookups go through Xray. Needs a gateway, and the TUN does not start if this fails.",
+        "autoSystemWfpBlockLeak": "Block leaks (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Windows only, with Auto system routes: dns keeps DNS inside the TUN (needs DNS servers), misconfigtun blocks an IP version that no route sends into the TUN."
       },
       "info": {
         "mode": "Mode",
@@ -2393,7 +2399,11 @@
         "clearAll": "Delete All",
         "clearAllTitle": "Delete all DNS servers?",
         "clearAllConfirm": "This removes every DNS server from the list. This cannot be undone.",
-        "dnsLeakWarning": "DNS can leak through localhost, plain UDP/TCP, local-mode DoH/DoQ, fallback queries, or EDNS client IP. Use routed DoH, hosts pins, and disable fallback when privacy matters."
+        "dnsLeakWarning": "DNS can leak through localhost, plain UDP/TCP, local-mode DoH/DoQ, fallback queries, or EDNS client IP. Use routed DoH, hosts pins, and disable fallback when privacy matters.",
+        "script": "Lua Script",
+        "scriptDesc": "Path to a .lua file defining HandleDNSQuery, absolute or relative to the Xray bin folder. The script then answers every query and reaches the servers only through its own code. Xray will not start if the file is missing or invalid.",
+        "serverId": "ID",
+        "serverIdDesc": "The name a Lua DNS script uses to pick this server"
       },
       "fakedns": {
         "add": "Add Fake DNS",
@@ -2401,7 +2411,10 @@
         "poolSize": "Pool Size"
       },
       "defaultOutbound": "Default Outbound",
-      "defaultOutboundDesc": "Traffic that does not match any routing rule uses this outbound (Xray uses the first outbound in the list)."
+      "defaultOutboundDesc": "Traffic that does not match any routing rule uses this outbound (Xray uses the first outbound in the list).",
+      "routingScript": "Routing Script",
+      "routingScriptDesc": "Path to a .lua file defining HandleRoute, absolute or relative to the Xray bin folder. The script then picks every route and Xray ignores all routing rules, the panel's api rule included: send the api inbound to the api outbound or traffic stats and live client changes stop. Xray will not start if the file is missing or invalid.",
+      "routingScriptActive": "A routing script is set under {section}, so Xray ignores these rules."
     },
     "sponsors": {
       "title": "Sponsors",

+ 16 - 3
internal/web/translation/es-ES.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "Pool de direcciones",
           "addressPoolDesc": "Las direcciones del túnel se asignan de estos prefijos: como máximo uno IPv4 y uno IPv6."
-        }
+        },
+        "tunDesc": "Tipo de adaptador",
+        "tunDescTooltip": "Solo Windows: el tipo de túnel con el que se crea un adaptador Wintun nuevo. Vacío equivale a Wintun.",
+        "autoSystemDnsToGateway": "DNS del sistema a la puerta de enlace",
+        "autoSystemDnsToGatewayTooltip": "Solo Linux: apunta systemd-resolved a la puerta de enlace del TUN para que las consultas del sistema pasen por Xray. Necesita una puerta de enlace y el TUN no arranca si esto falla.",
+        "autoSystemWfpBlockLeak": "Bloquear fugas (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Solo Windows, con Auto system routes: dns mantiene el DNS dentro del TUN (necesita servidores DNS) y misconfigtun bloquea una versión de IP que ninguna ruta envía al TUN."
       },
       "info": {
         "mode": "Modo",
@@ -2275,7 +2281,11 @@
         "clearAll": "Eliminar todos",
         "clearAllTitle": "¿Eliminar todos los servidores DNS?",
         "clearAllConfirm": "Esto eliminará todos los servidores DNS de la lista. No se puede deshacer.",
-        "dnsLeakWarning": "El DNS puede filtrarse por localhost, UDP/TCP sin cifrar, DoH/DoQ en modo local, consultas de respaldo o EDNS client IP. Usa DoH enrutado, fija los resolutores en hosts y desactiva el respaldo cuando la privacidad importe."
+        "dnsLeakWarning": "El DNS puede filtrarse por localhost, UDP/TCP sin cifrar, DoH/DoQ en modo local, consultas de respaldo o EDNS client IP. Usa DoH enrutado, fija los resolutores en hosts y desactiva el respaldo cuando la privacidad importe.",
+        "script": "Script Lua",
+        "scriptDesc": "Ruta a un archivo .lua que define HandleDNSQuery, absoluta o relativa a la carpeta bin de Xray. El script responde entonces a cada consulta y solo llega a los servidores a través de su propio código. Xray no arrancará si el archivo falta o no es válido.",
+        "serverId": "ID",
+        "serverIdDesc": "Nombre con el que un script DNS en Lua elige este servidor"
       },
       "fakedns": {
         "add": "Agregar DNS Falso",
@@ -2283,7 +2293,10 @@
         "poolSize": "Tamaño del grupo"
       },
       "defaultOutbound": "Salida predeterminada",
-      "defaultOutboundDesc": "El tráfico que no coincide con ninguna regla de enrutamiento usa esta salida (la primera de la lista)."
+      "defaultOutboundDesc": "El tráfico que no coincide con ninguna regla de enrutamiento usa esta salida (la primera de la lista).",
+      "routingScript": "Script de enrutamiento",
+      "routingScriptDesc": "Ruta a un archivo .lua que define HandleRoute, absoluta o relativa a la carpeta bin de Xray. El script elige entonces cada ruta y Xray ignora todas las reglas de enrutamiento, incluida la regla api del panel: envía el inbound api al outbound api o se detendrán las estadísticas de tráfico y los cambios de clientes en vivo. Xray no arrancará si el archivo falta o no es válido.",
+      "routingScriptActive": "Hay un script de enrutamiento definido en {section}, así que Xray ignora estas reglas."
     },
     "hosts": {
       "addHost": "Agregar host",

+ 16 - 3
internal/web/translation/fa-IR.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "مخزن آدرس",
           "addressPoolDesc": "آدرس‌های تونل از این پیشوندها تخصیص داده می‌شوند — حداکثر یک IPv4 و یک IPv6."
-        }
+        },
+        "tunDesc": "نوع آداپتور",
+        "tunDescTooltip": "فقط ویندوز: نوع تونلی که آداپتور Wintun جدید با آن ساخته می‌شود. خالی یعنی Wintun.",
+        "autoSystemDnsToGateway": "DNS سیستم به گیت‌وی",
+        "autoSystemDnsToGatewayTooltip": "فقط لینوکس: systemd-resolved را به گیت‌وی TUN می‌برد تا کوئری‌های سیستم از Xray عبور کنند. به گیت‌وی نیاز دارد و اگر این کار شکست بخورد TUN اجرا نمی‌شود.",
+        "autoSystemWfpBlockLeak": "جلوگیری از نشت (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "فقط ویندوز، همراه با مسیریابی خودکار سیستم: dns ترافیک DNS را داخل TUN نگه می‌دارد (به سرور DNS نیاز دارد) و misconfigtun نسخه‌ای از IP را که هیچ مسیری به TUN نمی‌فرستد مسدود می‌کند."
       },
       "info": {
         "mode": "حالت",
@@ -2275,7 +2281,11 @@
         "clearAll": "حذف همه",
         "clearAllTitle": "حذف همه سرورهای DNS؟",
         "clearAllConfirm": "این کار همه سرورهای DNS را از لیست حذف می‌کند و قابل بازگشت نیست.",
-        "dnsLeakWarning": "DNS ممکن است از طریق localhost، UDP/TCP ساده، حالت محلی DoH/DoQ، درخواست‌های fallback یا EDNS client IP نشت کند. برای حریم خصوصی از DoH مسیریابی‌شده، مقدارهای hosts ثابت و غیرفعال‌سازی fallback استفاده کنید."
+        "dnsLeakWarning": "DNS ممکن است از طریق localhost، UDP/TCP ساده، حالت محلی DoH/DoQ، درخواست‌های fallback یا EDNS client IP نشت کند. برای حریم خصوصی از DoH مسیریابی‌شده، مقدارهای hosts ثابت و غیرفعال‌سازی fallback استفاده کنید.",
+        "script": "اسکریپت Lua",
+        "scriptDesc": "مسیر یک فایل ‎.lua‎ که HandleDNSQuery را تعریف می‌کند، به‌صورت مطلق یا نسبت به پوشه‌ی bin در Xray. از آن پس اسکریپت به همه‌ی کوئری‌ها پاسخ می‌دهد و سرورها فقط از طریق کد خودش استفاده می‌شوند. اگر فایل وجود نداشته باشد یا نامعتبر باشد، Xray اجرا نمی‌شود.",
+        "serverId": "شناسه",
+        "serverIdDesc": "نامی که اسکریپت Lua‌ی DNS با آن این سرور را انتخاب می‌کند"
       },
       "fakedns": {
         "add": "افزودن دی‌ان‌اس جعلی",
@@ -2283,7 +2293,10 @@
         "poolSize": "اندازه استخر"
       },
       "defaultOutbound": "خروجی پیش‌فرض",
-      "defaultOutboundDesc": "ترافیکی که با هیچ قانون مسیریابی جور نشود از این خروجی استفاده می‌کند (اولین خروجی در فهرست)."
+      "defaultOutboundDesc": "ترافیکی که با هیچ قانون مسیریابی جور نشود از این خروجی استفاده می‌کند (اولین خروجی در فهرست).",
+      "routingScript": "اسکریپت مسیریابی",
+      "routingScriptDesc": "مسیر یک فایل ‎.lua‎ که HandleRoute را تعریف می‌کند، به‌صورت مطلق یا نسبت به پوشه‌ی bin در Xray. از آن پس اسکریپت همه‌ی مسیرها را انتخاب می‌کند و Xray همه‌ی قوانین مسیریابی، از جمله قانون api پنل، را نادیده می‌گیرد: اینباند api را به اوت‌باند api بفرستید، وگرنه آمار ترافیک و اعمال زنده‌ی تغییرات کلاینت‌ها متوقف می‌شود. اگر فایل وجود نداشته باشد یا نامعتبر باشد، Xray اجرا نمی‌شود.",
+      "routingScriptActive": "یک اسکریپت مسیریابی در بخش {section} تنظیم شده است، بنابراین Xray این قوانین را نادیده می‌گیرد."
     },
     "hosts": {
       "addHost": "افزودن میزبان",

+ 16 - 3
internal/web/translation/id-ID.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "Kumpulan alamat",
           "addressPoolDesc": "Alamat terowongan diberikan dari prefiks ini — maksimal satu IPv4 dan satu IPv6."
-        }
+        },
+        "tunDesc": "Jenis adaptor",
+        "tunDescTooltip": "Khusus Windows: jenis tunnel untuk adaptor Wintun baru. Kosong berarti Wintun.",
+        "autoSystemDnsToGateway": "DNS sistem ke gateway",
+        "autoSystemDnsToGatewayTooltip": "Khusus Linux: mengarahkan systemd-resolved ke gateway TUN agar lookup sistem lewat Xray. Butuh gateway, dan TUN tidak berjalan jika ini gagal.",
+        "autoSystemWfpBlockLeak": "Blokir kebocoran (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Khusus Windows, bersama Auto system routes: dns menjaga DNS tetap di dalam TUN (butuh server DNS), misconfigtun memblokir versi IP yang tidak dikirim rute mana pun ke TUN."
       },
       "info": {
         "mode": "Mode",
@@ -2275,7 +2281,11 @@
         "clearAll": "Hapus Semua",
         "clearAllTitle": "Hapus semua server DNS?",
         "clearAllConfirm": "Ini akan menghapus semua server DNS dari daftar. Tidak dapat dibatalkan.",
-        "dnsLeakWarning": "DNS dapat bocor lewat localhost, UDP/TCP polos, DoH/DoQ mode lokal, kueri fallback, atau EDNS client IP. Gunakan DoH yang dirutekan, pin resolver di hosts, dan nonaktifkan fallback saat privasi penting."
+        "dnsLeakWarning": "DNS dapat bocor lewat localhost, UDP/TCP polos, DoH/DoQ mode lokal, kueri fallback, atau EDNS client IP. Gunakan DoH yang dirutekan, pin resolver di hosts, dan nonaktifkan fallback saat privasi penting.",
+        "script": "Skrip Lua",
+        "scriptDesc": "Path ke file .lua yang mendefinisikan HandleDNSQuery, absolut atau relatif terhadap folder bin Xray. Skrip lalu menjawab setiap kueri dan hanya menjangkau server melalui kodenya sendiri. Xray tidak akan berjalan jika file tidak ada atau tidak valid.",
+        "serverId": "ID",
+        "serverIdDesc": "Nama yang dipakai skrip DNS Lua untuk memilih server ini"
       },
       "fakedns": {
         "add": "Tambahkan DNS Palsu",
@@ -2283,7 +2293,10 @@
         "poolSize": "Ukuran Kolam"
       },
       "defaultOutbound": "Outbound default",
-      "defaultOutboundDesc": "Lalu lintas tanpa aturan routing memakai outbound ini (yang pertama dalam daftar)."
+      "defaultOutboundDesc": "Lalu lintas tanpa aturan routing memakai outbound ini (yang pertama dalam daftar).",
+      "routingScript": "Skrip Routing",
+      "routingScriptDesc": "Path ke file .lua yang mendefinisikan HandleRoute, absolut atau relatif terhadap folder bin Xray. Skrip lalu memilih setiap rute dan Xray mengabaikan semua aturan routing, termasuk aturan api panel: arahkan inbound api ke outbound api atau statistik trafik dan perubahan klien langsung akan berhenti. Xray tidak akan berjalan jika file tidak ada atau tidak valid.",
+      "routingScriptActive": "Skrip routing diatur di {section}, jadi Xray mengabaikan aturan ini."
     },
     "hosts": {
       "addHost": "Tambah Host",

+ 16 - 3
internal/web/translation/ja-JP.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "アドレスプール",
           "addressPoolDesc": "トンネルのアドレスはこれらのプレフィックスから割り当てられます(IPv4 と IPv6 をそれぞれ最大 1 つ)。"
-        }
+        },
+        "tunDesc": "アダプターの種類",
+        "tunDescTooltip": "Windows のみ:新しく作成する Wintun アダプターのトンネル種別。空欄の場合は Wintun。",
+        "autoSystemDnsToGateway": "システム DNS をゲートウェイへ",
+        "autoSystemDnsToGatewayTooltip": "Linux のみ:systemd-resolved を TUN のゲートウェイに向け、システムの名前解決を Xray 経由にします。ゲートウェイが必要で、失敗すると TUN は起動しません。",
+        "autoSystemWfpBlockLeak": "リーク遮断 (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Windows のみ(自動システムルートと併用):dns は DNS を TUN 内に留め(DNS サーバーが必要)、misconfigtun は TUN へのルートがない IP バージョンを遮断します。"
       },
       "info": {
         "mode": "モード",
@@ -2275,7 +2281,11 @@
         "clearAll": "すべて削除",
         "clearAllTitle": "すべての DNS サーバを削除しますか?",
         "clearAllConfirm": "リストからすべての DNS サーバが削除されます。この操作は元に戻せません。",
-        "dnsLeakWarning": "DNS は localhost、暗号化なしの UDP/TCP、ローカルモードの DoH/DoQ、フォールバック問い合わせ、EDNS client IP から漏れる可能性があります。プライバシー重視ではルーティングされた DoH、hosts 固定、フォールバック無効化を使ってください。"
+        "dnsLeakWarning": "DNS は localhost、暗号化なしの UDP/TCP、ローカルモードの DoH/DoQ、フォールバック問い合わせ、EDNS client IP から漏れる可能性があります。プライバシー重視ではルーティングされた DoH、hosts 固定、フォールバック無効化を使ってください。",
+        "script": "Lua スクリプト",
+        "scriptDesc": "HandleDNSQuery を定義した .lua ファイルのパス(絶対パス、または Xray の bin フォルダからの相対パス)。設定するとスクリプトがすべてのクエリに応答し、サーバーへはスクリプト自身のコードからのみ問い合わせます。ファイルが存在しないか不正な場合、Xray は起動しません。",
+        "serverId": "ID",
+        "serverIdDesc": "Lua DNS スクリプトがこのサーバーを選ぶときの名前"
       },
       "fakedns": {
         "add": "フェイクDNS追加",
@@ -2283,7 +2293,10 @@
         "poolSize": "プールサイズ"
       },
       "defaultOutbound": "デフォルトアウトバウンド",
-      "defaultOutboundDesc": "ルーティング規則に一致しないトラフィックはこのアウトバウンドを使います(一覧の先頭)。"
+      "defaultOutboundDesc": "ルーティング規則に一致しないトラフィックはこのアウトバウンドを使います(一覧の先頭)。",
+      "routingScript": "ルーティングスクリプト",
+      "routingScriptDesc": "HandleRoute を定義した .lua ファイルのパス(絶対パス、または Xray の bin フォルダからの相対パス)。設定するとスクリプトがすべての経路を選び、Xray はパネルの api ルールを含むすべてのルーティングルールを無視します。api インバウンドを api アウトバウンドへ送らないと、トラフィック統計とクライアント変更の即時反映が止まります。ファイルが存在しないか不正な場合、Xray は起動しません。",
+      "routingScriptActive": "{section} でルーティングスクリプトが設定されているため、Xray はこれらのルールを無視します。"
     },
     "hosts": {
       "addHost": "ホストを追加",

+ 16 - 3
internal/web/translation/pt-BR.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "Pool de endereços",
           "addressPoolDesc": "Os endereços do túnel são atribuídos a partir destes prefixos — no máximo um IPv4 e um IPv6."
-        }
+        },
+        "tunDesc": "Tipo de adaptador",
+        "tunDescTooltip": "Somente Windows: o tipo de túnel com que um novo adaptador Wintun é criado. Vazio significa Wintun.",
+        "autoSystemDnsToGateway": "DNS do sistema para o gateway",
+        "autoSystemDnsToGatewayTooltip": "Somente Linux: aponta o systemd-resolved para o gateway do TUN para que as consultas do sistema passem pelo Xray. Precisa de um gateway, e o TUN não inicia se isso falhar.",
+        "autoSystemWfpBlockLeak": "Bloquear vazamentos (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Somente Windows, com Auto system routes: dns mantém o DNS dentro do TUN (precisa de servidores DNS) e misconfigtun bloqueia uma versão de IP que nenhuma rota envia ao TUN."
       },
       "info": {
         "mode": "Modo",
@@ -2275,7 +2281,11 @@
         "clearAll": "Remover Todos",
         "clearAllTitle": "Remover todos os servidores DNS?",
         "clearAllConfirm": "Isso remove todos os servidores DNS da lista. Não pode ser desfeito.",
-        "dnsLeakWarning": "DNS pode vazar por localhost, UDP/TCP sem criptografia, DoH/DoQ em modo local, consultas de fallback ou EDNS client IP. Use DoH roteado, fixe resolvedores em hosts e desative fallback quando privacidade importar."
+        "dnsLeakWarning": "DNS pode vazar por localhost, UDP/TCP sem criptografia, DoH/DoQ em modo local, consultas de fallback ou EDNS client IP. Use DoH roteado, fixe resolvedores em hosts e desative fallback quando privacidade importar.",
+        "script": "Script Lua",
+        "scriptDesc": "Caminho para um arquivo .lua que define HandleDNSQuery, absoluto ou relativo à pasta bin do Xray. O script passa a responder a todas as consultas e só alcança os servidores pelo próprio código. O Xray não inicia se o arquivo estiver ausente ou for inválido.",
+        "serverId": "ID",
+        "serverIdDesc": "Nome com que um script DNS em Lua escolhe este servidor"
       },
       "fakedns": {
         "add": "Adicionar Fake DNS",
@@ -2283,7 +2293,10 @@
         "poolSize": "Tamanho do Pool"
       },
       "defaultOutbound": "Saída padrão",
-      "defaultOutboundDesc": "Tráfego sem regra de roteamento usa esta saída (a primeira da lista)."
+      "defaultOutboundDesc": "Tráfego sem regra de roteamento usa esta saída (a primeira da lista).",
+      "routingScript": "Script de roteamento",
+      "routingScriptDesc": "Caminho para um arquivo .lua que define HandleRoute, absoluto ou relativo à pasta bin do Xray. O script passa a escolher cada rota e o Xray ignora todas as regras de roteamento, inclusive a regra api do painel: envie o inbound api para o outbound api ou as estatísticas de tráfego e as alterações de clientes ao vivo param. O Xray não inicia se o arquivo estiver ausente ou for inválido.",
+      "routingScriptActive": "Há um script de roteamento definido em {section}, então o Xray ignora estas regras."
     },
     "hosts": {
       "addHost": "Adicionar Host",

+ 16 - 3
internal/web/translation/ru-RU.json

@@ -723,7 +723,13 @@
         "masque": {
           "addressPool": "Пул адресов",
           "addressPoolDesc": "Адреса туннелей выдаются из этих префиксов — не более одного IPv4 и одного IPv6."
-        }
+        },
+        "tunDesc": "Тип адаптера",
+        "tunDescTooltip": "Только Windows: тип туннеля, с которым создаётся новый адаптер Wintun. Пусто — Wintun.",
+        "autoSystemDnsToGateway": "Системный DNS на шлюз",
+        "autoSystemDnsToGatewayTooltip": "Только Linux: направляет systemd-resolved на шлюз TUN, чтобы системные запросы шли через Xray. Нужен шлюз; при ошибке TUN не запустится.",
+        "autoSystemWfpBlockLeak": "Блокировка утечек (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Только Windows, вместе с авто-маршрутами системы: dns удерживает DNS внутри TUN (нужны DNS-серверы), misconfigtun блокирует версию IP, которую ни один маршрут не ведёт в TUN."
       },
       "info": {
         "mode": "Режим",
@@ -2275,7 +2281,11 @@
         "clearAll": "Удалить все",
         "clearAllTitle": "Удалить все DNS-серверы?",
         "clearAllConfirm": "Все DNS-серверы будут удалены из списка. Это действие нельзя отменить.",
-        "dnsLeakWarning": "DNS может утекать через localhost, обычный UDP/TCP, локальный режим DoH/DoQ, fallback-запросы или EDNS client IP. Для приватности используйте маршрутизируемый DoH, закрепляйте резолверы в hosts и отключайте fallback."
+        "dnsLeakWarning": "DNS может утекать через localhost, обычный UDP/TCP, локальный режим DoH/DoQ, fallback-запросы или EDNS client IP. Для приватности используйте маршрутизируемый DoH, закрепляйте резолверы в hosts и отключайте fallback.",
+        "script": "Lua-скрипт",
+        "scriptDesc": "Путь к файлу .lua с функцией HandleDNSQuery — абсолютный или относительно папки bin Xray. Скрипт отвечает на все запросы и обращается к серверам только из своего кода. Xray не запустится, если файл отсутствует или содержит ошибки.",
+        "serverId": "ID",
+        "serverIdDesc": "Имя, по которому DNS-скрипт на Lua выбирает этот сервер"
       },
       "fakedns": {
         "add": "Создать Fake DNS",
@@ -2283,7 +2293,10 @@
         "poolSize": "Размер пула"
       },
       "defaultOutbound": "Исходящий по умолчанию",
-      "defaultOutboundDesc": "Трафик без совпадения с правилами маршрутизации идёт через этот исходящий (первый в списке)."
+      "defaultOutboundDesc": "Трафик без совпадения с правилами маршрутизации идёт через этот исходящий (первый в списке).",
+      "routingScript": "Скрипт маршрутизации",
+      "routingScriptDesc": "Путь к файлу .lua с функцией HandleRoute — абсолютный или относительно папки bin Xray. Скрипт выбирает каждый маршрут, а Xray игнорирует все правила маршрутизации, включая правило api панели: направляйте входящий api на исходящий api, иначе остановятся статистика трафика и применение изменений клиентов на лету. Xray не запустится, если файл отсутствует или содержит ошибки.",
+      "routingScriptActive": "В разделе «{section}» задан скрипт маршрутизации, поэтому Xray игнорирует эти правила."
     },
     "hosts": {
       "addHost": "Добавить хост",

+ 16 - 3
internal/web/translation/tr-TR.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "Adres havuzu",
           "addressPoolDesc": "Tünel adresleri bu öneklerden atanır — en fazla bir IPv4 ve bir IPv6."
-        }
+        },
+        "tunDesc": "Bağdaştırıcı türü",
+        "tunDescTooltip": "Yalnızca Windows: yeni bir Wintun bağdaştırıcısının oluşturulduğu tünel türü. Boş bırakılırsa Wintun.",
+        "autoSystemDnsToGateway": "Sistem DNS'ini ağ geçidine yönlendir",
+        "autoSystemDnsToGatewayTooltip": "Yalnızca Linux: sistem sorguları Xray üzerinden gitsin diye systemd-resolved'ı TUN ağ geçidine yönlendirir. Ağ geçidi gerekir ve bu başarısız olursa TUN başlamaz.",
+        "autoSystemWfpBlockLeak": "Sızıntıları engelle (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Yalnızca Windows, Auto system routes ile: dns, DNS'i TUN içinde tutar (DNS sunucuları gerekir); misconfigtun, hiçbir rotanın TUN'a göndermediği IP sürümünü engeller."
       },
       "info": {
         "mode": "Mod",
@@ -2275,7 +2281,11 @@
         "clearAll": "Tümünü Sil",
         "clearAllTitle": "Tüm DNS sunucularını sil?",
         "clearAllConfirm": "Bu işlem tüm DNS sunucularını listeden kaldırır. Geri alınamaz.",
-        "dnsLeakWarning": "DNS; localhost, düz UDP/TCP, yerel mod DoH/DoQ, fallback sorguları veya EDNS client IP üzerinden sızabilir. Gizlilik önemliyse yönlendirilmiş DoH, hosts sabitlemeleri ve kapalı fallback kullanın."
+        "dnsLeakWarning": "DNS; localhost, düz UDP/TCP, yerel mod DoH/DoQ, fallback sorguları veya EDNS client IP üzerinden sızabilir. Gizlilik önemliyse yönlendirilmiş DoH, hosts sabitlemeleri ve kapalı fallback kullanın.",
+        "script": "Lua Betiği",
+        "scriptDesc": "HandleDNSQuery tanımlayan bir .lua dosyasının yolu; mutlak ya da Xray bin klasörüne göre göreli. Betik bundan sonra her sorguyu yanıtlar ve sunuculara yalnızca kendi koduyla ulaşır. Dosya yoksa veya geçersizse Xray başlamaz.",
+        "serverId": "Kimlik",
+        "serverIdDesc": "Bir Lua DNS betiğinin bu sunucuyu seçerken kullandığı ad"
       },
       "fakedns": {
         "add": "Sahte DNS Ekle",
@@ -2283,7 +2293,10 @@
         "poolSize": "Havuz Boyutu"
       },
       "defaultOutbound": "Varsayılan giden",
-      "defaultOutboundDesc": "Yönlendirme kuralıyla eşleşmeyen trafik bu gideni kullanır (listedeki ilk giden)."
+      "defaultOutboundDesc": "Yönlendirme kuralıyla eşleşmeyen trafik bu gideni kullanır (listedeki ilk giden).",
+      "routingScript": "Yönlendirme Betiği",
+      "routingScriptDesc": "HandleRoute tanımlayan bir .lua dosyasının yolu; mutlak ya da Xray bin klasörüne göre göreli. Betik bundan sonra her rotayı seçer ve Xray, panelin api kuralı dahil tüm yönlendirme kurallarını yok sayar: api gelen bağlantısını api giden bağlantısına yönlendirin, yoksa trafik istatistikleri ve canlı istemci değişiklikleri durur. Dosya yoksa veya geçersizse Xray başlamaz.",
+      "routingScriptActive": "{section} bölümünde bir yönlendirme betiği ayarlı, bu yüzden Xray bu kuralları yok sayar."
     },
     "hosts": {
       "addHost": "Host Ekle",

+ 16 - 3
internal/web/translation/uk-UA.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "Пул адрес",
           "addressPoolDesc": "Адреси тунелів видаються з цих префіксів — не більше одного IPv4 та одного IPv6."
-        }
+        },
+        "tunDesc": "Тип адаптера",
+        "tunDescTooltip": "Лише Windows: тип тунелю, з яким створюється новий адаптер Wintun. Порожньо — Wintun.",
+        "autoSystemDnsToGateway": "Системний DNS на шлюз",
+        "autoSystemDnsToGatewayTooltip": "Лише Linux: спрямовує systemd-resolved на шлюз TUN, щоб системні запити йшли через Xray. Потрібен шлюз; у разі помилки TUN не запуститься.",
+        "autoSystemWfpBlockLeak": "Блокування витоків (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Лише Windows, разом з авто-маршрутами системи: dns утримує DNS усередині TUN (потрібні DNS-сервери), misconfigtun блокує версію IP, яку жоден маршрут не веде в TUN."
       },
       "info": {
         "mode": "Режим",
@@ -2275,7 +2281,11 @@
         "clearAll": "Видалити всі",
         "clearAllTitle": "Видалити всі DNS-сервери?",
         "clearAllConfirm": "Усі DNS-сервери буде видалено зі списку. Дію не можна скасувати.",
-        "dnsLeakWarning": "DNS може витікати через localhost, звичайний UDP/TCP, локальний режим DoH/DoQ, fallback-запити або EDNS client IP. Для приватності використовуйте маршрутизований DoH, фіксацію hosts і вимикайте fallback."
+        "dnsLeakWarning": "DNS може витікати через localhost, звичайний UDP/TCP, локальний режим DoH/DoQ, fallback-запити або EDNS client IP. Для приватності використовуйте маршрутизований DoH, фіксацію hosts і вимикайте fallback.",
+        "script": "Lua-скрипт",
+        "scriptDesc": "Шлях до файлу .lua з функцією HandleDNSQuery — абсолютний або відносно теки bin Xray. Скрипт відповідає на всі запити й звертається до серверів лише зі свого коду. Xray не запуститься, якщо файл відсутній або містить помилки.",
+        "serverId": "ID",
+        "serverIdDesc": "Ім'я, за яким DNS-скрипт на Lua вибирає цей сервер"
       },
       "fakedns": {
         "add": "Додати підроблений DNS",
@@ -2283,7 +2293,10 @@
         "poolSize": "Розмір пулу"
       },
       "defaultOutbound": "Вихідний за замовчуванням",
-      "defaultOutboundDesc": "Трафік без збігу з правилами маршрутизації йде через цей вихідний (перший у списку)."
+      "defaultOutboundDesc": "Трафік без збігу з правилами маршрутизації йде через цей вихідний (перший у списку).",
+      "routingScript": "Скрипт маршрутизації",
+      "routingScriptDesc": "Шлях до файлу .lua з функцією HandleRoute — абсолютний або відносно теки bin Xray. Скрипт обирає кожен маршрут, а Xray ігнорує всі правила маршрутизації, зокрема правило api панелі: спрямовуйте вхідний api на вихідний api, інакше зупиниться статистика трафіку й застосування змін клієнтів на льоту. Xray не запуститься, якщо файл відсутній або містить помилки.",
+      "routingScriptActive": "У розділі «{section}» задано скрипт маршрутизації, тому Xray ігнорує ці правила."
     },
     "hosts": {
       "addHost": "Додати хост",

+ 16 - 3
internal/web/translation/vi-VN.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "Dải địa chỉ",
           "addressPoolDesc": "Địa chỉ đường hầm được cấp từ các tiền tố này — tối đa một IPv4 và một IPv6."
-        }
+        },
+        "tunDesc": "Loại adapter",
+        "tunDescTooltip": "Chỉ Windows: loại tunnel dùng khi tạo adapter Wintun mới. Để trống nghĩa là Wintun.",
+        "autoSystemDnsToGateway": "DNS hệ thống tới gateway",
+        "autoSystemDnsToGatewayTooltip": "Chỉ Linux: trỏ systemd-resolved tới gateway của TUN để truy vấn của hệ thống đi qua Xray. Cần có gateway, và TUN sẽ không khởi động nếu việc này thất bại.",
+        "autoSystemWfpBlockLeak": "Chặn rò rỉ (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "Chỉ Windows, cùng với Auto system routes: dns giữ DNS bên trong TUN (cần máy chủ DNS), misconfigtun chặn phiên bản IP mà không tuyến nào đưa vào TUN."
       },
       "info": {
         "mode": "Chế độ",
@@ -2275,7 +2281,11 @@
         "clearAll": "Xóa tất cả",
         "clearAllTitle": "Xóa tất cả máy chủ DNS?",
         "clearAllConfirm": "Thao tác này sẽ xóa toàn bộ máy chủ DNS khỏi danh sách. Không thể hoàn tác.",
-        "dnsLeakWarning": "DNS có thể rò rỉ qua localhost, UDP/TCP không mã hóa, DoH/DoQ chế độ cục bộ, truy vấn fallback hoặc EDNS client IP. Hãy dùng DoH được định tuyến, ghim resolver trong hosts và tắt fallback khi cần riêng tư."
+        "dnsLeakWarning": "DNS có thể rò rỉ qua localhost, UDP/TCP không mã hóa, DoH/DoQ chế độ cục bộ, truy vấn fallback hoặc EDNS client IP. Hãy dùng DoH được định tuyến, ghim resolver trong hosts và tắt fallback khi cần riêng tư.",
+        "script": "Script Lua",
+        "scriptDesc": "Đường dẫn tới tệp .lua định nghĩa HandleDNSQuery, tuyệt đối hoặc tương đối với thư mục bin của Xray. Khi đó script trả lời mọi truy vấn và chỉ dùng các máy chủ qua mã của chính nó. Xray sẽ không khởi động nếu tệp không tồn tại hoặc không hợp lệ.",
+        "serverId": "ID",
+        "serverIdDesc": "Tên mà script DNS Lua dùng để chọn máy chủ này"
       },
       "fakedns": {
         "add": "Thêm DNS giả",
@@ -2283,7 +2293,10 @@
         "poolSize": "Kích thước bể bơi"
       },
       "defaultOutbound": "Outbound mặc định",
-      "defaultOutboundDesc": "Lưu lượng không khớp quy tắc định tuyến dùng outbound này (mục đầu danh sách)."
+      "defaultOutboundDesc": "Lưu lượng không khớp quy tắc định tuyến dùng outbound này (mục đầu danh sách).",
+      "routingScript": "Script định tuyến",
+      "routingScriptDesc": "Đường dẫn tới tệp .lua định nghĩa HandleRoute, tuyệt đối hoặc tương đối với thư mục bin của Xray. Khi đó script chọn mọi tuyến và Xray bỏ qua tất cả quy tắc định tuyến, kể cả quy tắc api của panel: hãy chuyển inbound api tới outbound api, nếu không thống kê lưu lượng và việc áp dụng thay đổi client trực tiếp sẽ dừng. Xray sẽ không khởi động nếu tệp không tồn tại hoặc không hợp lệ.",
+      "routingScriptActive": "Một script định tuyến đã được đặt trong {section}, nên Xray bỏ qua các quy tắc này."
     },
     "hosts": {
       "addHost": "Thêm Host",

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

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "地址池",
           "addressPoolDesc": "隧道地址从这些前缀中分配——最多一个 IPv4 和一个 IPv6。"
-        }
+        },
+        "tunDesc": "适配器类型",
+        "tunDescTooltip": "仅限 Windows:新建 Wintun 适配器时使用的隧道类型,留空即为 Wintun。",
+        "autoSystemDnsToGateway": "系统 DNS 指向网关",
+        "autoSystemDnsToGatewayTooltip": "仅限 Linux:将 systemd-resolved 指向 TUN 网关,使系统解析经过 Xray。需要设置网关,失败时 TUN 不会启动。",
+        "autoSystemWfpBlockLeak": "阻止泄漏 (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "仅限 Windows,需配合自动系统路由:dns 让 DNS 只走 TUN(需设置 DNS 服务器),misconfigtun 阻止没有任何路由指向 TUN 的 IP 版本。"
       },
       "info": {
         "mode": "模式",
@@ -2275,7 +2281,11 @@
         "clearAll": "删除全部",
         "clearAllTitle": "删除所有 DNS 服务器?",
         "clearAllConfirm": "此操作将从列表中删除所有 DNS 服务器,且无法撤销。",
-        "dnsLeakWarning": "DNS 可能通过 localhost、明文 UDP/TCP、本地模式 DoH/DoQ、回退查询或 EDNS client IP 泄漏。重视隐私时请使用经路由的 DoH、在 hosts 中固定解析器,并禁用回退。"
+        "dnsLeakWarning": "DNS 可能通过 localhost、明文 UDP/TCP、本地模式 DoH/DoQ、回退查询或 EDNS client IP 泄漏。重视隐私时请使用经路由的 DoH、在 hosts 中固定解析器,并禁用回退。",
+        "script": "Lua 脚本",
+        "scriptDesc": "定义了 HandleDNSQuery 的 .lua 文件路径,可为绝对路径或相对于 Xray bin 目录的路径。设置后由脚本应答所有查询,只通过脚本自身的代码访问服务器。文件不存在或无效时 Xray 无法启动。",
+        "serverId": "ID",
+        "serverIdDesc": "Lua DNS 脚本用来选择此服务器的名称"
       },
       "fakedns": {
         "add": "添加假 DNS",
@@ -2283,7 +2293,10 @@
         "poolSize": "池大小"
       },
       "defaultOutbound": "默认出站",
-      "defaultOutboundDesc": "未匹配任何路由规则的流量走此出站(列表中的第一个出站)。"
+      "defaultOutboundDesc": "未匹配任何路由规则的流量走此出站(列表中的第一个出站)。",
+      "routingScript": "路由脚本",
+      "routingScriptDesc": "定义了 HandleRoute 的 .lua 文件路径,可为绝对路径或相对于 Xray bin 目录的路径。设置后由脚本选择所有路由,Xray 会忽略全部路由规则,包括面板的 api 规则:请把 api 入站路由到 api 出站,否则流量统计和客户端变更的实时生效都会停止。文件不存在或无效时 Xray 无法启动。",
+      "routingScriptActive": "已在“{section}”中设置路由脚本,因此 Xray 会忽略这些规则。"
     },
     "hosts": {
       "addHost": "添加主机",

+ 16 - 3
internal/web/translation/zh-TW.json

@@ -722,7 +722,13 @@
         "masque": {
           "addressPool": "位址池",
           "addressPoolDesc": "通道位址從這些前綴中分配——最多一個 IPv4 與一個 IPv6。"
-        }
+        },
+        "tunDesc": "介面卡類型",
+        "tunDescTooltip": "僅限 Windows:新建 Wintun 介面卡時使用的通道類型,留空即為 Wintun。",
+        "autoSystemDnsToGateway": "系統 DNS 指向閘道",
+        "autoSystemDnsToGatewayTooltip": "僅限 Linux:將 systemd-resolved 指向 TUN 閘道,使系統解析經過 Xray。需要設定閘道,失敗時 TUN 不會啟動。",
+        "autoSystemWfpBlockLeak": "阻止洩漏 (WFP)",
+        "autoSystemWfpBlockLeakTooltip": "僅限 Windows,需搭配自動系統路由:dns 讓 DNS 只走 TUN(需設定 DNS 伺服器),misconfigtun 阻止沒有任何路由指向 TUN 的 IP 版本。"
       },
       "info": {
         "mode": "模式",
@@ -2275,7 +2281,11 @@
         "clearAll": "全部刪除",
         "clearAllTitle": "刪除所有 DNS 伺服器?",
         "clearAllConfirm": "此操作將從清單中刪除所有 DNS 伺服器,無法復原。",
-        "dnsLeakWarning": "DNS 可能透過 localhost、明文 UDP/TCP、本機模式 DoH/DoQ、回退查詢或 EDNS client IP 洩漏。重視隱私時請使用經路由的 DoH、在 hosts 固定解析器,並停用回退。"
+        "dnsLeakWarning": "DNS 可能透過 localhost、明文 UDP/TCP、本機模式 DoH/DoQ、回退查詢或 EDNS client IP 洩漏。重視隱私時請使用經路由的 DoH、在 hosts 固定解析器,並停用回退。",
+        "script": "Lua 腳本",
+        "scriptDesc": "定義了 HandleDNSQuery 的 .lua 檔案路徑,可為絕對路徑或相對於 Xray bin 資料夾的路徑。設定後由腳本回應所有查詢,只透過腳本自身的程式碼存取伺服器。檔案不存在或無效時 Xray 無法啟動。",
+        "serverId": "ID",
+        "serverIdDesc": "Lua DNS 腳本用來選擇此伺服器的名稱"
       },
       "fakedns": {
         "add": "新增假 DNS",
@@ -2283,7 +2293,10 @@
         "poolSize": "池大小"
       },
       "defaultOutbound": "預設出站",
-      "defaultOutboundDesc": "未符合任何路由規則的流量走此出站(清單中的第一個出站)。"
+      "defaultOutboundDesc": "未符合任何路由規則的流量走此出站(清單中的第一個出站)。",
+      "routingScript": "路由腳本",
+      "routingScriptDesc": "定義了 HandleRoute 的 .lua 檔案路徑,可為絕對路徑或相對於 Xray bin 資料夾的路徑。設定後由腳本選擇所有路由,Xray 會忽略全部路由規則,包括面板的 api 規則:請將 api 入站路由到 api 出站,否則流量統計與用戶端變更的即時套用都會停止。檔案不存在或無效時 Xray 無法啟動。",
+      "routingScriptActive": "已在「{section}」中設定路由腳本,因此 Xray 會忽略這些規則。"
     },
     "hosts": {
       "addHost": "新增 Host",