mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-15 17:45:03 +03:00
feat(outbounds): TLS for XDRIVE outbounds
xray-core 26.10.10 dials an XDRIVE outbound's storage API with the
stream's tlsSettings: their SNI, uTLS fingerprint and ALPN are what the
API sees, which is the point of fronting it. canEnableTls kept XDRIVE out
of the TLS-capable networks, so the security picker never offered TLS and
switching a TLS outbound to XDRIVE silently reset it to none.
canEnableOutboundTls admits a TLS-eligible protocol on xdrive and backs
both the picker and the network-change cascade. Inbounds keep the plain
predicate: their tlsSettings carry server certificates, not a client hello.
Also restores the plain oxfmt / oxlint --fix lint-staged commands, reverting
da6872a3: that hook change was not wanted.
This commit is contained in:
@@ -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
|
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
|
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
|
## FinalMask — late-layer obfuscation
|
||||||
|
|
||||||
|
|||||||
@@ -136,7 +136,9 @@ XDRIVE جریان را بهصورت فایلهایی در پوشهای
|
|||||||
|
|
||||||
**آدرس/پورت Front** اختیاری (کلیدهای `address`/`port` در سطح stream) یک domain front
|
**آدرس/پورت Front** اختیاری (کلیدهای `address`/`port` در سطح stream) یک domain front
|
||||||
است که API ذخیرهساز از طریق آن وصل میشود؛ نام TLS همچنان میزبان API است. XDRIVE لایهٔ
|
است که API ذخیرهساز از طریق آن وصل میشود؛ نام TLS همچنان میزبان API است. XDRIVE لایهٔ
|
||||||
TLS/REALITY جداگانهای ندارد — ترافیکش همان HTTPS خود API ذخیرهساز است.
|
TLS/REALITY جداگانهای ندارد — ترافیکش همان HTTPS خود API ذخیرهساز است. در اوتباند،
|
||||||
|
امنیت **TLS** همین اتصال HTTPS را شکل میدهد: API ذخیرهساز همان SNI، اثرانگشت uTLS و
|
||||||
|
ALPN آن را میبیند.
|
||||||
|
|
||||||
## FinalMask — مبهمسازی لایه پایانی
|
## FinalMask — مبهمسازی لایه پایانی
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,8 @@ XDRIVE передаёт поток файлами в папке, доступн
|
|||||||
Необязательные **адрес/порт фронтинга** (`address`/`port` на уровне stream) — домен-фронт,
|
Необязательные **адрес/порт фронтинга** (`address`/`port` на уровне stream) — домен-фронт,
|
||||||
через который идёт подключение к API хранилища; в TLS по-прежнему указывается хост API.
|
через который идёт подключение к API хранилища; в TLS по-прежнему указывается хост API.
|
||||||
У XDRIVE нет собственного уровня TLS/REALITY — его трафик это HTTPS самого API хранилища.
|
У XDRIVE нет собственного уровня TLS/REALITY — его трафик это HTTPS самого API хранилища.
|
||||||
|
На исходящем безопасность **TLS** задаёт это HTTPS-подключение: API хранилища видит его
|
||||||
|
SNI, отпечаток uTLS и ALPN.
|
||||||
|
|
||||||
## FinalMask — обфускация на позднем уровне
|
## FinalMask — обфускация на позднем уровне
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,8 @@ XDRIVE 把数据流作为文件存放在双方都能访问的文件夹中:服
|
|||||||
| 调优参数 | 内核默认值 | `segmentBytes`(512 KiB)、`flushIntervalMs`(20)、`pollIntervalMs`/`maxPollIntervalMs`(50/500)、`eagerWindowMs`(2000)、`holeTimeoutMs`(30000)、`sessionTtlSeconds`(300)、`concurrency`(8)。 |
|
| 调优参数 | 内核默认值 | `segmentBytes`(512 KiB)、`flushIntervalMs`(20)、`pollIntervalMs`/`maxPollIntervalMs`(50/500)、`eagerWindowMs`(2000)、`holeTimeoutMs`(30000)、`sessionTtlSeconds`(300)、`concurrency`(8)。 |
|
||||||
|
|
||||||
可选的**前置地址/端口**(stream 级的 `address`/`port`)是连接存储 API 时使用的域前置;TLS 中仍使用 API 主机名。
|
可选的**前置地址/端口**(stream 级的 `address`/`port`)是连接存储 API 时使用的域前置;TLS 中仍使用 API 主机名。
|
||||||
XDRIVE 没有自己的 TLS/REALITY 层——其流量就是存储 API 自身的 HTTPS。
|
XDRIVE 没有自己的 TLS/REALITY 层——其流量就是存储 API 自身的 HTTPS。在出站上,**TLS**
|
||||||
|
安全设置决定这条 HTTPS 连接:存储 API 看到的就是其中的 SNI、uTLS 指纹和 ALPN。
|
||||||
|
|
||||||
## FinalMask — 末层混淆
|
## FinalMask — 末层混淆
|
||||||
|
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"src/**/*.{ts,tsx}": [
|
"src/**/*.{ts,tsx}": [
|
||||||
"oxfmt --no-error-on-unmatched-pattern",
|
"oxfmt",
|
||||||
"oxlint --fix --no-error-on-unmatched-pattern"
|
"oxlint --fix"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ export function canEnableTls(values: CapabilityProtocolSlice): boolean {
|
|||||||
return TLS_NETWORKS.includes(values.streamSettings?.network ?? '');
|
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 {
|
export function canEnableReality(values: CapabilityProtocolSlice): boolean {
|
||||||
if (!REALITY_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
|
if (!REALITY_ELIGIBLE_PROTOCOLS.includes(values.protocol)) return false;
|
||||||
return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');
|
return REALITY_NETWORKS.includes(values.streamSettings?.network ?? '');
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { OutboundFormBaseSchema, type OutboundFormValues } from '@/schemas/forms
|
|||||||
import {
|
import {
|
||||||
canEnableReality,
|
canEnableReality,
|
||||||
canEnableStream,
|
canEnableStream,
|
||||||
canEnableTls,
|
canEnableOutboundTls,
|
||||||
canEnableTlsFlow,
|
canEnableTlsFlow,
|
||||||
} from '@/lib/xray/protocol-capabilities';
|
} from '@/lib/xray/protocol-capabilities';
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ export default function OutboundFormModal({
|
|||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
const streamAllowed = canEnableStream({ protocol });
|
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 realityAllowed = canEnableReality({ protocol, streamSettings: { network, security } });
|
||||||
const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
|
const tlsFlowAllowed = canEnableTlsFlow({ protocol, streamSettings: { network, security } });
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { rawOutboundToFormValues } from '@/lib/xray/outbound-form-adapter';
|
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 type { OutboundFormValues } from '@/schemas/forms/outbound-form';
|
||||||
import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
|
import { XDriveStreamSettingsSchema } from '@/schemas/protocols/stream/xdrive';
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ export function applyNetworkChange(
|
|||||||
if (next === 'masque') return masqueStreamSlice();
|
if (next === 'masque') return masqueStreamSlice();
|
||||||
const stream = prevStream ?? {};
|
const stream = prevStream ?? {};
|
||||||
const currentSecurity = (stream.security as string) ?? 'none';
|
const currentSecurity = (stream.security as string) ?? 'none';
|
||||||
const stillTls = canEnableTls({
|
const stillTls = canEnableOutboundTls({
|
||||||
protocol,
|
protocol,
|
||||||
streamSettings: { network: next, security: currentSecurity },
|
streamSettings: { network: next, security: currentSecurity },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -379,6 +379,80 @@ describe('OutboundFormModal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// xray-core 26.10.10 dials the XDRIVE storage API with the outbound's tlsSettings (SNI,
|
||||||
|
// uTLS fingerprint), so switching a TLS outbound to XDRIVE must keep them.
|
||||||
|
describe('OutboundFormModal XDRIVE TLS', () => {
|
||||||
|
it('keeps TLS when a TLS outbound moves to XDRIVE', async () => {
|
||||||
|
const onConfirm = vi.fn();
|
||||||
|
const queryClient = makeTestQueryClient();
|
||||||
|
const outbound = {
|
||||||
|
protocol: 'vless',
|
||||||
|
tag: 'drive-out',
|
||||||
|
settings: {
|
||||||
|
vnext: [
|
||||||
|
{
|
||||||
|
address: 'www.google.com',
|
||||||
|
port: 443,
|
||||||
|
users: [{ id: 'c9f0c2d0-0000-4000-8000-000000000000', encryption: 'none' }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
streamSettings: {
|
||||||
|
network: 'tcp',
|
||||||
|
security: 'tls',
|
||||||
|
tlsSettings: { serverName: 'www.google.com', fingerprint: 'chrome' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const tree = (open: boolean) => (
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<ThemeProvider>
|
||||||
|
<OutboundFormModal
|
||||||
|
open={open}
|
||||||
|
outbound={outbound}
|
||||||
|
existingTags={[]}
|
||||||
|
onClose={() => {}}
|
||||||
|
onConfirm={onConfirm}
|
||||||
|
/>
|
||||||
|
</ThemeProvider>
|
||||||
|
</QueryClientProvider>
|
||||||
|
);
|
||||||
|
const { rerender } = render(tree(false));
|
||||||
|
rerender(tree(true));
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
chooseOptionByLabel('Transmission', 'XDRIVE');
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
});
|
||||||
|
expect((screen.getByRole('radio', { name: 'TLS' }) as HTMLInputElement).checked).toBe(true);
|
||||||
|
for (const [label, value] of [
|
||||||
|
['Remote folder', 'xray-tunnel'],
|
||||||
|
['Client ID', 'id.apps.googleusercontent.com'],
|
||||||
|
['Client secret', 'client-secret'],
|
||||||
|
['Refresh token', 'refresh-token'],
|
||||||
|
]) {
|
||||||
|
fireEvent.change(screen.getByLabelText(label), { target: { value } });
|
||||||
|
}
|
||||||
|
const ok = document.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement;
|
||||||
|
await act(async () => {
|
||||||
|
fireEvent.click(ok);
|
||||||
|
});
|
||||||
|
await act(async () => {
|
||||||
|
await new Promise((r) => setTimeout(r, 0));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(onConfirm).toHaveBeenCalledTimes(1);
|
||||||
|
const payload = onConfirm.mock.calls[0][0] as { streamSettings: Record<string, unknown> };
|
||||||
|
expect(payload.streamSettings).toMatchObject({
|
||||||
|
network: 'xdrive',
|
||||||
|
security: 'tls',
|
||||||
|
tlsSettings: { serverName: 'www.google.com', fingerprint: 'chrome' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('OutboundFormModal MASQUE', () => {
|
describe('OutboundFormModal MASQUE', () => {
|
||||||
// xray-core refuses user/pass next to warp, and builds the WARP client certificate
|
// 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.
|
// from privateKey; a save that kept stale credentials or dropped a key never connects.
|
||||||
|
|||||||
Reference in New Issue
Block a user