mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-28 00:24:19 +00:00
9c8cd08f90
WireGuard inbounds now manage per-client peers using xray-core's native WireGuard users (AddUser/RemoveUser). Each client lives in settings.clients (canonical, like every other protocol) and is projected to peers[] only when emitting the xray config, at level 0 so the dispatcher's per-user traffic/online counters work with no extra plumbing. Backend: internal/util/wireguard gains KeyToHex (base64 to hex for the gRPC path), PublicKeyFromPrivate and GenerateWireguardPSK; xray/api.go builds a wireguard account in AddUser with hex keys (RemoveUser already worked); client CRUD generates a keypair and allocates a unique tunnel address per client and never rotates keys on edit; an idempotent migration converts legacy settings.peers into managed clients; WireGuard is included in the raw subscription. Frontend: WireGuard in the add-client modal with keys on the credential tab, client schema, per-client QR/link/.conf, inbound form reduced to server settings; i18n added across 13 locales. Fix: guard the settings[clients] assertion in add/update so a legacy WireGuard inbound stored without a clients key no longer panics.
167 lines
6.0 KiB
TypeScript
167 lines
6.0 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
|
|
import {
|
|
createDefaultHttpInboundSettings,
|
|
createDefaultHysteriaClient,
|
|
createDefaultHysteriaInboundSettings,
|
|
createDefaultMixedInboundSettings,
|
|
createDefaultShadowsocksClient,
|
|
createDefaultShadowsocksInboundSettings,
|
|
createDefaultTrojanClient,
|
|
createDefaultTrojanInboundSettings,
|
|
createDefaultTunnelInboundSettings,
|
|
createDefaultVlessClient,
|
|
createDefaultVlessInboundSettings,
|
|
createDefaultVmessClient,
|
|
createDefaultVmessInboundSettings,
|
|
createDefaultWireguardInboundSettings,
|
|
} from '@/lib/xray/inbound-defaults';
|
|
import { createHysteriaTlsSettingsWithDefaultCert } from '@/lib/xray/inbound-tls-defaults';
|
|
import { HttpInboundSettingsSchema } from '@/schemas/protocols/inbound/http';
|
|
import { HysteriaClientSchema, HysteriaInboundSettingsSchema } from '@/schemas/protocols/inbound/hysteria';
|
|
import { MixedInboundSettingsSchema } from '@/schemas/protocols/inbound/mixed';
|
|
import { ShadowsocksClientSchema, ShadowsocksInboundSettingsSchema } from '@/schemas/protocols/inbound/shadowsocks';
|
|
import { TrojanClientSchema, TrojanInboundSettingsSchema } from '@/schemas/protocols/inbound/trojan';
|
|
import { TunnelInboundSettingsSchema } from '@/schemas/protocols/inbound/tunnel';
|
|
import { VlessClientSchema, VlessInboundSettingsSchema } from '@/schemas/protocols/inbound/vless';
|
|
import { VmessClientSchema, VmessInboundSettingsSchema } from '@/schemas/protocols/inbound/vmess';
|
|
import { WireguardInboundSettingsSchema } from '@/schemas/protocols/inbound/wireguard';
|
|
|
|
// Tests pass explicit seeds for every random field so the assertions don't
|
|
// depend on window.crypto (the node test env has no crypto.randomUUID).
|
|
// Each factory is verified two ways:
|
|
// 1. snapshot — locks the exact shape
|
|
// 2. Zod parse round-trip — confirms the factory output is a valid
|
|
// member of the protocol's client schema (no missing defaults, no
|
|
// stray fields)
|
|
|
|
const seed = {
|
|
email: 'fixture@example.test',
|
|
subId: 'fixed-sub-id-1234',
|
|
};
|
|
|
|
describe('createDefaultVlessClient', () => {
|
|
it('produces a Zod-valid client', () => {
|
|
const c = createDefaultVlessClient({ ...seed, id: '11111111-2222-4333-8444-555555555555' });
|
|
expect(c).toMatchSnapshot();
|
|
expect(VlessClientSchema.parse(c)).toEqual(c);
|
|
});
|
|
});
|
|
|
|
describe('createDefaultVmessClient', () => {
|
|
it('produces a Zod-valid client', () => {
|
|
const c = createDefaultVmessClient({ ...seed, id: 'aaaaaaaa-bbbb-4ccc-9ddd-eeeeeeeeeeee' });
|
|
expect(c).toMatchSnapshot();
|
|
expect(VmessClientSchema.parse(c)).toEqual(c);
|
|
});
|
|
});
|
|
|
|
describe('createDefaultTrojanClient', () => {
|
|
it('produces a Zod-valid client', () => {
|
|
const c = createDefaultTrojanClient({ ...seed, password: 'fixed-trojan-pw' });
|
|
expect(c).toMatchSnapshot();
|
|
expect(TrojanClientSchema.parse(c)).toEqual(c);
|
|
});
|
|
});
|
|
|
|
describe('createDefaultShadowsocksClient', () => {
|
|
it('produces a Zod-valid client', () => {
|
|
const c = createDefaultShadowsocksClient({ ...seed, password: 'ZmFrZS1zcy1wYXNzd29yZA==' });
|
|
expect(c).toMatchSnapshot();
|
|
expect(ShadowsocksClientSchema.parse(c)).toEqual(c);
|
|
});
|
|
});
|
|
|
|
describe('createDefaultHysteriaClient', () => {
|
|
it('produces a Zod-valid client', () => {
|
|
const c = createDefaultHysteriaClient({ ...seed, auth: 'fixed-hyst-auth' });
|
|
expect(c).toMatchSnapshot();
|
|
expect(HysteriaClientSchema.parse(c)).toEqual(c);
|
|
});
|
|
});
|
|
|
|
describe('createDefault*InboundSettings factories', () => {
|
|
it('vless', () => {
|
|
const s = createDefaultVlessInboundSettings();
|
|
expect(s).toMatchSnapshot();
|
|
expect(VlessInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('vmess', () => {
|
|
const s = createDefaultVmessInboundSettings();
|
|
expect(s).toMatchSnapshot();
|
|
expect(VmessInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('trojan', () => {
|
|
const s = createDefaultTrojanInboundSettings();
|
|
expect(s).toMatchSnapshot();
|
|
expect(TrojanInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('shadowsocks', () => {
|
|
const s = createDefaultShadowsocksInboundSettings({ password: 'ZmFrZS1zcy1zZWVk' });
|
|
expect(s).toMatchSnapshot();
|
|
expect(ShadowsocksInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('hysteria (v1, defaults to v2 wire version)', () => {
|
|
const s = createDefaultHysteriaInboundSettings();
|
|
expect(s).toMatchSnapshot();
|
|
expect(HysteriaInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('http', () => {
|
|
const s = createDefaultHttpInboundSettings();
|
|
expect(s.allowTransparent).toBe(false);
|
|
const accounts = s.accounts ?? [];
|
|
expect(accounts).toHaveLength(1);
|
|
expect(accounts[0].user.length).toBe(8);
|
|
expect(accounts[0].pass.length).toBe(12);
|
|
expect(HttpInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('mixed', () => {
|
|
const s = createDefaultMixedInboundSettings();
|
|
expect(s.auth).toBe('password');
|
|
expect(s.udp).toBe(false);
|
|
expect(s.ip).toBe('127.0.0.1');
|
|
const accounts = s.accounts ?? [];
|
|
expect(accounts).toHaveLength(1);
|
|
expect(accounts[0].user.length).toBe(8);
|
|
expect(accounts[0].pass.length).toBe(12);
|
|
expect(MixedInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('tunnel', () => {
|
|
const s = createDefaultTunnelInboundSettings();
|
|
expect(s).toMatchSnapshot();
|
|
expect(TunnelInboundSettingsSchema.parse(s)).toEqual(s);
|
|
});
|
|
|
|
it('wireguard', () => {
|
|
const s = createDefaultWireguardInboundSettings({
|
|
secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
|
|
});
|
|
expect(s).toMatchSnapshot();
|
|
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
|
|
expect(s.peers).toEqual([]);
|
|
expect(s.clients).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('createHysteriaTlsSettingsWithDefaultCert', () => {
|
|
it('defaults Hysteria TLS to uTLS None and h3 ALPN', () => {
|
|
const tls = createHysteriaTlsSettingsWithDefaultCert();
|
|
expect(tls.alpn).toEqual(['h3']);
|
|
expect((tls.settings as Record<string, unknown>).fingerprint).toBe('');
|
|
expect(tls.certificates).toEqual([
|
|
expect.objectContaining({
|
|
useFile: true,
|
|
certificateFile: '',
|
|
keyFile: '',
|
|
}),
|
|
]);
|
|
});
|
|
});
|