mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-13 09:25:14 +03:00
c7a76e9626
* fix: enable XTLS vision flow for VLESS+XHTTP+vlessenc in UI and share links (#5157) * fix: enable xtls-rprx-vision flow for VLESS XHTTP with vlessenc encryption (#5157) The flow selector was hidden and the vless:// link omitted flow= because: 1. The backend gate (inboundCanEnableTlsFlow) only accepted tcp+tls/reality. 2. The PR #5185 frontend check used `encryption === 'vlessenc'`, which never matches — the stored value is a generated ML-KEM dotted string, not the CLI subcommand name. Fix: extend inboundCanEnableTlsFlow to also return true for XHTTP when a non-none vlessenc encryption/decryption value is present. Update all three call-sites (inbound.go TlsFlowCapable field, client_crud.go clientWithInboundFlow, inbound_clients.go copy-flow path) and the sub/service.go link generator. Scope is XHTTP-only: TCP without tls/reality is intentionally excluded. Add inbound_protocol_test.go covering the new and existing gate combinations, extend client_flow_isolation_test.go with xhttp+vlessenc cases, and add frontend tests for canEnableTlsFlow with real ML-KEM key values. --------- Co-authored-by: rqzbeh <rqzbeh@users.noreply.github.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
247 lines
8.3 KiB
TypeScript
247 lines
8.3 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import { inboundFromDb } from '@/lib/xray/inbound-from-db';
|
|
import {
|
|
genAllLinks,
|
|
genInboundLinks,
|
|
genWireguardConfigs,
|
|
genWireguardLinks,
|
|
getInboundClients,
|
|
} from '@/lib/xray/inbound-link';
|
|
import {
|
|
canEnableTlsFlow,
|
|
isSS2022,
|
|
isSSMultiUser,
|
|
} from '@/lib/xray/protocol-capabilities';
|
|
|
|
const FALLBACK_HOST = 'panel.example.test';
|
|
|
|
const BASE_DB_FIELDS = {
|
|
port: 12345,
|
|
listen: '',
|
|
tag: '',
|
|
remark: 'unit',
|
|
enable: true,
|
|
expiryTime: 0,
|
|
up: 0,
|
|
down: 0,
|
|
total: 0,
|
|
sniffing: '',
|
|
};
|
|
|
|
describe('inboundFromDb', () => {
|
|
it('coerces JSON-string settings into a parsed object', () => {
|
|
const raw = {
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'vless',
|
|
settings: JSON.stringify({
|
|
clients: [{ id: 'abc', email: 'a@test', flow: '' }],
|
|
decryption: 'none',
|
|
}),
|
|
streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }),
|
|
};
|
|
const inbound = inboundFromDb(raw);
|
|
expect(inbound.protocol).toBe('vless');
|
|
expect(inbound.port).toBe(12345);
|
|
expect((inbound.settings as { decryption?: string }).decryption).toBe('none');
|
|
expect((inbound.streamSettings as { network?: string })?.network).toBe('tcp');
|
|
});
|
|
|
|
it('fills schema defaults onto partial object settings', () => {
|
|
const settings = { clients: [], decryption: 'none' };
|
|
const raw = {
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'vless',
|
|
settings,
|
|
streamSettings: { network: 'ws', security: 'tls' },
|
|
};
|
|
const inbound = inboundFromDb(raw);
|
|
// encryption/fallbacks defaulted by schema, original settings ref not preserved
|
|
expect(inbound.settings).not.toBe(settings);
|
|
expect((inbound.settings as { encryption?: string }).encryption).toBe('none');
|
|
expect((inbound.streamSettings as { security?: string })?.security).toBe('tls');
|
|
});
|
|
|
|
it('returns schema-default settings for missing/empty fields without throwing', () => {
|
|
const raw = {
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'http',
|
|
settings: '',
|
|
streamSettings: '',
|
|
sniffing: '',
|
|
};
|
|
const inbound = inboundFromDb(raw);
|
|
// http settings has its own schema defaults (accounts: [], allowTransparent: false)
|
|
expect(inbound.settings).toEqual(expect.objectContaining({ accounts: [] }));
|
|
expect(inbound.streamSettings).toEqual({});
|
|
expect(inbound.sniffing).toEqual({});
|
|
});
|
|
|
|
it('feeds genInboundLinks for vless without throwing', () => {
|
|
const raw = {
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'vless',
|
|
settings: {
|
|
clients: [{ id: '8c14d6f7-2e3b-4a91-9d24-3f7a6b8c1e02', email: 'alice@test', flow: '' }],
|
|
decryption: 'none',
|
|
},
|
|
streamSettings: { network: 'tcp', security: 'none' },
|
|
};
|
|
const inbound = inboundFromDb(raw);
|
|
const links = genInboundLinks({
|
|
inbound,
|
|
remark: 'unit',
|
|
fallbackHostname: FALLBACK_HOST,
|
|
});
|
|
expect(links).toContain('vless://');
|
|
expect(links).toContain('encryption=none');
|
|
});
|
|
|
|
it('feeds genWireguardConfigs + genWireguardLinks for wireguard peers', () => {
|
|
const raw = {
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'wireguard',
|
|
settings: {
|
|
mtu: 1420,
|
|
secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
|
|
peers: [
|
|
{
|
|
privateKey: 'iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=',
|
|
publicKey: 'DGSYIcEKAUkA7HhzGSjxLZuV67BR3LeyU0BMLJzNVHQ=',
|
|
allowedIPs: ['10.0.0.2/32'],
|
|
keepAlive: 25,
|
|
},
|
|
],
|
|
noKernelTun: false,
|
|
},
|
|
streamSettings: '',
|
|
};
|
|
const inbound = inboundFromDb(raw);
|
|
const configs = genWireguardConfigs({ inbound, remark: 'wg', fallbackHostname: FALLBACK_HOST });
|
|
expect(configs).toContain('[Interface]');
|
|
expect(configs).toContain('[Peer]');
|
|
const links = genWireguardLinks({ inbound, remark: 'wg', fallbackHostname: FALLBACK_HOST });
|
|
expect(links).toMatch(/^wireguard:\/\//);
|
|
});
|
|
|
|
it('feeds genAllLinks per client', () => {
|
|
const raw = {
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'trojan',
|
|
settings: {
|
|
clients: [
|
|
{ password: 'pw1', email: 'one@test' },
|
|
{ password: 'pw2', email: 'two@test' },
|
|
],
|
|
},
|
|
streamSettings: { network: 'tcp', security: 'tls', tlsSettings: { serverName: 'example.test' } },
|
|
};
|
|
const inbound = inboundFromDb(raw);
|
|
const entries = genAllLinks({
|
|
inbound,
|
|
remark: 'trojan',
|
|
client: { password: 'pw1', email: 'one@test' },
|
|
fallbackHostname: FALLBACK_HOST,
|
|
});
|
|
expect(entries.length).toBeGreaterThan(0);
|
|
expect(entries[0].link).toContain('trojan://');
|
|
});
|
|
});
|
|
|
|
describe('protocol-capability helpers with raw coerced shapes', () => {
|
|
it('isSSMultiUser returns true for legacy SS methods', () => {
|
|
expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(true);
|
|
expect(isSSMultiUser({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } })).toBe(true);
|
|
});
|
|
|
|
it('isSSMultiUser returns false for single-user blake3-chacha20 method', () => {
|
|
expect(isSSMultiUser({
|
|
protocol: 'shadowsocks',
|
|
settings: { method: '2022-blake3-chacha20-poly1305' },
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('isSS2022 detects 2022-blake3 family', () => {
|
|
expect(isSS2022({ protocol: 'shadowsocks', settings: { method: '2022-blake3-aes-128-gcm' } })).toBe(true);
|
|
expect(isSS2022({ protocol: 'shadowsocks', settings: { method: 'aes-256-gcm' } })).toBe(false);
|
|
});
|
|
|
|
it('canEnableTlsFlow gates on vless + tcp + tls/reality', () => {
|
|
expect(canEnableTlsFlow({
|
|
protocol: 'vless',
|
|
streamSettings: { network: 'tcp', security: 'tls' },
|
|
})).toBe(true);
|
|
expect(canEnableTlsFlow({
|
|
protocol: 'vless',
|
|
streamSettings: { network: 'ws', security: 'tls' },
|
|
})).toBe(false);
|
|
expect(canEnableTlsFlow({
|
|
protocol: 'vmess',
|
|
streamSettings: { network: 'tcp', security: 'tls' },
|
|
})).toBe(false);
|
|
});
|
|
|
|
it('canEnableTlsFlow allows vless + xhttp when vlessenc encryption is set', () => {
|
|
const enc = 'mlkem768x25519plus.native.0rtt.G3cdPSd1-NnlpTbWNSM5vHsT5VNzWfFzYSKwbUMnV1Y';
|
|
const dec = 'mlkem768x25519plus.native.600s.mMFxPe7lz5xoq2qBk22cQYefu5fpc_2dGR8lMOKem0E';
|
|
// XHTTP + a real (generated) encryption value → Vision flow allowed.
|
|
expect(canEnableTlsFlow({
|
|
protocol: 'vless',
|
|
settings: { encryption: enc },
|
|
streamSettings: { network: 'xhttp', security: 'none' },
|
|
})).toBe(true);
|
|
// decryption alone (server-side value) is enough on XHTTP.
|
|
expect(canEnableTlsFlow({
|
|
protocol: 'vless',
|
|
settings: { decryption: dec, encryption: 'none' },
|
|
streamSettings: { network: 'xhttp', security: 'none' },
|
|
})).toBe(true);
|
|
// No encryption → stays gated off.
|
|
expect(canEnableTlsFlow({
|
|
protocol: 'vless',
|
|
settings: { encryption: 'none' },
|
|
streamSettings: { network: 'xhttp', security: 'none' },
|
|
})).toBe(false);
|
|
// vlessenc is XHTTP-only: TCP without tls/reality is not Vision-capable.
|
|
expect(canEnableTlsFlow({
|
|
protocol: 'vless',
|
|
settings: { decryption: dec, encryption: enc },
|
|
streamSettings: { network: 'tcp', security: 'none' },
|
|
})).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('getInboundClients with schema-shaped inbound', () => {
|
|
it('returns clients array for vless/vmess/trojan/hysteria', () => {
|
|
const inbound = inboundFromDb({
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'vless',
|
|
settings: { clients: [{ id: 'x', email: 'e@test' }], decryption: 'none' },
|
|
streamSettings: { network: 'tcp', security: 'none' },
|
|
});
|
|
expect(getInboundClients(inbound)).toHaveLength(1);
|
|
});
|
|
|
|
it('returns null for SS single-user', () => {
|
|
const inbound = inboundFromDb({
|
|
...BASE_DB_FIELDS,
|
|
protocol: 'shadowsocks',
|
|
settings: { method: '2022-blake3-chacha20-poly1305', password: 'pw', clients: [] },
|
|
streamSettings: { network: 'tcp', security: 'none' },
|
|
});
|
|
expect(getInboundClients(inbound)).toBeNull();
|
|
});
|
|
|
|
it('returns null for non-client protocols (http/mixed/tun/tunnel)', () => {
|
|
for (const protocol of ['http', 'mixed', 'tun', 'tunnel']) {
|
|
const inbound = inboundFromDb({
|
|
...BASE_DB_FIELDS,
|
|
protocol,
|
|
settings: {},
|
|
streamSettings: '',
|
|
});
|
|
expect(getInboundClients(inbound)).toBeNull();
|
|
}
|
|
});
|
|
});
|