瀏覽代碼

feat(xray): add the XDRIVE transport

xray-core v26.10.10 tunnels a stream through files in shared cloud
storage ("network": "xdrive"): Google Drive, a local folder, or any HTTP
storage API described by a template. The panel could not select it, and
the stream schema rejected a pasted xdrive stream outright.

The inbound and outbound forms now offer XDRIVE with one shared editor:
service, folder, the three Google Drive secrets in the order the core
reads them, a template JSON editor with its {secretN} values, the
optional domain front (the stream-level address/port the storage client
dials), and the core's tuning knobs with their defaults as placeholders.
Switching an inbound to XDRIVE resets security to none, because the
core would otherwise read the inbound's TLS/REALITY as the storage API's
client TLS.

XDRIVE has no share-link format, and a link carrying the storage
secrets would print them into every QR code, so both link generators
emit none for it; the JSON subscription already passes the stream
through intact, and Clash drops the unknown transport.
MHSanaei 17 小時之前
父節點
當前提交
ca5983595b
共有 34 個文件被更改,包括 848 次插入 和 19 次删除
  1. 21 1
      docs/content/docs/en/config/transports.mdx
  2. 21 1
      docs/content/docs/fa/config/transports.mdx
  3. 21 1
      docs/content/docs/ru/config/transports.mdx
  4. 18 1
      docs/content/docs/zh/config/transports.mdx
  5. 1 1
      frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx
  6. 148 0
      frontend/src/lib/xray/forms/transport/XDriveForm.tsx
  7. 7 0
      frontend/src/lib/xray/inbound-link.ts
  8. 4 0
      frontend/src/lib/xray/stream-defaults.ts
  9. 17 0
      frontend/src/pages/inbounds/form/InboundFormModal.tsx
  10. 3 0
      frontend/src/pages/xray/outbounds/OutboundFormModal.tsx
  11. 1 0
      frontend/src/pages/xray/outbounds/outbound-form-constants.ts
  12. 3 0
      frontend/src/pages/xray/outbounds/outbound-form-helpers.ts
  13. 12 0
      frontend/src/schemas/protocols/stream/index.ts
  14. 26 0
      frontend/src/schemas/protocols/stream/xdrive.ts
  15. 25 0
      frontend/src/test/__snapshots__/stream.test.ts.snap
  16. 22 0
      frontend/src/test/golden/fixtures/stream/xdrive-google-drive.json
  17. 44 0
      frontend/src/test/inbound-form-modal.test.tsx
  18. 22 0
      frontend/src/test/inbound-link.test.ts
  19. 88 1
      frontend/src/test/outbound-form-modal.test.tsx
  20. 6 2
      internal/sub/service.go
  21. 15 0
      internal/sub/xdrive_test.go
  22. 25 1
      internal/web/translation/ar-EG.json
  23. 24 0
      internal/web/translation/en-US.json
  24. 25 1
      internal/web/translation/es-ES.json
  25. 25 1
      internal/web/translation/fa-IR.json
  26. 25 1
      internal/web/translation/id-ID.json
  27. 25 1
      internal/web/translation/ja-JP.json
  28. 25 1
      internal/web/translation/pt-BR.json
  29. 24 0
      internal/web/translation/ru-RU.json
  30. 25 1
      internal/web/translation/tr-TR.json
  31. 25 1
      internal/web/translation/uk-UA.json
  32. 25 1
      internal/web/translation/vi-VN.json
  33. 25 1
      internal/web/translation/zh-CN.json
  34. 25 1
      internal/web/translation/zh-TW.json

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

@@ -1,6 +1,6 @@
 ---
 title: Transports & Security
-description: Every transport 3x-ui exposes — TCP, mKCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, Hysteria — with their settings, plus FinalMask obfuscation, sockopt, TLS/REALITY, XTLS-Vision, and VLESS encryption.
+description: Every transport 3x-ui exposes — TCP, mKCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, Hysteria, XDRIVE — with their settings, plus FinalMask obfuscation, sockopt, TLS/REALITY, XTLS-Vision, and VLESS encryption.
 icon: Network
 ---
 
@@ -23,6 +23,7 @@ network writes its own settings key on the wire (`tcpSettings`, `kcpSettings`, 
 | **HTTPUpgrade** | `httpupgradeSettings` | CDN-friendly HTTP/1.1 `Upgrade`; lighter than full WebSocket.          |
 | **XHTTP**       | `xhttpSettings`       | Modern stream-multiplexed HTTP transport; CDN-friendly and REALITY-capable. |
 | **Hysteria**    | `hysteriaSettings`    | QUIC-based transport — only for the **Hysteria2** protocol.            |
+| **XDRIVE**      | `xdriveSettings`      | Tunnels the stream through files in shared cloud storage (Google Drive, a local folder or any HTTP storage API). |
 
 <Callout type="info">
   **WireGuard** and **Tunnel** (dokodemo-door) inbounds expose no transport
@@ -116,6 +117,25 @@ Only valid when the protocol is **Hysteria2**.
 | `udpIdleTimeout` | `60`    | Seconds (2–600) before idle UDP sessions are dropped.                   |
 | `masquerade`     | —       | Disguise as an HTTP/3 server: `type` `proxy`/`file`/`string` with `url`/`dir`/`content`, plus `headers` and `statusCode`. |
 
+### XDRIVE — `xdriveSettings`
+
+XDRIVE carries the stream as files in a folder both ends can reach: the server polls
+that folder instead of accepting connections on its port. Both sides must use the
+**same** service, folder and secrets, so the JSON subscription ships them to every
+client; XDRIVE inbounds get no share link or Clash entry.
+
+| Field          | Default        | Meaning                                                                 |
+| -------------- | -------------- | ----------------------------------------------------------------------- |
+| `service`      | `Google Drive` | `Google Drive`, `local` (a folder on disk) or `template` (any HTTP storage API). |
+| `remoteFolder` | —              | Folder that holds the session files.                                    |
+| `secrets`      | —              | Google Drive: ClientID, ClientSecret, RefreshToken — exactly three, in that order. Template: values read as `{secret0}`, `{secret1}`, …. |
+| `template`     | —              | Template service only: the storage API's `auth`, `put`, `get`, `list` and `delete` operations. |
+| tuning         | core defaults  | `segmentBytes` (512 KiB), `flushIntervalMs` (20), `pollIntervalMs`/`maxPollIntervalMs` (50/500), `eagerWindowMs` (2000), `holeTimeoutMs` (30000), `sessionTtlSeconds` (300), `concurrency` (8). |
+
+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.
+
 ## FinalMask — late-layer obfuscation
 
 **FinalMask** wraps traffic **after** the transport and security layers, so it can

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

@@ -1,6 +1,6 @@
 ---
 title: انتقال‌ها و امنیت
-description: هر انتقالی که 3x-ui ارائه می‌دهد — TCP، mKCP، WebSocket، gRPC، HTTPUpgrade، XHTTP، Hysteria — به‌همراه تنظیماتشان، و نیز مبهم‌سازی FinalMask، sockopt، TLS/REALITY، XTLS-Vision و رمزنگاری VLESS.
+description: هر انتقالی که 3x-ui ارائه می‌دهد — TCP، mKCP، WebSocket، gRPC، HTTPUpgrade، XHTTP، Hysteria، XDRIVE — به‌همراه تنظیماتشان، و نیز مبهم‌سازی FinalMask، sockopt، TLS/REALITY، XTLS-Vision و رمزنگاری VLESS.
 icon: Network
 ---
 
@@ -23,6 +23,7 @@ icon: Network
 | **HTTPUpgrade** | `httpupgradeSettings` | `Upgrade` مربوط به HTTP/1.1 و سازگار با CDN؛ سبک‌تر از WebSocket کامل.  |
 | **XHTTP**       | `xhttpSettings`       | انتقال HTTP مدرن با مالتی‌پلکس جریانی؛ سازگار با CDN و توانمند برای REALITY. |
 | **Hysteria**    | `hysteriaSettings`    | انتقال مبتنی بر QUIC — تنها برای پروتکل **Hysteria2**.                 |
+| **XDRIVE**      | `xdriveSettings`      | جریان را از طریق فایل‌ها در یک فضای ذخیره‌سازی ابری مشترک (Google Drive، یک پوشهٔ محلی یا هر API ذخیره‌سازی HTTP) تونل می‌کند. |
 
 <Callout type="info">
   inbound‌های **WireGuard** و **Tunnel** (dokodemo-door) هیچ انتخابگر انتقالی
@@ -117,6 +118,25 @@ icon: Network
 | `udpIdleTimeout` | `60`    | ثانیه (2–600) پیش از حذف نشست‌های بی‌کار UDP.                           |
 | `masquerade`     | —       | استتار به‌عنوان یک سرور HTTP/3: `type` با مقدار `proxy`/`file`/`string` و `url`/`dir`/`content`، به‌علاوه `headers` و `statusCode`. |
 
+### XDRIVE — `xdriveSettings`
+
+XDRIVE جریان را به‌صورت فایل‌هایی در پوشه‌ای که هر دو طرف به آن دسترسی دارند حمل می‌کند:
+سرور به‌جای پذیرش اتصال روی پورتش، آن پوشه را بررسی می‌کند. هر دو طرف باید سرویس، پوشه
+و مقادیر محرمانهٔ **یکسان** داشته باشند، بنابراین اشتراک JSON آن‌ها را به همهٔ کلاینت‌ها
+می‌فرستد؛ inboundهای XDRIVE لینک اشتراک‌گذاری یا ورودی Clash ندارند.
+
+| فیلد           | پیش‌فرض        | معنا |
+| -------------- | -------------- | ---- |
+| `service`      | `Google Drive` | `Google Drive`، `local` (پوشه‌ای روی دیسک) یا `template` (هر API ذخیره‌سازی HTTP). |
+| `remoteFolder` | —              | پوشه‌ای که فایل‌های نشست در آن قرار می‌گیرند. |
+| `secrets`      | —              | Google Drive: دقیقاً سه مقدار ClientID، ClientSecret و RefreshToken به همین ترتیب. Template: مقادیری که با `{secret0}`، `{secret1}`، … خوانده می‌شوند. |
+| `template`     | —              | فقط برای سرویس template: عملیات `auth`، `put`، `get`، `list` و `delete` در API ذخیره‌ساز. |
+| تنظیمات دقیق   | پیش‌فرض هسته   | `segmentBytes` (512 KiB)، `flushIntervalMs` (20)، `pollIntervalMs`/`maxPollIntervalMs` (50/500)، `eagerWindowMs` (2000)، `holeTimeoutMs` (30000)، `sessionTtlSeconds` (300)، `concurrency` (8). |
+
+**آدرس/پورت Front** اختیاری (کلیدهای `address`/`port` در سطح stream) یک domain front
+است که API ذخیره‌ساز از طریق آن وصل می‌شود؛ نام TLS همچنان میزبان API است. XDRIVE لایهٔ
+TLS/REALITY جداگانه‌ای ندارد — ترافیکش همان HTTPS خود API ذخیره‌ساز است.
+
 ## FinalMask — مبهم‌سازی لایه پایانی
 
 **FinalMask** ترافیک را **پس از** لایه‌های انتقال و امنیت می‌پیچد، بنابراین می‌تواند

+ 21 - 1
docs/content/docs/ru/config/transports.mdx

@@ -1,6 +1,6 @@
 ---
 title: Транспорты и безопасность
-description: Все транспорты, которые предоставляет 3x-ui — TCP, mKCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, Hysteria — с их настройками, а также обфускация FinalMask, sockopt, TLS/REALITY, XTLS-Vision и шифрование VLESS.
+description: Все транспорты, которые предоставляет 3x-ui — TCP, mKCP, WebSocket, gRPC, HTTPUpgrade, XHTTP, Hysteria, XDRIVE — с их настройками, а также обфускация FinalMask, sockopt, TLS/REALITY, XTLS-Vision и шифрование VLESS.
 icon: Network
 ---
 
@@ -25,6 +25,7 @@ network записывает свой собственный ключ настр
 | **HTTPUpgrade** | `httpupgradeSettings` | Дружественный к CDN апгрейд HTTP/1.1 `Upgrade`; легче полноценного WebSocket. |
 | **XHTTP**       | `xhttpSettings`       | Современный HTTP-транспорт с мультиплексированием потоков; дружественный к CDN и совместимый с REALITY. |
 | **Hysteria**    | `hysteriaSettings`    | Транспорт на базе QUIC — только для протокола **Hysteria2**.           |
+| **XDRIVE**      | `xdriveSettings`      | Туннелирует поток через файлы в общем облачном хранилище (Google Drive, локальная папка или любой HTTP API хранилища). |
 
 <Callout type="info">
   Inbound-соединения **WireGuard** и **Tunnel** (dokodemo-door) не предоставляют
@@ -119,6 +120,25 @@ HTTPUpgrade — это одноразовый HTTP/1.1 `Upgrade` без фрей
 | `udpIdleTimeout` | `60`    | Секунды (2–600) до сброса простаивающих UDP-сессий.                     |
 | `masquerade`     | —       | Маскировка под HTTP/3-сервер: `type` `proxy`/`file`/`string` с `url`/`dir`/`content`, а также `headers` и `statusCode`. |
 
+### XDRIVE — `xdriveSettings`
+
+XDRIVE передаёт поток файлами в папке, доступной обеим сторонам: сервер опрашивает эту
+папку, а не принимает соединения на своём порту. Обе стороны должны использовать
+**одинаковые** сервис, папку и секреты, поэтому JSON-подписка передаёт их каждому
+клиенту; для XDRIVE-инбаундов не создаются ссылки и записи Clash.
+
+| Поле           | По умолчанию   | Значение |
+| -------------- | -------------- | -------- |
+| `service`      | `Google Drive` | `Google Drive`, `local` (папка на диске) или `template` (любой HTTP API хранилища). |
+| `remoteFolder` | —              | Папка с файлами сессий. |
+| `secrets`      | —              | Google Drive: ровно три значения — ClientID, ClientSecret, RefreshToken, в этом порядке. Template: значения, доступные как `{secret0}`, `{secret1}`, …. |
+| `template`     | —              | Только для template: операции `auth`, `put`, `get`, `list` и `delete` API хранилища. |
+| тонкая настройка | значения ядра | `segmentBytes` (512 KiB), `flushIntervalMs` (20), `pollIntervalMs`/`maxPollIntervalMs` (50/500), `eagerWindowMs` (2000), `holeTimeoutMs` (30000), `sessionTtlSeconds` (300), `concurrency` (8). |
+
+Необязательные **адрес/порт фронтинга** (`address`/`port` на уровне stream) — домен-фронт,
+через который идёт подключение к API хранилища; в TLS по-прежнему указывается хост API.
+У XDRIVE нет собственного уровня TLS/REALITY — его трафик это HTTPS самого API хранилища.
+
 ## FinalMask — обфускация на позднем уровне
 
 **FinalMask** оборачивает трафик **после** уровней транспорта и безопасности,

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

@@ -1,6 +1,6 @@
 ---
 title: 传输方式与安全层
-description: 3x-ui 提供的每一种传输方式——TCP、mKCP、WebSocket、gRPC、HTTPUpgrade、XHTTP、Hysteria——及其设置项,外加 FinalMask 混淆、sockopt、TLS/REALITY、XTLS-Vision 以及 VLESS 加密。
+description: 3x-ui 提供的每一种传输方式——TCP、mKCP、WebSocket、gRPC、HTTPUpgrade、XHTTP、Hysteria、XDRIVE——及其设置项,外加 FinalMask 混淆、sockopt、TLS/REALITY、XTLS-Vision 以及 VLESS 加密。
 icon: Network
 ---
 
@@ -22,6 +22,7 @@ icon: Network
 | **HTTPUpgrade** | `httpupgradeSettings` | 对 CDN 友好的 HTTP/1.1 `Upgrade`;比完整的 WebSocket 更轻量。          |
 | **XHTTP**       | `xhttpSettings`       | 现代的流多路复用 HTTP 传输;对 CDN 友好且支持 REALITY。               |
 | **Hysteria**    | `hysteriaSettings`    | 基于 QUIC 的传输——仅用于 **Hysteria2** 协议。                          |
+| **XDRIVE**      | `xdriveSettings`      | 通过共享云存储(Google Drive、本地文件夹或任意 HTTP 存储 API)中的文件传输数据流。 |
 
 <Callout type="info">
   **WireGuard** 和 **Tunnel**(dokodemo-door)入站不提供传输方式选择器——它们的传输流
@@ -112,6 +113,22 @@ Session-ID 字段(`sessionIDPlacement`、`sessionIDKey`、`sessionIDTable`、
 | `udpIdleTimeout` | `60`    | 空闲 UDP 会话被丢弃前的秒数(2–600)。                                 |
 | `masquerade`     | —       | 伪装成一个 HTTP/3 服务器:`type` 为 `proxy`/`file`/`string`,配合 `url`/`dir`/`content`,外加 `headers` 与 `statusCode`。 |
 
+### XDRIVE — `xdriveSettings`
+
+XDRIVE 把数据流作为文件存放在双方都能访问的文件夹中:服务端轮询该文件夹,而不是在端口上接受连接。
+双方必须使用**相同**的服务、文件夹和密钥,因此 JSON 订阅会把它们下发给每个客户端;XDRIVE 入站不生成分享链接或 Clash 条目。
+
+| 字段           | 默认值         | 含义 |
+| -------------- | -------------- | ---- |
+| `service`      | `Google Drive` | `Google Drive`、`local`(磁盘上的文件夹)或 `template`(任意 HTTP 存储 API)。 |
+| `remoteFolder` | —              | 存放会话文件的文件夹。 |
+| `secrets`      | —              | Google Drive:恰好三项,依次为 ClientID、ClientSecret、RefreshToken。Template:以 `{secret0}`、`{secret1}`… 引用的值。 |
+| `template`     | —              | 仅用于 template 服务:存储 API 的 `auth`、`put`、`get`、`list` 和 `delete` 操作。 |
+| 调优参数       | 内核默认值     | `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。
+
 ## FinalMask — 末层混淆
 
 **FinalMask** 在传输方式和安全层**之后**包裹流量,因此它既能伪装那些承载不了 TLS 的

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

@@ -36,7 +36,7 @@ export interface FinalMaskFormProps {
   showAll?: boolean;
 }
 
-const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'];
+const TCP_NETWORKS = ['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp', 'xdrive'];
 const DEFAULT_GECKO_PACKET_SIZE = { min: 512, max: 1200 };
 // Xray-core caps the Gecko output packet size at its internal buffer (2048)
 // and needs 1 <= min <= max; mirror those bounds so the panel rejects what

+ 148 - 0
frontend/src/lib/xray/forms/transport/XDriveForm.tsx

@@ -0,0 +1,148 @@
+import { useState } from 'react';
+import { useTranslation } from 'react-i18next';
+import { Input, InputNumber, Select, Typography } from 'antd';
+import { useFormContext, useWatch } from 'react-hook-form';
+
+import { JsonEditor } from '@/components/form';
+import { FormField } from '@/components/form/rhf';
+import { XDriveServiceSchema } from '@/schemas/protocols/stream/xdrive';
+
+const BASE = ['streamSettings', 'xdriveSettings'] as const;
+
+const SERVICE_LABEL_KEYS: Partial<Record<string, string>> = {
+  local: 'pages.inbounds.form.xdrive.serviceLocal',
+  template: 'pages.inbounds.form.xdrive.serviceTemplate',
+};
+
+// Core defaults (transport/internet/xdrive/params.go), shown as placeholders so an
+// empty field keeps meaning "use the core's value".
+const TUNING_FIELDS = [
+  ['segmentBytes', 'segmentBytes', 524288],
+  ['flushIntervalMs', 'flushInterval', 20],
+  ['pollIntervalMs', 'pollInterval', 50],
+  ['maxPollIntervalMs', 'maxPollInterval', 500],
+  ['eagerWindowMs', 'eagerWindow', 2000],
+  ['holeTimeoutMs', 'holeTimeout', 30000],
+  ['sessionTtlSeconds', 'sessionTtl', 300],
+  ['concurrency', 'concurrency', 8],
+] as const;
+
+const GOOGLE_DRIVE_SECRETS = ['clientId', 'clientSecret', 'refreshToken'] as const;
+
+function TemplateEditor({
+  value,
+  onChange,
+}: {
+  value?: Record<string, unknown>;
+  onChange?: (next: Record<string, unknown> | undefined) => void;
+}) {
+  const { t } = useTranslation();
+  const [text, setText] = useState(() => (value ? JSON.stringify(value, null, 2) : ''));
+  const [invalid, setInvalid] = useState(false);
+  return (
+    <>
+      <JsonEditor
+        value={text}
+        minHeight="160px"
+        onChange={(next) => {
+          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">
+          {t('pages.inbounds.form.xdrive.templateInvalid')}
+        </Typography.Text>
+      )}
+    </>
+  );
+}
+
+export default function XDriveForm() {
+  const { t } = useTranslation();
+  const { control } = useFormContext();
+  const service = useWatch({ control, name: `${BASE.join('.')}.service` }) as string | undefined;
+  const serviceOptions = XDriveServiceSchema.options.map((value) => {
+    const labelKey = SERVICE_LABEL_KEYS[value];
+    return { value, label: labelKey ? t(labelKey) : value };
+  });
+
+  return (
+    <>
+      <FormField label={t('pages.inbounds.form.xdrive.service')} name={[...BASE, 'service']}>
+        <Select options={serviceOptions} />
+      </FormField>
+      <FormField
+        label={t('pages.inbounds.form.xdrive.remoteFolder')}
+        name={[...BASE, 'remoteFolder']}
+        required
+      >
+        <Input placeholder={service === 'local' ? '/var/lib/xdrive' : 'xray-tunnel'} />
+      </FormField>
+      {service === 'Google Drive' &&
+        GOOGLE_DRIVE_SECRETS.map((key, index) => (
+          <FormField
+            key={key}
+            label={t(`pages.inbounds.form.xdrive.${key}`)}
+            name={[...BASE, 'secrets', index]}
+            required
+          >
+            {key === 'clientId' ? <Input /> : <Input.Password />}
+          </FormField>
+        ))}
+      {service === 'template' && (
+        <>
+          <FormField
+            label={t('pages.inbounds.form.xdrive.secrets')}
+            tooltip={t('pages.inbounds.form.xdrive.secretsDesc')}
+            name={[...BASE, 'secrets']}
+          >
+            <Select mode="tags" tokenSeparators={[',']} open={false} />
+          </FormField>
+          <FormField
+            label={t('pages.inbounds.form.xdrive.template')}
+            name={[...BASE, 'template']}
+            required
+          >
+            <TemplateEditor />
+          </FormField>
+        </>
+      )}
+      {service !== 'local' && (
+        <>
+          <FormField
+            label={t('pages.inbounds.form.xdrive.front')}
+            tooltip={t('pages.inbounds.form.xdrive.frontDesc')}
+            name={['streamSettings', 'address']}
+            transform={{ output: (raw) => String(raw ?? '').trim() || undefined }}
+          >
+            <Input placeholder="www.googleapis.com" />
+          </FormField>
+          <FormField
+            label={t('pages.inbounds.form.xdrive.frontPort')}
+            name={['streamSettings', 'port']}
+          >
+            <InputNumber min={1} max={65535} placeholder="443" />
+          </FormField>
+        </>
+      )}
+      {TUNING_FIELDS.map(([key, label, placeholder]) => (
+        <FormField key={key} label={t(`pages.inbounds.form.xdrive.${label}`)} name={[...BASE, key]}>
+          <InputNumber min={0} placeholder={String(placeholder)} style={{ width: '100%' }} />
+        </FormField>
+      ))}
+    </>
+  );
+}

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

@@ -1449,6 +1449,11 @@ export interface GenLinkInput {
   externalProxy?: ExternalProxyEntry | null;
 }
 
+// XDRIVE has no link format; a link carrying its storage secrets would leak them.
+function hasNoLinkFormat(inbound: Inbound): boolean {
+  return inbound.streamSettings?.network === 'xdrive';
+}
+
 // Per-protocol dispatcher matching the legacy `genLink` switch. Returns
 // '' for protocols that don't have client-based share links (wireguard
 // goes through genWireguardLinks/Configs separately, http/mixed/tunnel
@@ -1463,6 +1468,7 @@ export function genLink(input: GenLinkInput): string {
     client,
     externalProxy = null,
   } = input;
+  if (hasNoLinkFormat(inbound)) return '';
   switch (inbound.protocol) {
     case 'vmess':
       return genVmessLink({
@@ -1603,6 +1609,7 @@ export interface GenInboundLinksInput {
 // other clientless protocols (http, mixed, tunnel).
 export function genInboundLinks(input: GenInboundLinksInput): string {
   const { inbound, remark = '', hostOverride = '', fallbackHostname } = input;
+  if (hasNoLinkFormat(inbound)) return '';
   const addr = resolveAddr(inbound, hostOverride, fallbackHostname);
   const clients = getInboundClients(inbound);
   if (clients) {

+ 4 - 0
frontend/src/lib/xray/stream-defaults.ts

@@ -5,6 +5,7 @@ import {
   KcpStreamSettingsSchema,
   TcpStreamSettingsSchema,
   WsStreamSettingsSchema,
+  XDriveStreamSettingsSchema,
   XHttpStreamSettingsSchema,
 } from '@/schemas/protocols/stream';
 import { RealityStreamSettingsSchema, TlsStreamSettingsSchema } from '@/schemas/protocols/security';
@@ -17,6 +18,7 @@ const NETWORK_KEY_MAP = {
   httpupgrade: 'httpupgradeSettings',
   xhttp: 'xhttpSettings',
   hysteria: 'hysteriaSettings',
+  xdrive: 'xdriveSettings',
 } as const;
 
 type SchemaWithParse = { safeParse: (v: unknown) => { success: boolean; data?: unknown } };
@@ -44,6 +46,8 @@ function networkSchemaFor(network: string): SchemaWithParse | null {
       return XHttpStreamSettingsSchema;
     case 'hysteria':
       return HysteriaStreamSettingsSchema;
+    case 'xdrive':
+      return XDriveStreamSettingsSchema;
     default:
       return null;
   }

+ 17 - 0
frontend/src/pages/inbounds/form/InboundFormModal.tsx

@@ -54,8 +54,10 @@ import { WsStreamSettingsSchema } from '@/schemas/protocols/stream/ws';
 import { GrpcStreamSettingsSchema } from '@/schemas/protocols/stream/grpc';
 import { HttpUpgradeStreamSettingsSchema } from '@/schemas/protocols/stream/httpupgrade';
 import { XHttpStreamSettingsSchema } from '@/schemas/protocols/stream/xhttp';
+import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
 import { DateTimePicker } from '@/components/form';
 import { FinalMaskField } from '@/lib/xray/forms/fields';
+import XDriveForm from '@/lib/xray/forms/transport/XDriveForm';
 import './InboundFormModal.css';
 
 import { AdvancedAllEditor, AdvancedSliceEditor } from './advanced-editors';
@@ -214,6 +216,8 @@ function newStreamSlice(n: string): Record<string, unknown> {
       return HttpUpgradeStreamSettingsSchema.parse({});
     case 'xhttp':
       return XHttpStreamSettingsSchema.parse({});
+    case 'xdrive':
+      return XDriveStreamSettingsSchema.parse({});
     default:
       return {};
   }
@@ -865,12 +869,22 @@ export default function InboundFormModal({
       'grpcSettings',
       'httpupgradeSettings',
       'xhttpSettings',
+      'xdriveSettings',
     ];
     const current = (getV('streamSettings') as Record<string, unknown>) ?? {};
     const cleaned: Record<string, unknown> = { ...current, network: next };
     for (const k of ALL) {
       if (k !== `${next}Settings`) delete cleaned[k];
     }
+    if (next === 'xdrive') {
+      /* The core would read the inbound's TLS as the storage API's client TLS. */
+      cleaned.security = 'none';
+      delete cleaned.tlsSettings;
+      delete cleaned.realitySettings;
+    } else {
+      delete cleaned.address;
+      delete cleaned.port;
+    }
     cleaned[`${next}Settings`] = newStreamSlice(next);
     if (next === 'kcp') {
       const fm = (cleaned.finalmask as Record<string, unknown> | undefined) ?? {};
@@ -918,6 +932,7 @@ export default function InboundFormModal({
               { value: 'grpc', label: 'gRPC' },
               { value: 'httpupgrade', label: 'HTTPUpgrade' },
               { value: 'xhttp', label: 'XHTTP' },
+              { value: 'xdrive', label: 'XDRIVE' },
             ]}
           />
         </Form.Item>
@@ -941,6 +956,8 @@ export default function InboundFormModal({
           {network === 'httpupgrade' && <HttpUpgradeForm />}
 
           {network === 'kcp' && <KcpForm />}
+
+          {network === 'xdrive' && <XDriveForm />}
         </>
       )}
 

+ 3 - 0
frontend/src/pages/xray/outbounds/OutboundFormModal.tsx

@@ -8,6 +8,7 @@ import { JsonEditor } from '@/components/form';
 import { Wireguard } from '@/utils';
 import { formValuesToWirePayload, rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
 import { parseOutboundLink } from '@/lib/xray/outbound-link-parser';
+import XDriveForm from '@/lib/xray/forms/transport/XDriveForm';
 import { XMUX_FRESH_DEFAULTS } from '@/schemas/protocols/stream/xhttp';
 import { OutboundFormBaseSchema, type OutboundFormValues } from '@/schemas/forms/outbound-form';
 import {
@@ -487,6 +488,8 @@ export default function OutboundFormModal({
                           {network === 'xhttp' && <XhttpForm onXmuxToggle={onXmuxToggle} />}
 
                           {network === 'hysteria' && <HysteriaForm />}
+
+                          {network === 'xdrive' && <XDriveForm />}
                         </>
                       )}
 

+ 1 - 0
frontend/src/pages/xray/outbounds/outbound-form-constants.ts

@@ -44,6 +44,7 @@ export const NETWORK_OPTIONS: { value: string; label: string }[] = [
   { value: 'grpc', label: 'gRPC' },
   { value: 'httpupgrade', label: 'HTTPUpgrade' },
   { value: 'xhttp', label: 'XHTTP' },
+  { value: 'xdrive', label: 'XDRIVE' },
 ];
 
 // The hysteria protocol is locked to its own QUIC transport: the selector

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

@@ -1,6 +1,7 @@
 import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
 import { canEnableReality, canEnableTls } from '@/lib/xray/protocol-capabilities';
 import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
+import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
 
 import { MUX_PROTOCOLS } from './outbound-form-constants';
 
@@ -64,6 +65,8 @@ export function newStreamSlice(network: string): Record<string, unknown> {
           udpIdleTimeout: 60,
         },
       };
+    case 'xdrive':
+      return { network: 'xdrive', xdriveSettings: XDriveStreamSettingsSchema.parse({}) };
     default:
       return { network: 'tcp', tcpSettings: { header: { type: 'none' } } };
   }

+ 12 - 0
frontend/src/schemas/protocols/stream/index.ts

@@ -1,5 +1,7 @@
 import { z } from 'zod';
 
+import { PortSchema } from '@/schemas/primitives';
+
 import { ExternalProxyEntrySchema } from './external-proxy';
 import { FinalMaskStreamSettingsSchema } from './finalmask';
 import { GrpcStreamSettingsSchema } from './grpc';
@@ -9,6 +11,7 @@ import { KcpStreamSettingsSchema } from './kcp';
 import { SockoptStreamSettingsSchema } from './sockopt';
 import { TcpStreamSettingsSchema } from './tcp';
 import { WsStreamSettingsSchema } from './ws';
+import { XDriveStreamSettingsSchema } from './xdrive';
 import { XHttpStreamSettingsSchema } from './xhttp';
 
 export * from './external-proxy';
@@ -20,6 +23,7 @@ export * from './kcp';
 export * from './sockopt';
 export * from './tcp';
 export * from './ws';
+export * from './xdrive';
 export * from './xhttp';
 
 export const NetworkSchema = z.enum([
@@ -30,6 +34,7 @@ export const NetworkSchema = z.enum([
   'httpupgrade',
   'xhttp',
   'hysteria',
+  'xdrive',
 ]);
 export type Network = z.infer<typeof NetworkSchema>;
 
@@ -53,6 +58,13 @@ const TransportNetworkSettingsSchema = z.discriminatedUnion('network', [
   }),
   z.object({ network: z.literal('xhttp'), xhttpSettings: XHttpStreamSettingsSchema }),
   z.object({ network: z.literal('hysteria'), hysteriaSettings: HysteriaStreamSettingsSchema }),
+  // The stream-level address/port is the domain front XDRIVE dials for its storage API.
+  z.object({
+    network: z.literal('xdrive'),
+    xdriveSettings: XDriveStreamSettingsSchema,
+    address: z.string().optional(),
+    port: PortSchema.optional(),
+  }),
 ]);
 
 // Wireguard (always a UDP listener) and Tunnel (dokodemo-door) expose no

+ 26 - 0
frontend/src/schemas/protocols/stream/xdrive.ts

@@ -0,0 +1,26 @@
+import { z } from 'zod';
+
+// XDRIVE tunnels the stream through files in shared cloud storage, so both ends
+// carry the same service, folder and secrets. Zero/absent tuning knobs take the
+// core's defaults; segmentBytes and concurrency are capped there at 16 MiB / 64.
+export const XDriveServiceSchema = z.enum(['Google Drive', 'local', 'template']);
+export type XDriveService = z.infer<typeof XDriveServiceSchema>;
+
+const MillisSchema = z.number().int().min(0);
+
+export const XDriveStreamSettingsSchema = z.object({
+  service: XDriveServiceSchema.default('Google Drive'),
+  remoteFolder: z.string().default(''),
+  // Google Drive takes exactly three, in order: ClientID, ClientSecret, RefreshToken.
+  secrets: z.array(z.string()).default([]),
+  segmentBytes: z.number().int().min(0).optional(),
+  flushIntervalMs: MillisSchema.optional(),
+  pollIntervalMs: MillisSchema.optional(),
+  maxPollIntervalMs: MillisSchema.optional(),
+  sessionTtlSeconds: z.number().int().min(0).optional(),
+  concurrency: z.number().int().min(0).optional(),
+  eagerWindowMs: MillisSchema.optional(),
+  holeTimeoutMs: MillisSchema.optional(),
+  template: z.record(z.string(), z.unknown()).optional(),
+});
+export type XDriveStreamSettings = z.infer<typeof XDriveStreamSettingsSchema>;

+ 25 - 0
frontend/src/test/__snapshots__/stream.test.ts.snap

@@ -35,6 +35,31 @@ exports[`NetworkSettingsSchema fixtures > parses ws-default byte-stably 1`] = `
 }
 `;
 
+exports[`NetworkSettingsSchema fixtures > parses xdrive-google-drive byte-stably 1`] = `
+{
+  "address": "142.250.0.1",
+  "network": "xdrive",
+  "port": 443,
+  "xdriveSettings": {
+    "concurrency": 8,
+    "eagerWindowMs": 2000,
+    "flushIntervalMs": 20,
+    "holeTimeoutMs": 30000,
+    "maxPollIntervalMs": 500,
+    "pollIntervalMs": 50,
+    "remoteFolder": "xray-tunnel",
+    "secrets": [
+      "000000000000-example.apps.googleusercontent.com",
+      "GOCSPX-example-client-secret",
+      "1//example-refresh-token",
+    ],
+    "segmentBytes": 524288,
+    "service": "Google Drive",
+    "sessionTtlSeconds": 300,
+  },
+}
+`;
+
 exports[`NetworkSettingsSchema fixtures > parses xhttp-basic byte-stably 1`] = `
 {
   "network": "xhttp",

+ 22 - 0
frontend/src/test/golden/fixtures/stream/xdrive-google-drive.json

@@ -0,0 +1,22 @@
+{
+  "network": "xdrive",
+  "address": "142.250.0.1",
+  "port": 443,
+  "xdriveSettings": {
+    "service": "Google Drive",
+    "remoteFolder": "xray-tunnel",
+    "secrets": [
+      "000000000000-example.apps.googleusercontent.com",
+      "GOCSPX-example-client-secret",
+      "1//example-refresh-token"
+    ],
+    "segmentBytes": 524288,
+    "flushIntervalMs": 20,
+    "pollIntervalMs": 50,
+    "maxPollIntervalMs": 500,
+    "sessionTtlSeconds": 300,
+    "concurrency": 8,
+    "eagerWindowMs": 2000,
+    "holeTimeoutMs": 30000
+  }
+}

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

@@ -318,6 +318,50 @@ describe('InboundFormModal', () => {
     });
   });
 
+  // The core reads an XDRIVE inbound's TLS/REALITY as the storage API's client TLS, so
+  // switching a REALITY inbound to XDRIVE must store security none with its secrets.
+  it('switches a REALITY inbound to an XDRIVE stream with security none', async () => {
+    const post = vi.mocked(HttpUtil.post);
+    post.mockClear();
+    renderCloneLikeEdit(cloneLikeVlessInbound('example.com:443'));
+
+    fireEvent.click(screen.getByRole('tab', { name: 'Stream' }));
+    const transmission = Array.from(document.querySelectorAll('.ant-form-item')).find(
+      (el) =>
+        el.querySelector('.ant-form-item-label label')?.textContent?.trim() === 'Transmission',
+    );
+    const select = transmission?.querySelector('.ant-select') as HTMLElement;
+    fireEvent.mouseDown(select.querySelector('.ant-select-selector') ?? select);
+    const option = Array.from(document.querySelectorAll('.ant-select-item-option')).find(
+      (o) => (o.getAttribute('title') ?? o.textContent ?? '').trim() === 'XDRIVE',
+    ) as HTMLElement;
+    fireEvent.click(option);
+    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(await screen.findByLabelText(label), { target: { value } });
+    }
+    fireEvent.click(primaryButton());
+
+    const isUpdate = ([url]: unknown[]) => url === '/panel/api/inbounds/update/42';
+    await waitFor(() => expect(post.mock.calls.some(isUpdate)).toBe(true));
+    const payload = post.mock.calls.find(isUpdate)![1] as { streamSettings: string };
+    const stream = JSON.parse(payload.streamSettings) as Record<string, unknown>;
+    expect(stream).toMatchObject({
+      network: 'xdrive',
+      security: 'none',
+      xdriveSettings: {
+        service: 'Google Drive',
+        remoteFolder: 'xray-tunnel',
+        secrets: ['id.apps.googleusercontent.com', 'client-secret', 'refresh-token'],
+      },
+    });
+    expect(stream.realitySettings).toBeUndefined();
+  });
+
   // 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 () => {

+ 22 - 0
frontend/src/test/inbound-link.test.ts

@@ -127,6 +127,28 @@ describe('genVlessLink', () => {
   }
 });
 
+describe('XDRIVE share links', () => {
+  // No client reads an xdrive link, and one carrying the storage secrets would print
+  // them into every QR code; mirrors the Go generator, which emits none either.
+  it('emits no link for an XDRIVE stream', () => {
+    const [, raw] = fixturesForProtocol('vless').find(([name]) => name === 'vless-ws-tls')!;
+    const inbound = InboundSchema.parse({
+      ...raw,
+      streamSettings: {
+        network: 'xdrive',
+        security: 'none',
+        xdriveSettings: { service: 'local', remoteFolder: '/srv/xdrive' },
+      },
+    });
+    const client = (raw as { settings: { clients: Array<{ id: string; email: string }> } }).settings
+      .clients[0];
+
+    const entries = genAllLinks({ inbound, client, fallbackHostname: 'panel.example.test' });
+
+    expect(entries.map((e) => e.link)).toEqual(['']);
+  });
+});
+
 describe('applyVlessRoute', () => {
   const id = '11111111-2222-4333-8444-555555555555';
   it('encodes a single value into the 3rd group and no-ops on invalid input', () => {

+ 88 - 1
frontend/src/test/outbound-form-modal.test.tsx

@@ -1,5 +1,5 @@
 import { describe, it, expect, vi } from 'vitest';
-import { act, fireEvent, render } from '@testing-library/react';
+import { act, fireEvent, render, screen } from '@testing-library/react';
 import { QueryClientProvider } from '@tanstack/react-query';
 
 import { ThemeProvider } from '@/hooks/useTheme';
@@ -24,6 +24,23 @@ function renderModal(outbound: Record<string, unknown> | null = null) {
   );
 }
 
+function formItemByLabel(label: string): Element | undefined {
+  return Array.from(document.querySelectorAll('.ant-form-item')).find(
+    (el) => (el.querySelector('.ant-form-item-label label')?.textContent ?? '').trim() === label,
+  );
+}
+
+function chooseOptionByLabel(label: string, optionText: string) {
+  const select = formItemByLabel(label)?.querySelector('.ant-select');
+  if (!select) throw new Error(`${label} select not found`);
+  fireEvent.mouseDown(select.querySelector('.ant-select-selector') ?? select);
+  const option = Array.from(document.querySelectorAll('.ant-select-item-option')).find(
+    (o) => (o.getAttribute('title') ?? o.textContent ?? '').trim() === optionText,
+  );
+  if (!option) throw new Error(`Option '${optionText}' not found for ${label}`);
+  fireEvent.click(option);
+}
+
 function toggleSwitch(label: string) {
   const item = Array.from(document.querySelectorAll('.ant-form-item')).find(
     (el) => (el.querySelector('.ant-form-item-label label')?.textContent ?? '').trim() === label,
@@ -290,4 +307,74 @@ describe('OutboundFormModal', () => {
       useSystemCA: true,
     });
   });
+
+  // xray-core reads Google Drive's three secrets by position (ClientID, ClientSecret,
+  // RefreshToken); a misordered list fails OAuth only when the tunnel first dials.
+  it('saves an XDRIVE stream picked in the transmission selector', async () => {
+    const onConfirm = vi.fn();
+    const queryClient = makeTestQueryClient();
+    const outbound = {
+      protocol: 'vless',
+      tag: 'drive-out',
+      settings: {
+        vnext: [
+          {
+            address: 'example.com',
+            port: 443,
+            users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
+          },
+        ],
+      },
+      streamSettings: { network: 'tcp', security: 'none' },
+    };
+    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));
+    });
+    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',
+      xdriveSettings: {
+        service: 'Google Drive',
+        remoteFolder: 'xray-tunnel',
+        secrets: ['id.apps.googleusercontent.com', 'client-secret', 'refresh-token'],
+      },
+    });
+  });
 });

+ 6 - 2
internal/sub/service.go

@@ -818,9 +818,13 @@ func mergeStreamFromMaster(childStream, masterStream string) string {
 
 // GetLink dispatches to the protocol-specific generator for one (inbound, client)
 // pair. Returns "" when the inbound's protocol doesn't produce a subscription URL
-// (socks, http, mixed, wireguard, dokodemo, tunnel). The returned string may
-// contain multiple `\n`-separated URLs when the inbound has externalProxy set.
+// (socks, http, mixed, wireguard, dokodemo, tunnel) or its transport has no link
+// format (xdrive). The returned string may contain multiple `\n`-separated URLs
+// when the inbound has externalProxy set.
 func (s *SubService) GetLink(inbound *model.Inbound, email string) string {
+	if network, _ := unmarshalStreamSettings(inbound.StreamSettings)["network"].(string); network == "xdrive" {
+		return ""
+	}
 	switch inbound.Protocol {
 	case "vmess":
 		return s.genVmessLink(inbound, email)

+ 15 - 0
internal/sub/xdrive_test.go

@@ -0,0 +1,15 @@
+package sub
+
+import "testing"
+
+// XDRIVE has no share-link format: a link would either drop the storage secrets both
+// ends need or print them into every QR code, so only the JSON subscription carries it.
+func TestGetLink_SkipsXdriveStreams(t *testing.T) {
+	stream := `{"network":"xdrive","security":"none",
+		"xdriveSettings":{"service":"Google Drive","remoteFolder":"xray-tunnel","secrets":["id","secret","token"]}}`
+	inbound := flowTestInbound(stream, "none")
+
+	if link := (&SubService{}).GetLink(inbound, "user"); link != "" {
+		t.Fatalf("GetLink = %q, want no link for an XDRIVE stream", link)
+	}
+}

+ 25 - 1
internal/web/translation/ar-EG.json

@@ -694,7 +694,31 @@
         "bytesPerSecTip": "حد السرعة (بايت/ثانية) المطبّق على مرور الـ fallback بعد العتبة، عشان الفحوصات ماتقدرش تستخدم سيرفرك كنطاق ترددي مجاني للهدف. 0 = بلا حد (بيعطّل الاتجاه ده).",
         "burstBytesPerSec": "بايت في الثانية للدفقة",
         "burstBytesPerSecTip": "السماح بدفقات قصيرة فوق المعدل الثابت (حجم token-bucket). لو أقل من بايت في الثانية بيترفع ليطابقه.",
-        "autoFill": "الملء التلقائي"
+        "autoFill": "الملء التلقائي",
+        "xdrive": {
+          "service": "خدمة التخزين",
+          "serviceLocal": "مجلد محلي",
+          "serviceTemplate": "قالب مخصص",
+          "remoteFolder": "المجلد البعيد",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "الأسرار",
+          "secretsDesc": "قيم يقرؤها القالب باسم {secret0} و{secret1} …",
+          "template": "قالب التخزين",
+          "templateInvalid": "يجب أن يكون كائن JSON",
+          "front": "عنوان الواجهة (Front)",
+          "frontDesc": "مضيف أو IP اختياري يُتصل عبره بواجهة API التخزين (domain fronting).",
+          "frontPort": "منفذ الواجهة (Front)",
+          "segmentBytes": "حجم المقطع (بايت)",
+          "flushInterval": "فاصل الإرسال (ms)",
+          "pollInterval": "فاصل الاستطلاع (ms)",
+          "maxPollInterval": "أقصى فاصل استطلاع (ms)",
+          "eagerWindow": "نافذة الإرسال المبكر (ms)",
+          "holeTimeout": "مهلة المقطع المفقود (ms)",
+          "sessionTtl": "مدة صلاحية الجلسة (s)",
+          "concurrency": "التزامن"
+        }
       },
       "info": {
         "mode": "الوضع",

+ 24 - 0
internal/web/translation/en-US.json

@@ -695,6 +695,30 @@
           "node": "Node address",
           "listen": "Inbound listen",
           "custom": "Custom"
+        },
+        "xdrive": {
+          "service": "Storage service",
+          "serviceLocal": "Local folder",
+          "serviceTemplate": "Custom template",
+          "remoteFolder": "Remote folder",
+          "clientId": "Client ID",
+          "clientSecret": "Client secret",
+          "refreshToken": "Refresh token",
+          "secrets": "Secrets",
+          "secretsDesc": "Values the template reads as {secret0}, {secret1}, …",
+          "template": "Storage template",
+          "templateInvalid": "Must be a JSON object",
+          "front": "Front address",
+          "frontDesc": "Optional host or IP the storage API is dialed through (domain fronting).",
+          "frontPort": "Front port",
+          "segmentBytes": "Segment size (bytes)",
+          "flushInterval": "Flush interval (ms)",
+          "pollInterval": "Poll interval (ms)",
+          "maxPollInterval": "Max poll interval (ms)",
+          "eagerWindow": "Eager window (ms)",
+          "holeTimeout": "Missing segment timeout (ms)",
+          "sessionTtl": "Session TTL (s)",
+          "concurrency": "Concurrency"
         }
       },
       "info": {

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

@@ -694,7 +694,31 @@
           "listen": "Dirección de escucha del inbound",
           "custom": "Personalizada"
         },
-        "autoFill": "Autocompletar"
+        "autoFill": "Autocompletar",
+        "xdrive": {
+          "service": "Servicio de almacenamiento",
+          "serviceLocal": "Carpeta local",
+          "serviceTemplate": "Plantilla personalizada",
+          "remoteFolder": "Carpeta remota",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "Secretos",
+          "secretsDesc": "Valores que la plantilla lee como {secret0}, {secret1}, …",
+          "template": "Plantilla de almacenamiento",
+          "templateInvalid": "Debe ser un objeto JSON",
+          "front": "Dirección de fronting",
+          "frontDesc": "Host o IP opcional a través del cual se conecta a la API de almacenamiento (domain fronting).",
+          "frontPort": "Puerto de fronting",
+          "segmentBytes": "Tamaño de segmento (bytes)",
+          "flushInterval": "Intervalo de envío (ms)",
+          "pollInterval": "Intervalo de sondeo (ms)",
+          "maxPollInterval": "Intervalo máximo de sondeo (ms)",
+          "eagerWindow": "Ventana anticipada (ms)",
+          "holeTimeout": "Tiempo de espera de segmento perdido (ms)",
+          "sessionTtl": "TTL de sesión (s)",
+          "concurrency": "Concurrencia"
+        }
       },
       "info": {
         "mode": "Modo",

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

@@ -694,7 +694,31 @@
           "listen": "آدرس شنود ورودی",
           "custom": "سفارشی"
         },
-        "autoFill": "تکمیل خودکار"
+        "autoFill": "تکمیل خودکار",
+        "xdrive": {
+          "service": "سرویس ذخیره‌سازی",
+          "serviceLocal": "پوشهٔ محلی",
+          "serviceTemplate": "قالب سفارشی",
+          "remoteFolder": "پوشهٔ راه دور",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "مقادیر محرمانه",
+          "secretsDesc": "مقادیری که قالب با {secret0}، {secret1}، … می‌خواند",
+          "template": "قالب ذخیره‌سازی",
+          "templateInvalid": "باید یک شیء JSON باشد",
+          "front": "آدرس Front",
+          "frontDesc": "میزبان یا IP اختیاری که API ذخیره‌ساز از طریق آن وصل می‌شود (domain fronting).",
+          "frontPort": "پورت Front",
+          "segmentBytes": "اندازهٔ قطعه (بایت)",
+          "flushInterval": "فاصلهٔ ارسال (میلی‌ثانیه)",
+          "pollInterval": "فاصلهٔ بررسی (میلی‌ثانیه)",
+          "maxPollInterval": "حداکثر فاصلهٔ بررسی (میلی‌ثانیه)",
+          "eagerWindow": "پنجرهٔ پیش‌ارسال (میلی‌ثانیه)",
+          "holeTimeout": "مهلت قطعهٔ گمشده (میلی‌ثانیه)",
+          "sessionTtl": "TTL نشست (ثانیه)",
+          "concurrency": "هم‌زمانی"
+        }
       },
       "info": {
         "mode": "حالت",

+ 25 - 1
internal/web/translation/id-ID.json

@@ -694,7 +694,31 @@
         "bytesPerSecTip": "Batas kecepatan (byte/detik) yang diterapkan pada lalu lintas fallback setelah ambang batas, agar probe tidak dapat memakai server Anda sebagai bandwidth gratis menuju target. 0 = tanpa batas (menonaktifkan arah ini).",
         "burstBytesPerSec": "Byte per detik burst",
         "burstBytesPerSecTip": "Kelonggaran untuk burst singkat di atas laju tetap (ukuran token-bucket). Jika lebih kecil dari Byte per detik, nilainya dinaikkan agar sama.",
-        "autoFill": "Isi Otomatis"
+        "autoFill": "Isi Otomatis",
+        "xdrive": {
+          "service": "Layanan penyimpanan",
+          "serviceLocal": "Folder lokal",
+          "serviceTemplate": "Template kustom",
+          "remoteFolder": "Folder jarak jauh",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "Rahasia",
+          "secretsDesc": "Nilai yang dibaca template sebagai {secret0}, {secret1}, …",
+          "template": "Template penyimpanan",
+          "templateInvalid": "Harus berupa objek JSON",
+          "front": "Alamat fronting",
+          "frontDesc": "Host atau IP opsional untuk menghubungi API penyimpanan (domain fronting).",
+          "frontPort": "Port fronting",
+          "segmentBytes": "Ukuran segmen (byte)",
+          "flushInterval": "Interval pengiriman (ms)",
+          "pollInterval": "Interval polling (ms)",
+          "maxPollInterval": "Interval polling maks. (ms)",
+          "eagerWindow": "Jendela kirim awal (ms)",
+          "holeTimeout": "Batas waktu segmen hilang (ms)",
+          "sessionTtl": "TTL sesi (s)",
+          "concurrency": "Konkurensi"
+        }
       },
       "info": {
         "mode": "Mode",

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

@@ -694,7 +694,31 @@
           "listen": "インバウンドのリッスンアドレス",
           "custom": "カスタム"
         },
-        "autoFill": "自動入力"
+        "autoFill": "自動入力",
+        "xdrive": {
+          "service": "ストレージサービス",
+          "serviceLocal": "ローカルフォルダ",
+          "serviceTemplate": "カスタムテンプレート",
+          "remoteFolder": "リモートフォルダ",
+          "clientId": "クライアント ID",
+          "clientSecret": "クライアントシークレット",
+          "refreshToken": "リフレッシュトークン",
+          "secrets": "シークレット",
+          "secretsDesc": "テンプレートで {secret0}、{secret1}、… として参照される値",
+          "template": "ストレージテンプレート",
+          "templateInvalid": "JSON オブジェクトである必要があります",
+          "front": "フロンティングアドレス",
+          "frontDesc": "ストレージ API への接続に使う任意のホストまたは IP(ドメインフロンティング)。",
+          "frontPort": "フロンティングポート",
+          "segmentBytes": "セグメントサイズ(バイト)",
+          "flushInterval": "送出間隔(ms)",
+          "pollInterval": "ポーリング間隔(ms)",
+          "maxPollInterval": "最大ポーリング間隔(ms)",
+          "eagerWindow": "先行送信ウィンドウ(ms)",
+          "holeTimeout": "欠落セグメントのタイムアウト(ms)",
+          "sessionTtl": "セッション TTL(秒)",
+          "concurrency": "同時実行数"
+        }
       },
       "info": {
         "mode": "モード",

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

@@ -694,7 +694,31 @@
           "listen": "Endereço de escuta do inbound",
           "custom": "Personalizada"
         },
-        "autoFill": "Preenchimento automático"
+        "autoFill": "Preenchimento automático",
+        "xdrive": {
+          "service": "Serviço de armazenamento",
+          "serviceLocal": "Pasta local",
+          "serviceTemplate": "Template personalizado",
+          "remoteFolder": "Pasta remota",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "Segredos",
+          "secretsDesc": "Valores que o template lê como {secret0}, {secret1}, …",
+          "template": "Template de armazenamento",
+          "templateInvalid": "Deve ser um objeto JSON",
+          "front": "Endereço de fronting",
+          "frontDesc": "Host ou IP opcional usado para conectar à API de armazenamento (domain fronting).",
+          "frontPort": "Porta de fronting",
+          "segmentBytes": "Tamanho do segmento (bytes)",
+          "flushInterval": "Intervalo de envio (ms)",
+          "pollInterval": "Intervalo de polling (ms)",
+          "maxPollInterval": "Intervalo máximo de polling (ms)",
+          "eagerWindow": "Janela antecipada (ms)",
+          "holeTimeout": "Timeout de segmento ausente (ms)",
+          "sessionTtl": "TTL da sessão (s)",
+          "concurrency": "Concorrência"
+        }
       },
       "info": {
         "mode": "Modo",

+ 24 - 0
internal/web/translation/ru-RU.json

@@ -695,6 +695,30 @@
           "node": "Адрес узла",
           "listen": "Адрес прослушивания inbound",
           "custom": "Пользовательская"
+        },
+        "xdrive": {
+          "service": "Сервис хранилища",
+          "serviceLocal": "Локальная папка",
+          "serviceTemplate": "Свой шаблон",
+          "remoteFolder": "Удалённая папка",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "Секреты",
+          "secretsDesc": "Значения, которые шаблон читает как {secret0}, {secret1}, …",
+          "template": "Шаблон хранилища",
+          "templateInvalid": "Должен быть объект JSON",
+          "front": "Адрес фронтинга",
+          "frontDesc": "Необязательный хост или IP для подключения к API хранилища (domain fronting).",
+          "frontPort": "Порт фронтинга",
+          "segmentBytes": "Размер сегмента (байт)",
+          "flushInterval": "Интервал отправки (мс)",
+          "pollInterval": "Интервал опроса (мс)",
+          "maxPollInterval": "Макс. интервал опроса (мс)",
+          "eagerWindow": "Окно упреждения (мс)",
+          "holeTimeout": "Тайм-аут пропущенного сегмента (мс)",
+          "sessionTtl": "TTL сессии (с)",
+          "concurrency": "Параллелизм"
         }
       },
       "info": {

+ 25 - 1
internal/web/translation/tr-TR.json

@@ -694,7 +694,31 @@
         "bytesPerSecTip": "Eşik aşıldıktan sonra fallback trafiğine uygulanan hız sınırı (bayt/sn); böylece sondalar sunucunuzu hedefe ücretsiz bant genişliği olarak kullanamaz. 0 = sınır yok (bu yönü devre dışı bırakır).",
         "burstBytesPerSec": "Saniye Başına Patlama Baytı",
         "burstBytesPerSecTip": "Sabit hızın üzerindeki kısa patlamalar için pay (token-bucket boyutu). Saniye Başına Bayt değerinden düşükse ona eşitlenecek şekilde yükseltilir.",
-        "autoFill": "Otomatik Doldur"
+        "autoFill": "Otomatik Doldur",
+        "xdrive": {
+          "service": "Depolama hizmeti",
+          "serviceLocal": "Yerel klasör",
+          "serviceTemplate": "Özel şablon",
+          "remoteFolder": "Uzak klasör",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "Gizli değerler",
+          "secretsDesc": "Şablonun {secret0}, {secret1}, … olarak okuduğu değerler",
+          "template": "Depolama şablonu",
+          "templateInvalid": "Bir JSON nesnesi olmalı",
+          "front": "Fronting adresi",
+          "frontDesc": "Depolama API'sine bağlanmak için isteğe bağlı ana makine veya IP (domain fronting).",
+          "frontPort": "Fronting portu",
+          "segmentBytes": "Segment boyutu (bayt)",
+          "flushInterval": "Gönderim aralığı (ms)",
+          "pollInterval": "Yoklama aralığı (ms)",
+          "maxPollInterval": "Maks. yoklama aralığı (ms)",
+          "eagerWindow": "Erken gönderim penceresi (ms)",
+          "holeTimeout": "Eksik segment zaman aşımı (ms)",
+          "sessionTtl": "Oturum TTL (s)",
+          "concurrency": "Eşzamanlılık"
+        }
       },
       "info": {
         "mode": "Mod",

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

@@ -694,7 +694,31 @@
           "listen": "Адреса прослуховування inbound",
           "custom": "Користувацька"
         },
-        "autoFill": "Автозаповнення"
+        "autoFill": "Автозаповнення",
+        "xdrive": {
+          "service": "Сервіс сховища",
+          "serviceLocal": "Локальна тека",
+          "serviceTemplate": "Власний шаблон",
+          "remoteFolder": "Віддалена тека",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "Секрети",
+          "secretsDesc": "Значення, які шаблон читає як {secret0}, {secret1}, …",
+          "template": "Шаблон сховища",
+          "templateInvalid": "Має бути об'єкт JSON",
+          "front": "Адреса фронтингу",
+          "frontDesc": "Необов'язковий хост або IP для підключення до API сховища (domain fronting).",
+          "frontPort": "Порт фронтингу",
+          "segmentBytes": "Розмір сегмента (байт)",
+          "flushInterval": "Інтервал надсилання (мс)",
+          "pollInterval": "Інтервал опитування (мс)",
+          "maxPollInterval": "Макс. інтервал опитування (мс)",
+          "eagerWindow": "Вікно випередження (мс)",
+          "holeTimeout": "Тайм-аут пропущеного сегмента (мс)",
+          "sessionTtl": "TTL сесії (с)",
+          "concurrency": "Паралельність"
+        }
       },
       "info": {
         "mode": "Режим",

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

@@ -694,7 +694,31 @@
           "listen": "Địa chỉ listen inbound",
           "custom": "Tùy chỉnh"
         },
-        "autoFill": "Tự động điền"
+        "autoFill": "Tự động điền",
+        "xdrive": {
+          "service": "Dịch vụ lưu trữ",
+          "serviceLocal": "Thư mục cục bộ",
+          "serviceTemplate": "Template tùy chỉnh",
+          "remoteFolder": "Thư mục từ xa",
+          "clientId": "Client ID",
+          "clientSecret": "Client Secret",
+          "refreshToken": "Refresh Token",
+          "secrets": "Giá trị bí mật",
+          "secretsDesc": "Các giá trị template đọc dưới dạng {secret0}, {secret1}, …",
+          "template": "Template lưu trữ",
+          "templateInvalid": "Phải là một đối tượng JSON",
+          "front": "Địa chỉ fronting",
+          "frontDesc": "Host hoặc IP tùy chọn để kết nối tới API lưu trữ (domain fronting).",
+          "frontPort": "Cổng fronting",
+          "segmentBytes": "Kích thước phân đoạn (byte)",
+          "flushInterval": "Khoảng gửi (ms)",
+          "pollInterval": "Khoảng thăm dò (ms)",
+          "maxPollInterval": "Khoảng thăm dò tối đa (ms)",
+          "eagerWindow": "Cửa sổ gửi sớm (ms)",
+          "holeTimeout": "Thời gian chờ phân đoạn thiếu (ms)",
+          "sessionTtl": "TTL phiên (s)",
+          "concurrency": "Số luồng đồng thời"
+        }
       },
       "info": {
         "mode": "Chế độ",

+ 25 - 1
internal/web/translation/zh-CN.json

@@ -694,7 +694,31 @@
           "custom": "自定义"
         },
         "verifyPeerCertByName": "按名称验证对端证书",
-        "autoFill": "自动填充"
+        "autoFill": "自动填充",
+        "xdrive": {
+          "service": "存储服务",
+          "serviceLocal": "本地文件夹",
+          "serviceTemplate": "自定义模板",
+          "remoteFolder": "远程文件夹",
+          "clientId": "客户端 ID",
+          "clientSecret": "客户端密钥",
+          "refreshToken": "刷新令牌",
+          "secrets": "密钥",
+          "secretsDesc": "模板中以 {secret0}、{secret1}… 引用的值",
+          "template": "存储模板",
+          "templateInvalid": "必须是 JSON 对象",
+          "front": "前置地址",
+          "frontDesc": "连接存储 API 时使用的可选主机或 IP(域前置)。",
+          "frontPort": "前置端口",
+          "segmentBytes": "分段大小(字节)",
+          "flushInterval": "发送间隔(毫秒)",
+          "pollInterval": "轮询间隔(毫秒)",
+          "maxPollInterval": "最大轮询间隔(毫秒)",
+          "eagerWindow": "预发窗口(毫秒)",
+          "holeTimeout": "缺失分段超时(毫秒)",
+          "sessionTtl": "会话 TTL(秒)",
+          "concurrency": "并发数"
+        }
       },
       "info": {
         "mode": "模式",

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

@@ -694,7 +694,31 @@
           "listen": "入站監聽地址",
           "custom": "自訂"
         },
-        "autoFill": "自動填入"
+        "autoFill": "自動填入",
+        "xdrive": {
+          "service": "儲存服務",
+          "serviceLocal": "本機資料夾",
+          "serviceTemplate": "自訂範本",
+          "remoteFolder": "遠端資料夾",
+          "clientId": "用戶端 ID",
+          "clientSecret": "用戶端密鑰",
+          "refreshToken": "重新整理權杖",
+          "secrets": "密鑰",
+          "secretsDesc": "範本中以 {secret0}、{secret1}… 引用的值",
+          "template": "儲存範本",
+          "templateInvalid": "必須是 JSON 物件",
+          "front": "前置位址",
+          "frontDesc": "連線儲存 API 時使用的選用主機或 IP(網域前置)。",
+          "frontPort": "前置連接埠",
+          "segmentBytes": "分段大小(位元組)",
+          "flushInterval": "傳送間隔(毫秒)",
+          "pollInterval": "輪詢間隔(毫秒)",
+          "maxPollInterval": "最大輪詢間隔(毫秒)",
+          "eagerWindow": "預發視窗(毫秒)",
+          "holeTimeout": "遺失分段逾時(毫秒)",
+          "sessionTtl": "工作階段 TTL(秒)",
+          "concurrency": "並行數"
+        }
       },
       "info": {
         "mode": "模式",