fix(xray): keep XDRIVE storage and Vision flow within what xray-core runs

xray-core opens an XDRIVE stream's storage only when the inbound starts
listening, so a Google Drive inbound saved with the form's defaults (no
folder, no secrets), a local one without a folder, or a template one
whose template xray cannot use stopped xray and every other inbound with
it. validateXDriveStorage applies the core's storage rules at save, on
inbound add/update and on the xray template's inbounds and outbounds:
the service must be local, Google Drive or template; local and Google
Drive need a remoteFolder; Google Drive needs its three non-empty
secrets; a template must pass newTemplateStorage's checks (put, get,
list and delete urls, and a list namesRegex with one capture group).

A VLESS client kept its xtls-rprx-vision flow when its inbound moved to
a transport that cannot carry it, and GetXrayConfig only honoured
DisableFlow, so the server demanded Vision there and refused every
client. GetXrayConfig and the outbound form now apply the Vision gate,
and that gate now follows the core in the backend, the subscription
service and the frontend: TLS/REALITY on raw TCP, or VLESS encryption on
any transport (xray runs Vision inside encryption.CommonConn whatever
the transport, not only on XHTTP). The outbound modal passes the VLESS
encryption into the gate and gates Mux on the effective flow.
This commit is contained in:
MHSanaei
2026-10-12 00:49:11 +02:00
parent 8fe7be943f
commit c5e1e54c75
22 changed files with 439 additions and 72 deletions
+2 -2
View File
@@ -204,7 +204,7 @@ The `xtls-rprx-vision` flow is fast and DPI-resistant. It's available for
- the transport is raw **TCP** with **TLS** or **REALITY** security (classic - the transport is raw **TCP** with **TLS** or **REALITY** security (classic
XTLS-Vision), or XTLS-Vision), or
- the transport is **XHTTP** with VLESS encryption enabled (see below). - VLESS encryption is enabled, on any transport (see below).
Set the flow on the VLESS **client**, not the inbound. With classic Vision on Set the flow on the VLESS **client**, not the inbound. With classic Vision on
TCP, the panel can also offer a **Vision seed** once a client uses the flow. TCP, the panel can also offer a **Vision seed** once a client uses the flow.
@@ -213,7 +213,7 @@ TCP, the panel can also offer a **Vision seed** once a client uses the flow.
VLESS supports post-quantum **encryption** (ML-KEM / `mlkem768x25519`), stored in VLESS supports post-quantum **encryption** (ML-KEM / `mlkem768x25519`), stored in
the inbound's `decryption` (server) and clients' `encryption` (for link the inbound's `decryption` (server) and clients' `encryption` (for link
generation). When enabled, it unlocks the Vision flow over XHTTP. Generate the generation). When enabled, it unlocks the Vision flow on any transport. Generate the
keys from the panel's VLESS settings. keys from the panel's VLESS settings.
## Shadowsocks ciphers ## Shadowsocks ciphers
+2 -2
View File
@@ -205,7 +205,7 @@ mKCP و Hysteria لایه TLS/REALITY جداگانه‌ای نمی‌گیرند
- انتقال، **TCP** خام با امنیت **TLS** یا **REALITY** باشد (XTLS-Vision - انتقال، **TCP** خام با امنیت **TLS** یا **REALITY** باشد (XTLS-Vision
کلاسیک)، یا کلاسیک)، یا
- انتقال، **XHTTP** با رمزنگاری VLESS فعال باشد (به ادامه مراجعه کنید). - رمزنگاری VLESS فعال باشد، روی هر انتقالی (به ادامه مراجعه کنید).
جریان را روی **کلاینت** VLESS تنظیم کنید، نه روی inbound. با Vision کلاسیک روی جریان را روی **کلاینت** VLESS تنظیم کنید، نه روی inbound. با Vision کلاسیک روی
TCP، پنل می‌تواند پس از آنکه یک کلاینت از جریان استفاده کرد، یک **Vision seed** نیز TCP، پنل می‌تواند پس از آنکه یک کلاینت از جریان استفاده کرد، یک **Vision seed** نیز
@@ -215,7 +215,7 @@ TCP، پنل می‌تواند پس از آنکه یک کلاینت از جری
‏VLESS از **رمزنگاری** پساکوانتومی (ML-KEM / `mlkem768x25519`) پشتیبانی می‌کند که در ‏VLESS از **رمزنگاری** پساکوانتومی (ML-KEM / `mlkem768x25519`) پشتیبانی می‌کند که در
‏`decryption` مربوط به inbound (سمت سرور) و `encryption` کلاینت‌ها (برای تولید ‏`decryption` مربوط به inbound (سمت سرور) و `encryption` کلاینت‌ها (برای تولید
لینک) ذخیره می‌شود. وقتی فعال باشد، جریان Vision را روی XHTTP باز می‌کند. کلیدها لینک) ذخیره می‌شود. وقتی فعال باشد، جریان Vision را روی هر انتقالی باز می‌کند. کلیدها
را از تنظیمات VLESS در پنل تولید کنید. را از تنظیمات VLESS در پنل تولید کنید.
## رمزهای Shadowsocks ## رمزهای Shadowsocks
+2 -2
View File
@@ -207,7 +207,7 @@ mKCP и Hysteria не используют отдельный уровень TLS
- транспорт — это «сырой» **TCP** с безопасностью **TLS** или **REALITY** - транспорт — это «сырой» **TCP** с безопасностью **TLS** или **REALITY**
(классический XTLS-Vision), либо (классический XTLS-Vision), либо
- транспорт — это **XHTTP** с включённым шифрованием VLESS (см. ниже). - включено шифрование VLESS, на любом транспорте (см. ниже).
Поток задаётся на **клиенте** VLESS, а не на inbound. При классическом Vision Поток задаётся на **клиенте** VLESS, а не на inbound. При классическом Vision
поверх TCP панель также может предложить **Vision seed** после того, как клиент поверх TCP панель также может предложить **Vision seed** после того, как клиент
@@ -218,7 +218,7 @@ mKCP и Hysteria не используют отдельный уровень TLS
VLESS поддерживает постквантовое **шифрование** (ML-KEM / `mlkem768x25519`), VLESS поддерживает постквантовое **шифрование** (ML-KEM / `mlkem768x25519`),
которое хранится в поле `decryption` у inbound (сервер) и в поле `encryption` у которое хранится в поле `decryption` у inbound (сервер) и в поле `encryption` у
клиентов (для генерации ссылок). Когда оно включено, становится доступен поток клиентов (для генерации ссылок). Когда оно включено, становится доступен поток
Vision поверх XHTTP. Сгенерируйте ключи в настройках VLESS на панели. Vision на любом транспорте. Сгенерируйте ключи в настройках VLESS на панели.
## Шифры Shadowsocks ## Шифры Shadowsocks
+2 -2
View File
@@ -185,7 +185,7 @@ mKCP 和 Hysteria 不使用单独的 TLS/REALITY 层——mKCP 以明文运行
`xtls-rprx-vision` 流控既快速又能抵抗 DPI。在以下任一情况下,它都可用于 **VLESS**: `xtls-rprx-vision` 流控既快速又能抵抗 DPI。在以下任一情况下,它都可用于 **VLESS**:
- 传输方式为裸 **TCP**,安全层为 **TLS** 或 **REALITY**(经典的 XTLS-Vision),或者 - 传输方式为裸 **TCP**,安全层为 **TLS** 或 **REALITY**(经典的 XTLS-Vision),或者
- 传输方式为 **XHTTP** 且启用了 VLESS 加密(见下文)。 - 启用了 VLESS 加密,任意传输方式均可(见下文)。
请在 VLESS **客户端**上设置流控,而不是在入站上。在 TCP 上使用经典 Vision 时,一旦某个 请在 VLESS **客户端**上设置流控,而不是在入站上。在 TCP 上使用经典 Vision 时,一旦某个
客户端使用该流控,面板还可以提供一个 **Vision seed**。 客户端使用该流控,面板还可以提供一个 **Vision seed**。
@@ -193,7 +193,7 @@ mKCP 和 Hysteria 不使用单独的 TLS/REALITY 层——mKCP 以明文运行
## VLESS 加密(ML-KEM) ## VLESS 加密(ML-KEM)
VLESS 支持后量子**加密**(ML-KEM / `mlkem768x25519`),它存储在入站的 `decryption` VLESS 支持后量子**加密**(ML-KEM / `mlkem768x25519`),它存储在入站的 `decryption`
(服务器)以及客户端的 `encryption`(用于生成链接)中。启用后,它会解锁在 XHTTP (服务器)以及客户端的 `encryption`(用于生成链接)中。启用后,它会解锁任意传输方式
之上的 Vision 流控。请从面板的 VLESS 设置中生成密钥。 之上的 Vision 流控。请从面板的 VLESS 设置中生成密钥。
## Shadowsocks 加密方式 ## Shadowsocks 加密方式
+2 -4
View File
@@ -503,10 +503,8 @@ export function genVlessLink(input: GenVlessLinkInput): string {
params.set('security', 'none'); params.set('security', 'none');
} }
// XTLS Vision flow: TCP over tls/reality (classic) or XHTTP+vlessenc (the // Same gate as the backend's vlessFlowAllowed and the form's flow field, so the
// VLESS-level encryption stands in for transport TLS). Mirrors the backend's // panel link, share link and subscription agree.
// vlessFlowAllowed and the form's flow-field gating so panel link, share
// link and subscription agree.
if ( if (
flow.length > 0 && flow.length > 0 &&
canEnableTlsFlow({ canEnableTlsFlow({
+19 -2
View File
@@ -1,6 +1,7 @@
import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp'; import { XHttpXmuxSchema } from '@/schemas/protocols/stream/xhttp';
import { OutboundDomainStrategySchema } from '@/schemas/protocols/outbound'; import { OutboundDomainStrategySchema } from '@/schemas/protocols/outbound';
import { AmneziaWGOutboundSettingsSchema } from '@/schemas/protocols/outbound'; import { AmneziaWGOutboundSettingsSchema } from '@/schemas/protocols/outbound';
import { canEnableTlsFlow } from '@/lib/xray/protocol-capabilities';
import { import {
dropEmptyFinalMask, dropEmptyFinalMask,
normalizeStreamSettingsForWire, normalizeStreamSettingsForWire,
@@ -902,10 +903,23 @@ function stripUiOnlyStreamFields(stream: unknown): Raw {
return normalizeStreamSettingsForWire(next, { side: 'outbound' }) as Raw; return normalizeStreamSettingsForWire(next, { side: 'outbound' }) as Raw;
} }
// The flow select only hides off a Vision-capable transport; its stale value must not reach xray.
export function effectiveVlessFlow(
settings: Pick<VlessOutboundFormSettings, 'flow' | 'encryption'>,
stream: { network?: string; security?: string } | undefined,
): string {
const allowed = canEnableTlsFlow({
protocol: 'vless',
settings: { encryption: settings.encryption },
streamSettings: { network: stream?.network, security: stream?.security },
});
return allowed ? settings.flow : '';
}
function muxAllowed(values: OutboundFormValues): boolean { function muxAllowed(values: OutboundFormValues): boolean {
if (!MUX_PROTOCOLS.has(values.protocol)) return false; if (!MUX_PROTOCOLS.has(values.protocol)) return false;
const flow = const flow =
values.protocol === 'vless' ? (values.settings as VlessOutboundFormSettings).flow : ''; values.protocol === 'vless' ? effectiveVlessFlow(values.settings, values.streamSettings) : '';
if (flow) return false; if (flow) return false;
const network = const network =
values.streamSettings && 'network' in values.streamSettings values.streamSettings && 'network' in values.streamSettings
@@ -924,7 +938,10 @@ export function formValuesToWirePayload(values: OutboundFormValues): WireOutboun
settings = vmessToWire(values.settings); settings = vmessToWire(values.settings);
break; break;
case 'vless': case 'vless':
settings = vlessToWire(values.settings); settings = vlessToWire({
...values.settings,
flow: effectiveVlessFlow(values.settings, values.streamSettings),
});
break; break;
case 'trojan': case 'trojan':
settings = trojanToWire(values.settings); settings = trojanToWire(values.settings);
@@ -74,10 +74,8 @@ export function canEnableTlsFlow(values: CapabilityProtocolSlice): boolean {
// Classic XTLS Vision: raw TCP carried over TLS or REALITY. // Classic XTLS Vision: raw TCP carried over TLS or REALITY.
if (network === 'tcp' && (security === 'tls' || security === 'reality')) return true; if (network === 'tcp' && (security === 'tls' || security === 'reality')) return true;
// vlessenc carries Vision over XHTTP without transport TLS. // xray-core runs Vision inside VLESS encryption (encryption.CommonConn) on any transport.
if (network === 'xhttp' && hasVlessEncryption(values.settings)) return true; return hasVlessEncryption(values.settings);
return false;
} }
export function canEnableStream(values: { protocol: string }): boolean { export function canEnableStream(values: { protocol: string }): boolean {
@@ -104,7 +104,11 @@ export default function OutboundFormModal({
'') as string; '') as string;
const security = (useWatch({ control: methods.control, name: 'streamSettings.security' }) ?? const security = (useWatch({ control: methods.control, name: 'streamSettings.security' }) ??
'none') as string; 'none') as string;
const flow = (useWatch({ control: methods.control, name: 'settings.flow' }) ?? '') as string; const storedFlow = (useWatch({ control: methods.control, name: 'settings.flow' }) ??
'') as string;
const encryption = useWatch({ control: methods.control, name: 'settings.encryption' }) as
| string
| undefined;
const reverseTag = useWatch({ control: methods.control, name: 'settings.reverseTag' }); const reverseTag = useWatch({ control: methods.control, name: 'settings.reverseTag' });
const wgSecretKey = useWatch({ control: methods.control, name: 'settings.secretKey' }) as const wgSecretKey = useWatch({ control: methods.control, name: 'settings.secretKey' }) as
| string | string
@@ -113,7 +117,12 @@ export default function OutboundFormModal({
const streamAllowed = canEnableStream({ protocol }); const streamAllowed = canEnableStream({ protocol });
const tlsAllowed = canEnableOutboundTls({ 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,
settings: { encryption },
streamSettings: { network, security },
});
const flow = tlsFlowAllowed ? storedFlow : '';
/* /*
* Parse a share link (vmess:// / vless:// / trojan:// / ss:// / * Parse a share link (vmess:// / vless:// / trojan:// / ss:// /
@@ -527,7 +536,7 @@ export default function OutboundFormModal({
{/* Vision seed knobs only meaningful for the exact {/* Vision seed knobs only meaningful for the exact
xtls-rprx-vision flow, on TCP+(tls|reality). */} xtls-rprx-vision flow, on TCP+(tls|reality). */}
{tlsFlowAllowed && flow === 'xtls-rprx-vision' && ( {flow === 'xtls-rprx-vision' && (
<> <>
<FormField <FormField
label={t('pages.xray.outboundForm.visionTestpre')} label={t('pages.xray.outboundForm.visionTestpre')}
@@ -590,7 +599,7 @@ export default function OutboundFormModal({
)} )}
/> />
<MuxForm protocol={protocol} network={network} /> <MuxForm protocol={protocol} network={network} flow={flow} />
</> </>
), ),
}, },
@@ -9,12 +9,12 @@ import { isMuxAllowed } from '../outbound-form-helpers';
interface MuxFormProps { interface MuxFormProps {
protocol: string; protocol: string;
network: string; network: string;
flow?: string;
} }
export default function MuxForm({ protocol, network }: MuxFormProps) { export default function MuxForm({ protocol, network, flow = '' }: MuxFormProps) {
const { t } = useTranslation(); const { t } = useTranslation();
const { control } = useFormContext(); const { control } = useFormContext();
const flow = (useWatch({ control, name: 'settings.flow' }) ?? '') as string;
const muxEnabled = !!useWatch({ control, name: 'mux.enabled' }); const muxEnabled = !!useWatch({ control, name: 'mux.enabled' });
if (!isMuxAllowed(protocol, flow, network)) return null; if (!isMuxAllowed(protocol, flow, network)) return null;
return ( return (
+12 -3
View File
@@ -226,12 +226,21 @@ describe('protocol-capability helpers with raw coerced shapes', () => {
streamSettings: { network: 'xhttp', security: 'none' }, streamSettings: { network: 'xhttp', security: 'none' },
}), }),
).toBe(false); ).toBe(false);
// vlessenc is XHTTP-only: TCP without tls/reality is not Vision-capable. // xray-core runs Vision inside VLESS encryption on any transport, RAW and WS included.
for (const network of ['tcp', 'ws', 'grpc', 'xdrive']) {
expect(
canEnableTlsFlow({
protocol: 'vless',
settings: { decryption: dec, encryption: enc },
streamSettings: { network, security: 'none' },
}),
).toBe(true);
}
expect( expect(
canEnableTlsFlow({ canEnableTlsFlow({
protocol: 'vless', protocol: 'vless',
settings: { decryption: dec, encryption: enc }, settings: { encryption: 'none' },
streamSettings: { network: 'tcp', security: 'none' }, streamSettings: { network: 'ws', security: 'tls' },
}), }),
).toBe(false); ).toBe(false);
}); });
@@ -58,6 +58,7 @@ describe('outbound-form-adapter: round-trip', () => {
flow: 'xtls-rprx-vision', flow: 'xtls-rprx-vision',
encryption: 'none', encryption: 'none',
}, },
streamSettings: { network: 'tcp', security: 'reality' },
}; };
const form = rawOutboundToFormValues(wire); const form = rawOutboundToFormValues(wire);
expect(form.protocol).toBe('vless'); expect(form.protocol).toBe('vless');
@@ -119,6 +120,64 @@ describe('outbound-form-adapter: round-trip', () => {
expect(settings.reverse).toMatchObject({ tag: 'rev-1' }); expect(settings.reverse).toMatchObject({ tag: 'rev-1' });
}); });
// The flow select hides off TCP+TLS/REALITY but keeps its value; xray then fails every dial.
it('vless drops a Vision flow its transport cannot carry, and lets mux back on', () => {
const outbound = (streamSettings: Record<string, unknown>) =>
formValuesToWirePayload(
rawOutboundToFormValues({
protocol: 'vless',
settings: {
address: 'srv',
port: 443,
id: '11111111-2222-4333-8444-555555555555',
flow: 'xtls-rprx-vision',
encryption: 'none',
testpre: 5,
},
streamSettings,
mux: { enabled: true, concurrency: 8 },
}),
);
const xdrive = outbound({
network: 'xdrive',
security: 'none',
xdriveSettings: { service: 'local', remoteFolder: '/tmp/shared' },
});
expect(xdrive.settings).toMatchObject({ flow: '' });
expect(xdrive.settings).not.toHaveProperty('testpre');
expect(xdrive.mux).toMatchObject({ enabled: true });
const ws = outbound({ network: 'ws', security: 'tls', wsSettings: { path: '/' } });
expect(ws.settings).toMatchObject({ flow: '' });
const reality = outbound({
network: 'tcp',
security: 'reality',
realitySettings: { serverName: 'a.example', publicKey: 'k', fingerprint: 'chrome' },
});
expect(reality.settings).toMatchObject({ flow: 'xtls-rprx-vision', testpre: 5 });
expect(reality).not.toHaveProperty('mux');
});
// VLESS encryption carries Vision on any transport; stripping it gets the server's "client flow is empty".
it('vless keeps a Vision flow over VLESS encryption on RAW without TLS', () => {
const wire = formValuesToWirePayload(
rawOutboundToFormValues({
protocol: 'vless',
settings: {
address: 'srv',
port: 443,
id: '11111111-2222-4333-8444-555555555555',
flow: 'xtls-rprx-vision',
encryption: 'mlkem768x25519plus.native.0rtt.cbEXq-_hfkxDUex6HqmSaemEELvnFWc8qQ1MxlNDHnE',
},
streamSettings: { network: 'tcp', security: 'none' },
}),
);
expect(wire.settings).toMatchObject({ flow: 'xtls-rprx-vision' });
});
it('vless does not emit testpre/testseed unless flow is vision', () => { it('vless does not emit testpre/testseed unless flow is vision', () => {
const wire = { const wire = {
protocol: 'vless', protocol: 'vless',
@@ -543,6 +602,7 @@ describe('outbound-form-adapter: round-trip', () => {
flow: 'xtls-rprx-vision', flow: 'xtls-rprx-vision',
encryption: 'none', encryption: 'none',
}, },
streamSettings: { network: 'tcp', security: 'reality' },
mux: { enabled: true }, mux: { enabled: true },
}), }),
); );
@@ -310,6 +310,60 @@ describe('OutboundFormModal', () => {
// xray-core reads Google Drive's three secrets by position (ClientID, ClientSecret, // xray-core reads Google Drive's three secrets by position (ClientID, ClientSecret,
// RefreshToken); a misordered list fails OAuth only when the tunnel first dials. // RefreshToken); a misordered list fails OAuth only when the tunnel first dials.
it('gates Flow and Mux on the flow the transport can actually carry', async () => {
const vless = (encryption: string, streamSettings: Record<string, unknown>) => ({
protocol: 'vless',
tag: 'flow-out',
settings: {
address: 'example.com',
port: 443,
id: 'c9f0c2d0-0000-4000-8000-000000000000',
flow: 'xtls-rprx-vision',
encryption,
},
streamSettings,
});
const openModal = async (outbound: Record<string, unknown>) => {
const queryClient = makeTestQueryClient();
const tree = (open: boolean) => (
<QueryClientProvider client={queryClient}>
<ThemeProvider>
<OutboundFormModal
open={open}
outbound={outbound}
existingTags={[]}
onClose={() => {}}
onConfirm={() => {}}
/>
</ThemeProvider>
</QueryClientProvider>
);
const view = render(tree(false));
view.rerender(tree(true));
await act(async () => {
await new Promise((r) => setTimeout(r, 0));
});
return view;
};
const ws = await openModal(
vless('none', { network: 'ws', security: 'tls', wsSettings: { path: '/' } }),
);
expect(formItemByLabel('Transmission')?.textContent).toContain('WebSocket');
expect(formItemByLabel('Flow')).toBeUndefined();
expect(formItemByLabel('Mux')).toBeDefined();
ws.unmount();
await openModal(
vless('mlkem768x25519plus.native.0rtt.key', {
network: 'xhttp',
security: 'none',
xhttpSettings: { path: '/', mode: 'auto' },
}),
);
expect(formItemByLabel('Flow')).toBeDefined();
});
it('saves an XDRIVE stream picked in the transmission selector', async () => { it('saves an XDRIVE stream picked in the transmission selector', async () => {
const onConfirm = vi.fn(); const onConfirm = vi.fn();
const queryClient = makeTestQueryClient(); const queryClient = makeTestQueryClient();
+5 -13
View File
@@ -1193,21 +1193,13 @@ func vlessEncryptionEnabled(settings map[string]any) bool {
return false return false
} }
// vlessFlowAllowed reports whether a client's XTLS Vision flow belongs in // vlessFlowAllowed mirrors inboundCanEnableTlsFlow in internal/web/service: Vision runs over
// generated links/configs. Mirrors inboundCanEnableTlsFlow in // TLS/REALITY on raw TCP, or inside VLESS encryption on any transport.
// internal/web/service: Vision runs on TCP with tls/reality (classic), and on
// XHTTP whenever VLESS encryption (vlessenc / ML-KEM) is enabled — there the
// VLESS-level encryption stands in for the transport TLS that Vision relies
// on, regardless of the stream security layer (so XHTTP+REALITY+vlessenc
// keeps its flow too).
func vlessFlowAllowed(network, security string, settings map[string]any) bool { func vlessFlowAllowed(network, security string, settings map[string]any) bool {
switch network { if network == "tcp" && (security == "tls" || security == "reality") {
case "tcp": return true
return security == "tls" || security == "reality"
case "xhttp":
return vlessEncryptionEnabled(settings)
} }
return false return vlessEncryptionEnabled(settings)
} }
func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string { func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
+2 -1
View File
@@ -26,7 +26,8 @@ func TestVlessFlowAllowed(t *testing.T) {
{"tcp tls", "tcp", "tls", noEnc, true}, {"tcp tls", "tcp", "tls", noEnc, true},
{"tcp reality", "tcp", "reality", noEnc, true}, {"tcp reality", "tcp", "reality", noEnc, true},
{"tcp none", "tcp", "none", noEnc, false}, {"tcp none", "tcp", "none", noEnc, false},
{"tcp none vlessenc", "tcp", "none", enc, false}, {"tcp none vlessenc", "tcp", "none", enc, true},
{"ws none vlessenc", "ws", "none", enc, true},
{"xhttp none vlessenc", "xhttp", "none", enc, true}, {"xhttp none vlessenc", "xhttp", "none", enc, true},
{"xhttp reality vlessenc (#5232)", "xhttp", "reality", enc, true}, {"xhttp reality vlessenc (#5232)", "xhttp", "reality", enc, true},
{"xhttp tls vlessenc", "xhttp", "tls", enc, true}, {"xhttp tls vlessenc", "xhttp", "tls", enc, true},
@@ -26,9 +26,8 @@ func TestClientWithInboundFlow_GatesByInboundCapability(t *testing.T) {
{"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, "", ""}, {"vless tcp none clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, "", ""},
{"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, "", ""}, {"vmess tcp tls clears flow", model.VMESS, `{"network":"tcp","security":"tls"}`, "", ""},
{"empty stream clears flow", model.VLESS, "", "", ""}, {"empty stream clears flow", model.VLESS, "", "", ""},
// vlessenc (ML-KEM) keeps Vision flow without transport TLS only on XHTTP. // vlessenc (ML-KEM) carries Vision without transport TLS on any transport.
// TCP without tls/reality clears it even with vlessenc set. {"vless tcp vlessenc keeps flow", model.VLESS, `{"network":"tcp","security":"none"}`, enc, vision},
{"vless tcp vlessenc clears flow", model.VLESS, `{"network":"tcp","security":"none"}`, enc, ""},
{"vless xhttp vlessenc keeps flow", model.VLESS, `{"network":"xhttp","security":"none"}`, enc, vision}, {"vless xhttp vlessenc keeps flow", model.VLESS, `{"network":"xhttp","security":"none"}`, enc, vision},
{"vless xhttp no encryption clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, `{"encryption":"none"}`, ""}, {"vless xhttp no encryption clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, `{"encryption":"none"}`, ""},
{"vless xhttp empty settings clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, "", ""}, {"vless xhttp empty settings clears flow", model.VLESS, `{"network":"xhttp","security":"none"}`, "", ""},
+78
View File
@@ -1005,6 +1005,78 @@ func validateFinalMaskSalamander(streamSettings string) error {
return maskcompat.CheckSalamanderPasswords(stream["finalmask"]) return maskcompat.CheckSalamanderPasswords(stream["finalmask"])
} }
// validateXDriveStorage refuses at save the XDRIVE storage xray-core refuses when an inbound starts
// listening (stopping every inbound) or an outbound dials.
func validateXDriveStorage(streamSettings string) error {
var stream struct {
Network string `json:"network"`
XDriveSettings struct {
Service string `json:"service"`
RemoteFolder string `json:"remoteFolder"`
Secrets []string `json:"secrets"`
Template json.RawMessage `json:"template"`
} `json:"xdriveSettings"`
}
if json.Unmarshal([]byte(streamSettings), &stream) != nil || stream.Network != "xdrive" {
return nil
}
xd := stream.XDriveSettings
hasFolder := strings.TrimSpace(xd.RemoteFolder) != ""
switch xd.Service {
case "local":
if !hasFolder {
return common.NewError(`XDRIVE needs a "remoteFolder": the local folder both ends share.`)
}
case "Google Drive":
if !hasFolder {
return common.NewError(`XDRIVE needs a "remoteFolder": the id of the shared Google Drive folder.`)
}
if len(xd.Secrets) != 3 || slices.ContainsFunc(xd.Secrets, func(s string) bool { return strings.TrimSpace(s) == "" }) {
return common.NewError("Google Drive needs 3 secrets, in order: ClientID, ClientSecret, RefreshToken.")
}
case "template":
if len(xd.Template) == 0 || string(xd.Template) == "null" {
return common.NewError(`XDRIVE service "template" needs a "template" object describing the storage API.`)
}
return validateXDriveTemplate(xd.Template)
default:
return common.NewError(fmt.Sprintf("unsupported XDRIVE service %q: use local, Google Drive or template.", xd.Service))
}
return nil
}
// validateXDriveTemplate applies the checks of xray-core's newTemplateStorage
// (transport/internet/xdrive/template.go), which only run once the listener starts.
func validateXDriveTemplate(raw json.RawMessage) error {
type op struct {
URL string `json:"url"`
NamesRegex string `json:"namesRegex"`
}
var tmpl struct {
Put op `json:"put"`
Get op `json:"get"`
Delete op `json:"delete"`
List op `json:"list"`
}
if err := json.Unmarshal(raw, &tmpl); err != nil {
return common.NewError("XDRIVE template is not a valid storage template:", err)
}
if tmpl.Put.URL == "" || tmpl.Get.URL == "" || tmpl.List.URL == "" || tmpl.Delete.URL == "" {
return common.NewError("XDRIVE template needs put, get, list and delete operations, each with a url.")
}
if tmpl.List.NamesRegex == "" {
return common.NewError("XDRIVE template list needs a namesRegex.")
}
names, err := regexp.Compile(tmpl.List.NamesRegex)
if err != nil {
return common.NewError("XDRIVE template namesRegex does not compile:", err)
}
if names.NumSubexp() < 1 {
return common.NewError("XDRIVE template namesRegex needs one capture group for the object name.")
}
return nil
}
// validateFinalMaskJSONSalamander checks a stored whole finalmask: a host's, or the subscription's. // validateFinalMaskJSONSalamander checks a stored whole finalmask: a host's, or the subscription's.
func validateFinalMaskJSONSalamander(finalmask string) error { func validateFinalMaskJSONSalamander(finalmask string) error {
var fm map[string]any var fm map[string]any
@@ -1178,6 +1250,9 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if err := validateFinalMaskSalamander(inbound.StreamSettings); err != nil { if err := validateFinalMaskSalamander(inbound.StreamSettings); err != nil {
return inbound, false, err return inbound, false, err
} }
if err := validateXDriveStorage(inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateShadowsocks2022Inbound(inbound.Protocol, inbound.Settings); err != nil { if err := validateShadowsocks2022Inbound(inbound.Protocol, inbound.Settings); err != nil {
return inbound, false, err return inbound, false, err
} }
@@ -1826,6 +1901,9 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
if err := validateFinalMaskSalamander(inbound.StreamSettings); err != nil { if err := validateFinalMaskSalamander(inbound.StreamSettings); err != nil {
return inbound, false, err return inbound, false, err
} }
if err := validateXDriveStorage(inbound.StreamSettings); err != nil {
return inbound, false, err
}
if err := validateShadowsocks2022Inbound(inbound.Protocol, inbound.Settings); err != nil { if err := validateShadowsocks2022Inbound(inbound.Protocol, inbound.Settings); err != nil {
return inbound, false, err return inbound, false, err
} }
+7 -21
View File
@@ -27,35 +27,21 @@ func inboundShadowsocksMethod(protocol, settings string) string {
return s.Method return s.Method
} }
// inboundCanEnableTlsFlow mirrors canEnableTlsFlow() from the frontend // inboundCanEnableTlsFlow mirrors the frontend's canEnableTlsFlow: xray-core runs Vision over
// (frontend/src/lib/xray/protocol-capabilities.ts). XTLS Vision is valid for // TLS/REALITY on raw TCP, or inside VLESS encryption (encryption.CommonConn) on any transport.
// VLESS on TCP with tls or reality (classic), and on XHTTP when VLESS encryption
// (vlessenc / ML-KEM) is enabled — there the post-quantum, VLESS-level
// encryption stands in for the transport TLS that Vision relies on. settings is
// the inbound's raw settings JSON, which carries the encryption value
// (streamSettings does not).
func inboundCanEnableTlsFlow(protocol, streamSettings, settings string) bool { func inboundCanEnableTlsFlow(protocol, streamSettings, settings string) bool {
if protocol != string(model.VLESS) { if protocol != string(model.VLESS) {
return false return false
} }
if streamSettings == "" {
return false
}
var stream struct { var stream struct {
Network string `json:"network"` Network string `json:"network"`
Security string `json:"security"` Security string `json:"security"`
} }
if err := json.Unmarshal([]byte(streamSettings), &stream); err != nil { if json.Unmarshal([]byte(streamSettings), &stream) == nil && stream.Network == "tcp" &&
return false (stream.Security == "tls" || stream.Security == "reality") {
} return true
switch stream.Network {
case "tcp":
return stream.Security == "tls" || stream.Security == "reality"
case "xhttp":
return vlessEncryptionEnabled(settings)
default:
return false
} }
return vlessEncryptionEnabled(settings)
} }
// nodeEligibleProtocols mirrors the frontend's NODE_ELIGIBLE_PROTOCOLS. A sidecar // nodeEligibleProtocols mirrors the frontend's NODE_ELIGIBLE_PROTOCOLS. A sidecar
@@ -141,7 +127,7 @@ func vlessEncValueSet(v string) bool {
// inboundCanHostFallbacks gates the settings.fallbacks injection. // inboundCanHostFallbacks gates the settings.fallbacks injection.
// Xray only honors fallbacks on VLESS and Trojan inbounds carried over // Xray only honors fallbacks on VLESS and Trojan inbounds carried over
// TCP transport with TLS or Reality security. This is intentionally stricter // TCP transport with TLS or Reality security. This is intentionally stricter
// than inboundCanEnableTlsFlow (which also accepts XHTTP+vlessenc): fallbacks // than inboundCanEnableTlsFlow (which also accepts any vlessenc stream): fallbacks
// are a raw-TCP-only feature. // are a raw-TCP-only feature.
func inboundCanHostFallbacks(ib *model.Inbound) bool { func inboundCanHostFallbacks(ib *model.Inbound) bool {
if ib == nil { if ib == nil {
@@ -26,12 +26,11 @@ func TestInboundCanEnableTlsFlow(t *testing.T) {
{"vmess tcp tls", string(model.VMESS), `{"network":"tcp","security":"tls"}`, "", false}, {"vmess tcp tls", string(model.VMESS), `{"network":"tcp","security":"tls"}`, "", false},
{"empty stream", string(model.VLESS), "", "", false}, {"empty stream", string(model.VLESS), "", "", false},
// vlessenc is gated to XHTTP only. TCP without tls/reality is NOT // xray-core runs Vision inside VLESS encryption on any transport (encryption.CommonConn).
// Vision-capable even with vlessenc set — the combination only works on {"vless raw vlessenc", string(model.VLESS), `{"network":"tcp","security":"none"}`, `{"decryption":"mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E","encryption":"mlkem768x25519plus.native.0rtt.hT4AY_tPWY9NVuKR3BIXxXq6zx9DqN2X86QPYW09XEM"}`, true},
// XHTTP in practice. {"vless ws vlessenc", string(model.VLESS), `{"network":"ws","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, true},
{"vless tcp vlessenc not capable", string(model.VLESS), `{"network":"tcp","security":"none"}`, `{"decryption":"mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E","encryption":"mlkem768x25519plus.native.0rtt.hT4AY_tPWY9NVuKR3BIXxXq6zx9DqN2X86QPYW09XEM"}`, false}, {"vless xdrive vlessenc", string(model.VLESS), `{"network":"xdrive","security":"none"}`, `{"decryption":"` + vlessEncValue + `"}`, true},
// ws is a framed transport — vlessenc never enables Vision there. {"vless xdrive no encryption", string(model.VLESS), `{"network":"xdrive","security":"none"}`, `{"decryption":"none"}`, false},
{"vless ws vlessenc still off", string(model.VLESS), `{"network":"ws","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, false},
// XHTTP + VLESS encryption (the #5157 case). // XHTTP + VLESS encryption (the #5157 case).
{"vless xhttp vlessenc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, true}, {"vless xhttp vlessenc", string(model.VLESS), `{"network":"xhttp","security":"none"}`, `{"encryption":"` + vlessEncValue + `"}`, true},
@@ -0,0 +1,91 @@
package service
import (
"strings"
"testing"
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
)
func xdriveInbound(port int, xdriveSettings string) *model.Inbound {
return &model.Inbound{
Enable: true, Listen: "127.0.0.1", Port: port, Protocol: model.VLESS,
Settings: `{"clients":[],"decryption":"none"}`,
StreamSettings: `{"network":"xdrive","security":"none","xdriveSettings":` + xdriveSettings + `}`,
}
}
// xray-core opens XDRIVE storage when the listener starts; a storage it refuses there stops every inbound.
func TestAddInboundRefusesXDriveStorageTheCoreCannotOpen(t *testing.T) {
tests := []struct {
name string
settings string
want string
}{
{"local without folder", `{"service":"local","remoteFolder":""}`, `XDRIVE needs a "remoteFolder"`},
{"google drive without folder", `{"service":"Google Drive","remoteFolder":" ","secrets":["id","secret","token"]}`, `XDRIVE needs a "remoteFolder"`},
{"google drive with two secrets", `{"service":"Google Drive","remoteFolder":"f","secrets":["id","secret"]}`, "Google Drive needs 3 secrets"},
{"google drive with an empty secret", `{"service":"Google Drive","remoteFolder":"f","secrets":["id","","token"]}`, "Google Drive needs 3 secrets"},
{"template without template", `{"service":"template","remoteFolder":"f"}`, `XDRIVE service "template" needs a "template" object`},
{"template that is empty", `{"service":"template","remoteFolder":"f","template":{}}`, "XDRIVE template needs put, get, list and delete operations"},
{"template that is not an object", `{"service":"template","remoteFolder":"f","template":"put"}`, "XDRIVE template is not a valid storage template"},
{"template without a delete op", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u","namesRegex":"(.+)"}}}`, "XDRIVE template needs put, get, list and delete operations"},
{"template without namesRegex", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u"},"delete":{"url":"u"}}}`, "XDRIVE template list needs a namesRegex"},
{"template namesRegex without a group", `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u","namesRegex":"name"},"delete":{"url":"u"}}}`, "XDRIVE template namesRegex needs one capture group"},
{"unknown service", `{"service":"Dropbox","remoteFolder":"f"}`, `unsupported XDRIVE service "Dropbox"`},
}
for i, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
setupBulkDB(t)
_, _, err := (&InboundService{}).AddInbound(xdriveInbound(18460+i, tc.settings))
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("AddInbound err = %v, want one containing %q", err, tc.want)
}
})
}
}
func TestAddInboundAcceptsACompleteXDriveTemplate(t *testing.T) {
setupBulkDB(t)
tmpl := `{"service":"template","template":{"put":{"url":"u"},"get":{"url":"u"},"list":{"url":"u","namesRegex":"\"name\":\"([^\"]+)\""},"delete":{"url":"u"}}}`
if _, _, err := (&InboundService{}).AddInbound(xdriveInbound(18480, tmpl)); err != nil {
t.Fatalf("AddInbound of a template xray-core can open: %v", err)
}
}
func TestSavePathsRefuseXDriveWithoutStorage(t *testing.T) {
const want = `XDRIVE needs a "remoteFolder"`
const empty = `{"service":"local","remoteFolder":""}`
tests := []struct {
name string
save func(t *testing.T) error
}{
{"update inbound", func(t *testing.T) error {
in, _, err := (&InboundService{}).AddInbound(xdriveInbound(18470, `{"service":"local","remoteFolder":"/tmp/drive"}`))
if err != nil {
t.Fatalf("AddInbound of a complete XDRIVE inbound: %v", err)
}
in.StreamSettings = xdriveInbound(18470, empty).StreamSettings
_, _, err = (&InboundService{}).UpdateInbound(in)
return err
}},
{"xray template inbound", func(t *testing.T) error {
return (&XraySettingService{}).SaveXraySetting(`{"inbounds":[{"tag":"drive","port":18471,"protocol":"vless",
"settings":{"clients":[],"decryption":"none"},"streamSettings":` + xdriveInbound(18471, empty).StreamSettings + `}],
"outbounds":[{"protocol":"freedom","tag":"direct"}]}`)
}},
{"xray template outbound", func(t *testing.T) error {
return (&XraySettingService{}).SaveXraySetting(`{"outbounds":[{"protocol":"freedom","tag":"direct"},
{"protocol":"vless","tag":"drive-out","settings":{"address":"drive.example","port":443,"id":"11111111-2222-4333-8444-555555555555","encryption":"none"},
"streamSettings":` + xdriveInbound(0, empty).StreamSettings + `}]}`)
}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
setupBulkDB(t)
if err := tc.save(t); err == nil || !strings.Contains(err.Error(), want) {
t.Fatalf("save err = %v, want one containing %q", err, want)
}
})
}
}
+3 -1
View File
@@ -208,6 +208,8 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
} }
settings := map[string]any{} settings := map[string]any{}
_ = json.Unmarshal([]byte(inbound.Settings), &settings) _ = json.Unmarshal([]byte(inbound.Settings), &settings)
flowAllowed := !inbound.DisableFlow &&
inboundCanEnableTlsFlow(string(inbound.Protocol), inbound.StreamSettings, inbound.Settings)
var wireguardClientsByEmail map[string]model.Client var wireguardClientsByEmail map[string]model.Client
if inbound.Protocol == model.WireGuard { if inbound.Protocol == model.WireGuard {
inboundClients, _ := ParseInboundSettingsClients(inbound.Settings) inboundClients, _ := ParseInboundSettingsClients(inbound.Settings)
@@ -245,7 +247,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if flow == "xtls-rprx-vision-udp443" { if flow == "xtls-rprx-vision-udp443" {
flow = "xtls-rprx-vision" flow = "xtls-rprx-vision"
} }
if inbound.DisableFlow { if !flowAllowed {
flow = "" flow = ""
} }
entry := map[string]any{"email": c.Email} entry := map[string]any{"email": c.Email}
@@ -107,3 +107,71 @@ func TestGetXrayConfig_EnabledClientsStillEmitted(t *testing.T) {
t.Errorf("client id not carried through: %#v", entry) t.Errorf("client id not carried through: %#v", entry)
} }
} }
// A flow kept from a TCP+REALITY stream makes xray demand Vision on a transport that cannot run it
// without VLESS encryption: "XTLS only supports TLS and REALITY directly".
func TestGetXrayConfig_OmitsVisionFlowOnIneligibleStream(t *testing.T) {
cases := []struct {
name string
stream string
}{
{"xdrive", `{"network":"xdrive","security":"none","xdriveSettings":{"service":"local","remoteFolder":"/tmp/shared"}}`},
{"ws over tls", `{"network":"ws","security":"tls","wsSettings":{"path":"/"}}`},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tag := "stale-flow"
seedVlessInbound(t, tag, 43110+i, []model.Client{
{Email: "vision@x", ID: "44444444-4444-4444-4444-444444444444", Enable: true, Flow: "xtls-rprx-vision"},
})
if err := database.GetDB().Model(&model.Inbound{}).Where("tag = ?", tag).
Update("stream_settings", tc.stream).Error; err != nil {
t.Fatalf("set stream: %v", err)
}
value, _ := emittedClients(t, tag)
list, ok := value.([]any)
if !ok || len(list) != 1 {
t.Fatalf("settings.clients = %#v, want the one enabled client", value)
}
entry, _ := list[0].(map[string]any)
if flow, present := entry["flow"]; present {
t.Fatalf("flow %q emitted for a %s stream", flow, tc.name)
}
})
}
}
// xray-core runs Vision over TLS/REALITY on RAW, or over VLESS encryption on any transport.
func TestGetXrayConfig_KeepsVisionFlowWhereTheCoreRunsIt(t *testing.T) {
const vlessenc = `{"clients":[],"decryption":"mlkem768x25519plus.native.600s.WB3OymHLL_DJ1jdHiqcMC-u84TgvfNFlgbZIJE-NtUQ"}`
cases := []struct {
name string
stream string
settings string
}{
{"tcp reality", `{"network":"tcp","security":"reality","realitySettings":{}}`, `{"clients":[],"decryption":"none"}`},
{"raw vlessenc", `{"network":"tcp","security":"none"}`, vlessenc},
{"ws vlessenc", `{"network":"ws","security":"none","wsSettings":{"path":"/"}}`, vlessenc},
{"xdrive vlessenc", `{"network":"xdrive","security":"none","xdriveSettings":{"service":"local","remoteFolder":"/tmp/shared"}}`, vlessenc},
}
for i, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
tag := "vision-flow"
seedVlessInbound(t, tag, 43120+i, []model.Client{
{Email: "vision@x", ID: "55555555-5555-5555-5555-555555555555", Enable: true, Flow: "xtls-rprx-vision"},
})
if err := database.GetDB().Model(&model.Inbound{}).Where("tag = ?", tag).
Updates(map[string]any{"stream_settings": tc.stream, "settings": tc.settings}).Error; err != nil {
t.Fatalf("set stream: %v", err)
}
value, _ := emittedClients(t, tag)
list, _ := value.([]any)
if len(list) != 1 {
t.Fatalf("settings.clients = %#v, want one client", value)
}
if entry, _ := list[0].(map[string]any); entry["flow"] != "xtls-rprx-vision" {
t.Fatalf("flow = %#v, want xtls-rprx-vision kept on %s", entry["flow"], tc.name)
}
})
}
}
+6
View File
@@ -57,6 +57,9 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
if err := validateFinalMaskSalamander(string(inbound.StreamSettings)); err != nil { if err := validateFinalMaskSalamander(string(inbound.StreamSettings)); err != nil {
return common.NewError("xray template inbound \""+inbound.Tag+"\":", err) return common.NewError("xray template inbound \""+inbound.Tag+"\":", err)
} }
if err := validateXDriveStorage(string(inbound.StreamSettings)); err != nil {
return common.NewError("xray template inbound \""+inbound.Tag+"\":", err)
}
} }
if len(xrayConfig.OutboundConfigs) > 0 { if len(xrayConfig.OutboundConfigs) > 0 {
var outbounds []json.RawMessage var outbounds []json.RawMessage
@@ -90,6 +93,9 @@ func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error {
if err := validateFinalMaskSalamander(string(probe.StreamSettings)); err != nil { if err := validateFinalMaskSalamander(string(probe.StreamSettings)); err != nil {
return common.NewError("xray template outbound \""+probe.Tag+"\":", err) return common.NewError("xray template outbound \""+probe.Tag+"\":", err)
} }
if err := validateXDriveStorage(string(probe.StreamSettings)); err != nil {
return common.NewError("xray template outbound \""+probe.Tag+"\":", err)
}
if err := xray.ValidateOutboundConfig(outbound); err != nil { if err := xray.ValidateOutboundConfig(outbound); err != nil {
if shouldSkipLegacyUnencryptedOutboundRejection(coreVersion, err) { if shouldSkipLegacyUnencryptedOutboundRejection(coreVersion, err) {
continue continue