diff --git a/docs/content/docs/en/config/transports.mdx b/docs/content/docs/en/config/transports.mdx index 77ed33cf6..f5baf7c91 100644 --- a/docs/content/docs/en/config/transports.mdx +++ b/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). | **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 diff --git a/docs/content/docs/fa/config/transports.mdx b/docs/content/docs/fa/config/transports.mdx index c9657a959..2690a269d 100644 --- a/docs/content/docs/fa/config/transports.mdx +++ b/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) تونل می‌کند. | 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** ترافیک را **پس از** لایه‌های انتقال و امنیت می‌پیچد، بنابراین می‌تواند diff --git a/docs/content/docs/ru/config/transports.mdx b/docs/content/docs/ru/config/transports.mdx index 3d67cc9eb..ccdbe2e11 100644 --- a/docs/content/docs/ru/config/transports.mdx +++ b/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 хранилища). | 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** оборачивает трафик **после** уровней транспорта и безопасности, diff --git a/docs/content/docs/zh/config/transports.mdx b/docs/content/docs/zh/config/transports.mdx index a6887cb87..e87cf5dec 100644 --- a/docs/content/docs/zh/config/transports.mdx +++ b/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)中的文件传输数据流。 | **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 的 diff --git a/frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx b/frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx index 6efffd7f6..c9a5137da 100644 --- a/frontend/src/lib/xray/forms/transport/FinalMaskForm.tsx +++ b/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 diff --git a/frontend/src/lib/xray/forms/transport/XDriveForm.tsx b/frontend/src/lib/xray/forms/transport/XDriveForm.tsx new file mode 100644 index 000000000..3b1402f86 --- /dev/null +++ b/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> = { + 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; + onChange?: (next: Record | undefined) => void; +}) { + const { t } = useTranslation(); + const [text, setText] = useState(() => (value ? JSON.stringify(value, null, 2) : '')); + const [invalid, setInvalid] = useState(false); + return ( + <> + { + 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); + } catch { + setInvalid(true); + } + }} + /> + {invalid && ( + + {t('pages.inbounds.form.xdrive.templateInvalid')} + + )} + + ); +} + +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 ( + <> + + + + {service === 'Google Drive' && + GOOGLE_DRIVE_SECRETS.map((key, index) => ( + + {key === 'clientId' ? : } + + ))} + {service === 'template' && ( + <> + + + + + + + + )} + {TUNING_FIELDS.map(([key, label, placeholder]) => ( + + + + ))} + + ); +} diff --git a/frontend/src/lib/xray/inbound-link.ts b/frontend/src/lib/xray/inbound-link.ts index 4fd457f44..25a6365c2 100644 --- a/frontend/src/lib/xray/inbound-link.ts +++ b/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) { diff --git a/frontend/src/lib/xray/stream-defaults.ts b/frontend/src/lib/xray/stream-defaults.ts index b042117a9..9f1dc1da2 100644 --- a/frontend/src/lib/xray/stream-defaults.ts +++ b/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; } diff --git a/frontend/src/pages/inbounds/form/InboundFormModal.tsx b/frontend/src/pages/inbounds/form/InboundFormModal.tsx index e5c8f7b55..ae0fa59cb 100644 --- a/frontend/src/pages/inbounds/form/InboundFormModal.tsx +++ b/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 { 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) ?? {}; const cleaned: Record = { ...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 | 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' }, ]} /> @@ -941,6 +956,8 @@ export default function InboundFormModal({ {network === 'httpupgrade' && } {network === 'kcp' && } + + {network === 'xdrive' && } )} diff --git a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx b/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx index 9ad8e2fd4..c5bea48b9 100644 --- a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx +++ b/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' && } {network === 'hysteria' && } + + {network === 'xdrive' && } )} diff --git a/frontend/src/pages/xray/outbounds/outbound-form-constants.ts b/frontend/src/pages/xray/outbounds/outbound-form-constants.ts index be3a85cbe..50ba9eaeb 100644 --- a/frontend/src/pages/xray/outbounds/outbound-form-constants.ts +++ b/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 diff --git a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts index aa25dff02..a4a145e58 100644 --- a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts +++ b/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 { udpIdleTimeout: 60, }, }; + case 'xdrive': + return { network: 'xdrive', xdriveSettings: XDriveStreamSettingsSchema.parse({}) }; default: return { network: 'tcp', tcpSettings: { header: { type: 'none' } } }; } diff --git a/frontend/src/schemas/protocols/stream/index.ts b/frontend/src/schemas/protocols/stream/index.ts index dd9833775..5a6adeb85 100644 --- a/frontend/src/schemas/protocols/stream/index.ts +++ b/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; @@ -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 diff --git a/frontend/src/schemas/protocols/stream/xdrive.ts b/frontend/src/schemas/protocols/stream/xdrive.ts new file mode 100644 index 000000000..f3387b5f6 --- /dev/null +++ b/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; + +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; diff --git a/frontend/src/test/__snapshots__/stream.test.ts.snap b/frontend/src/test/__snapshots__/stream.test.ts.snap index 6b9adcfa2..17bfef5b8 100644 --- a/frontend/src/test/__snapshots__/stream.test.ts.snap +++ b/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", diff --git a/frontend/src/test/golden/fixtures/stream/xdrive-google-drive.json b/frontend/src/test/golden/fixtures/stream/xdrive-google-drive.json new file mode 100644 index 000000000..966ce4334 --- /dev/null +++ b/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 + } +} diff --git a/frontend/src/test/inbound-form-modal.test.tsx b/frontend/src/test/inbound-form-modal.test.tsx index 93351dd26..e5a6cf593 100644 --- a/frontend/src/test/inbound-form-modal.test.tsx +++ b/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; + 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 () => { diff --git a/frontend/src/test/inbound-link.test.ts b/frontend/src/test/inbound-link.test.ts index 084765cb6..745bdf7a8 100644 --- a/frontend/src/test/inbound-link.test.ts +++ b/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', () => { diff --git a/frontend/src/test/outbound-form-modal.test.tsx b/frontend/src/test/outbound-form-modal.test.tsx index 4f12ef4ba..565aa3d1a 100644 --- a/frontend/src/test/outbound-form-modal.test.tsx +++ b/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 | 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) => ( + + + {}} + onConfirm={onConfirm} + /> + + + ); + 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 }; + expect(payload.streamSettings).toMatchObject({ + network: 'xdrive', + xdriveSettings: { + service: 'Google Drive', + remoteFolder: 'xray-tunnel', + secrets: ['id.apps.googleusercontent.com', 'client-secret', 'refresh-token'], + }, + }); + }); }); diff --git a/internal/sub/service.go b/internal/sub/service.go index ec5a51818..2867ff2be 100644 --- a/internal/sub/service.go +++ b/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) diff --git a/internal/sub/xdrive_test.go b/internal/sub/xdrive_test.go new file mode 100644 index 000000000..ca7de7cfe --- /dev/null +++ b/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) + } +} diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index 123422e6a..920f8f012 100644 --- a/internal/web/translation/ar-EG.json +++ b/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": "الوضع", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 234d57df0..95b723f4f 100644 --- a/internal/web/translation/en-US.json +++ b/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": { diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index d2a44b80a..e2cb1a73f 100644 --- a/internal/web/translation/es-ES.json +++ b/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", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 6a021977a..802815ea2 100644 --- a/internal/web/translation/fa-IR.json +++ b/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": "حالت", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index c02b70877..81affa685 100644 --- a/internal/web/translation/id-ID.json +++ b/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", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index 35318ddf3..6b6354fdc 100644 --- a/internal/web/translation/ja-JP.json +++ b/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": "モード", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index d37328b0b..4c5dee2b3 100644 --- a/internal/web/translation/pt-BR.json +++ b/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", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 37eaef461..ba859b0c5 100644 --- a/internal/web/translation/ru-RU.json +++ b/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": { diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index da96b7962..f80bca6e3 100644 --- a/internal/web/translation/tr-TR.json +++ b/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", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index 0c91f0eba..b1547258d 100644 --- a/internal/web/translation/uk-UA.json +++ b/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": "Режим", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 4edbb142e..71ad6af03 100644 --- a/internal/web/translation/vi-VN.json +++ b/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ế độ", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 6ab6b005f..6a7e54512 100644 --- a/internal/web/translation/zh-CN.json +++ b/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": "模式", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 6c7fdccd3..1100cb596 100644 --- a/internal/web/translation/zh-TW.json +++ b/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": "模式",