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.
This commit is contained in:
MHSanaei
2026-10-11 08:21:08 +02:00
parent 73a028f52d
commit ca5983595b
34 changed files with 848 additions and 19 deletions
+6 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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": "模式",