| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246 |
- 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();
- }
- });
- });
|