diff --git a/docs/content/docs/en/config/transports.mdx b/docs/content/docs/en/config/transports.mdx
index dbf45a460..fdcd21293 100644
--- a/docs/content/docs/en/config/transports.mdx
+++ b/docs/content/docs/en/config/transports.mdx
@@ -135,7 +135,9 @@ client; XDRIVE inbounds get no share link or Clash entry.
The optional **Front address/port** (the stream-level `address`/`port`) is a domain
front the storage API is dialed through; TLS still names the API host. XDRIVE has
-no TLS/REALITY layer of its own — its traffic is the storage API's HTTPS.
+no TLS/REALITY layer of its own — its traffic is the storage API's HTTPS. On an
+outbound, **TLS** security shapes that HTTPS connection: its SNI, uTLS fingerprint
+and ALPN are what the storage API sees.
## FinalMask — late-layer obfuscation
diff --git a/docs/content/docs/fa/config/transports.mdx b/docs/content/docs/fa/config/transports.mdx
index 0e02eb1b8..79cac589d 100644
--- a/docs/content/docs/fa/config/transports.mdx
+++ b/docs/content/docs/fa/config/transports.mdx
@@ -136,7 +136,9 @@ XDRIVE جریان را بهصورت فایلهایی در پوشهای
**آدرس/پورت Front** اختیاری (کلیدهای `address`/`port` در سطح stream) یک domain front
است که API ذخیرهساز از طریق آن وصل میشود؛ نام TLS همچنان میزبان API است. XDRIVE لایهٔ
-TLS/REALITY جداگانهای ندارد — ترافیکش همان HTTPS خود API ذخیرهساز است.
+TLS/REALITY جداگانهای ندارد — ترافیکش همان HTTPS خود API ذخیرهساز است. در اوتباند،
+امنیت **TLS** همین اتصال HTTPS را شکل میدهد: API ذخیرهساز همان SNI، اثرانگشت uTLS و
+ALPN آن را میبیند.
## FinalMask — مبهمسازی لایه پایانی
diff --git a/docs/content/docs/ru/config/transports.mdx b/docs/content/docs/ru/config/transports.mdx
index c7b5ad51f..7b517138b 100644
--- a/docs/content/docs/ru/config/transports.mdx
+++ b/docs/content/docs/ru/config/transports.mdx
@@ -139,6 +139,8 @@ XDRIVE передаёт поток файлами в папке, доступн
Необязательные **адрес/порт фронтинга** (`address`/`port` на уровне stream) — домен-фронт,
через который идёт подключение к API хранилища; в TLS по-прежнему указывается хост API.
У XDRIVE нет собственного уровня TLS/REALITY — его трафик это HTTPS самого API хранилища.
+На исходящем безопасность **TLS** задаёт это HTTPS-подключение: API хранилища видит его
+SNI, отпечаток uTLS и ALPN.
## FinalMask — обфускация на позднем уровне
diff --git a/docs/content/docs/zh/config/transports.mdx b/docs/content/docs/zh/config/transports.mdx
index 3e04286ec..953ffe559 100644
--- a/docs/content/docs/zh/config/transports.mdx
+++ b/docs/content/docs/zh/config/transports.mdx
@@ -128,7 +128,8 @@ XDRIVE 把数据流作为文件存放在双方都能访问的文件夹中:服
| 调优参数 | 内核默认值 | `segmentBytes`(512 KiB)、`flushIntervalMs`(20)、`pollIntervalMs`/`maxPollIntervalMs`(50/500)、`eagerWindowMs`(2000)、`holeTimeoutMs`(30000)、`sessionTtlSeconds`(300)、`concurrency`(8)。 |
可选的**前置地址/端口**(stream 级的 `address`/`port`)是连接存储 API 时使用的域前置;TLS 中仍使用 API 主机名。
-XDRIVE 没有自己的 TLS/REALITY 层——其流量就是存储 API 自身的 HTTPS。
+XDRIVE 没有自己的 TLS/REALITY 层——其流量就是存储 API 自身的 HTTPS。在出站上,**TLS**
+安全设置决定这条 HTTPS 连接:存储 API 看到的就是其中的 SNI、uTLS 指纹和 ALPN。
## FinalMask — 末层混淆
diff --git a/frontend/package.json b/frontend/package.json
index 861bc6f2e..abb3c5c1f 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -29,8 +29,8 @@
},
"lint-staged": {
"src/**/*.{ts,tsx}": [
- "oxfmt --no-error-on-unmatched-pattern",
- "oxlint --fix --no-error-on-unmatched-pattern"
+ "oxfmt",
+ "oxlint --fix"
]
},
"dependencies": {
diff --git a/frontend/src/lib/xray/protocol-capabilities.ts b/frontend/src/lib/xray/protocol-capabilities.ts
index be6d76ce6..68d086010 100644
--- a/frontend/src/lib/xray/protocol-capabilities.ts
+++ b/frontend/src/lib/xray/protocol-capabilities.ts
@@ -43,6 +43,15 @@ export function canEnableTls(values: CapabilityProtocolSlice): boolean {
return TLS_NETWORKS.includes(values.streamSettings?.network ?? '');
}
+// An XDRIVE outbound's tlsSettings shape only its HTTPS calls to the storage API (SNI, uTLS
+// fingerprint); xray-core 26.10.10 reads them on the dial side, never on a listener.
+export function canEnableOutboundTls(values: CapabilityProtocolSlice): boolean {
+ if (canEnableTls(values)) return true;
+ return (
+ TLS_ELIGIBLE_PROTOCOLS.includes(values.protocol) && values.streamSettings?.network === 'xdrive'
+ );
+}
+
export function canEnableReality(values: CapabilityProtocolSlice): boolean {
if (!REALITY_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');
diff --git a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx b/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx
index d02f75c05..0ab97ca0e 100644
--- a/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx
+++ b/frontend/src/pages/xray/outbounds/OutboundFormModal.tsx
@@ -14,7 +14,7 @@ import { OutboundFormBaseSchema, type OutboundFormValues } from '@/schemas/forms
import {
canEnableReality,
canEnableStream,
- canEnableTls,
+ canEnableOutboundTls,
canEnableTlsFlow,
} from '@/lib/xray/protocol-capabilities';
@@ -111,7 +111,7 @@ export default function OutboundFormModal({
| undefined;
const streamAllowed = canEnableStream({ protocol });
- const tlsAllowed = canEnableTls({ protocol, streamSettings: { network, security } });
+ const tlsAllowed = canEnableOutboundTls({ protocol, streamSettings: { network, security } });
const realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
diff --git a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts
index 5f3b1f2fa..396c5b415 100644
--- a/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts
+++ b/frontend/src/pages/xray/outbounds/outbound-form-helpers.ts
@@ -1,5 +1,5 @@
import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
-import { canEnableReality, canEnableTls } from '@/lib/xray/protocol-capabilities';
+import { canEnableOutboundTls, canEnableReality } from '@/lib/xray/protocol-capabilities';
import type { OutboundFormValues } from '@/schemas/forms/outbound-form';
import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
@@ -112,7 +112,7 @@ export function applyNetworkChange(
if (next === 'masque') return masqueStreamSlice();
const stream = prevStream ?? {};
const currentSecurity = (stream.security as string) ?? 'none';
- const stillTls = canEnableTls({
+ const stillTls = canEnableOutboundTls({
protocol,
streamSettings: { network: next, security: currentSecurity },
});
diff --git a/frontend/src/test/outbound-form-modal.test.tsx b/frontend/src/test/outbound-form-modal.test.tsx
index a2c62427b..249d7f3fc 100644
--- a/frontend/src/test/outbound-form-modal.test.tsx
+++ b/frontend/src/test/outbound-form-modal.test.tsx
@@ -379,6 +379,80 @@ describe('OutboundFormModal', () => {
});
});
+// xray-core 26.10.10 dials the XDRIVE storage API with the outbound's tlsSettings (SNI,
+// uTLS fingerprint), so switching a TLS outbound to XDRIVE must keep them.
+describe('OutboundFormModal XDRIVE TLS', () => {
+ it('keeps TLS when a TLS outbound moves to XDRIVE', async () => {
+ const onConfirm = vi.fn();
+ const queryClient = makeTestQueryClient();
+ const outbound = {
+ protocol: 'vless',
+ tag: 'drive-out',
+ settings: {
+ vnext: [
+ {
+ address: 'www.google.com',
+ port: 443,
+ users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
+ },
+ ],
+ },
+ streamSettings: {
+ network: 'tcp',
+ security: 'tls',
+ tlsSettings: { serverName: 'www.google.com', fingerprint: 'chrome' },
+ },
+ };
+ const tree = (open: boolean) => (
+
+
+ {}}
+ 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));
+ });
+ expect((screen.getByRole('radio', { name: 'TLS' }) as HTMLInputElement).checked).toBe(true);
+ for (const [label, value] of [
+ ['Remote folder', 'xray-tunnel'],
+ ['Client ID', 'id.apps.googleusercontent.com'],
+ ['Client secret', 'client-secret'],
+ ['Refresh token', 'refresh-token'],
+ ]) {
+ fireEvent.change(screen.getByLabelText(label), { target: { value } });
+ }
+ const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
+ await act(async () => {
+ fireEvent.click(ok);
+ });
+ await act(async () => {
+ await new Promise((r) => setTimeout(r, 0));
+ });
+
+ expect(onConfirm).toHaveBeenCalledTimes(1);
+ const payload = onConfirm.mock.calls[0][0] as { streamSettings: Record };
+ expect(payload.streamSettings).toMatchObject({
+ network: 'xdrive',
+ security: 'tls',
+ tlsSettings: { serverName: 'www.google.com', fingerprint: 'chrome' },
+ });
+ });
+});
+
describe('OutboundFormModal MASQUE', () => {
// xray-core refuses user/pass next to warp, and builds the WARP client certificate
// from privateKey; a save that kept stale credentials or dropped a key never connects.