mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-15 17:16:07 +00:00
feat(wireguard): multi-client support
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.
This commit is contained in:
@@ -49,18 +49,10 @@ exports[`createDefault*InboundSettings factories > vmess 1`] = `
|
||||
|
||||
exports[`createDefault*InboundSettings factories > wireguard 1`] = `
|
||||
{
|
||||
"clients": [],
|
||||
"mtu": 1420,
|
||||
"noKernelTun": false,
|
||||
"peers": [
|
||||
{
|
||||
"allowedIPs": [
|
||||
"10.0.0.2/32",
|
||||
],
|
||||
"keepAlive": 0,
|
||||
"privateKey": "cGVlci1maXh0dXJlLXByaXZhdGUta2V5LWZvci10ZXN0cw==",
|
||||
"publicKey": "RNa/H++60PStnhoiiU/vIuwFimZUBuIkLkbrmEoDz34=",
|
||||
},
|
||||
],
|
||||
"peers": [],
|
||||
"secretKey": "QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=",
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -608,6 +608,7 @@ exports[`InboundSchema (full) fixtures > parses wireguard-server byte-stably 1`]
|
||||
"protocol": "wireguard",
|
||||
"remark": "wg-server",
|
||||
"settings": {
|
||||
"clients": [],
|
||||
"mtu": 1420,
|
||||
"noKernelTun": false,
|
||||
"peers": [
|
||||
|
||||
@@ -207,6 +207,7 @@ exports[`InboundSettingsSchema fixtures > parses wireguard-basic byte-stably 1`]
|
||||
{
|
||||
"protocol": "wireguard",
|
||||
"settings": {
|
||||
"clients": [],
|
||||
"mtu": 1420,
|
||||
"noKernelTun": false,
|
||||
"peers": [
|
||||
|
||||
@@ -142,10 +142,11 @@ describe('createDefault*InboundSettings factories', () => {
|
||||
it('wireguard', () => {
|
||||
const s = createDefaultWireguardInboundSettings({
|
||||
secretKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
|
||||
peerPrivateKey: 'cGVlci1maXh0dXJlLXByaXZhdGUta2V5LWZvci10ZXN0cw==',
|
||||
});
|
||||
expect(s).toMatchSnapshot();
|
||||
expect(WireguardInboundSettingsSchema.parse(s)).toEqual(s);
|
||||
expect(s.peers).toEqual([]);
|
||||
expect(s.clients).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { genWireguardConfigs, genWireguardLinks } from '@/lib/xray/inbound-link';
|
||||
import { InboundSchema } from '@/schemas/api/inbound';
|
||||
|
||||
// Multi-client WireGuard renders one link/config per entry in settings.clients
|
||||
// (the canonical store), not settings.peers. Each client carries its own
|
||||
// privateKey + allowedIPs; the server public key is derived from secretKey.
|
||||
function wgInbound() {
|
||||
return InboundSchema.parse({
|
||||
id: 90,
|
||||
remark: 'wg-mc',
|
||||
port: 51820,
|
||||
protocol: 'wireguard',
|
||||
settings: {
|
||||
mtu: 1420,
|
||||
secretKey: 'iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=',
|
||||
peers: [],
|
||||
clients: [
|
||||
{
|
||||
email: 'alice',
|
||||
privateKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
|
||||
publicKey: 'DGSYIcEKAUkA7HhzGSjxLZuV67BR3LeyU0BMLJzNVHQ=',
|
||||
allowedIPs: ['10.0.0.2/32'],
|
||||
keepAlive: 25,
|
||||
},
|
||||
{
|
||||
email: 'bob',
|
||||
privateKey: 'aGVsbG8td29ybGQtdGVzdC1wcml2YXRlLWtleS1ub3chIQ==',
|
||||
publicKey: 'b3RoZXItcHVibGljLWtleS1mb3ItYm9iLXRlc3QtdmFsISE=',
|
||||
allowedIPs: ['10.0.0.3/32'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('wireguard multi-client link/config fan-out', () => {
|
||||
it('emits one link per client from settings.clients', () => {
|
||||
const out = genWireguardLinks({
|
||||
inbound: wgInbound(),
|
||||
remark: 'wg-mc',
|
||||
fallbackHostname: 'wg.example.test',
|
||||
});
|
||||
const links = out.split('\r\n').filter(Boolean);
|
||||
expect(links).toHaveLength(2);
|
||||
expect(links[0]).toContain('wireguard://');
|
||||
expect(links[0]).toContain('address=10.0.0.2%2F32');
|
||||
expect(links[1]).toContain('address=10.0.0.3%2F32');
|
||||
});
|
||||
|
||||
it('emits one .conf per client with its own address', () => {
|
||||
const out = genWireguardConfigs({
|
||||
inbound: wgInbound(),
|
||||
remark: 'wg-mc',
|
||||
fallbackHostname: 'wg.example.test',
|
||||
});
|
||||
const configs = out.split('\r\n[Interface]').length;
|
||||
expect(out).toContain('Address = 10.0.0.2/32');
|
||||
expect(out).toContain('Address = 10.0.0.3/32');
|
||||
expect(configs).toBe(2);
|
||||
});
|
||||
|
||||
it('falls back to settings.peers for legacy single-config inbounds', () => {
|
||||
const legacy = InboundSchema.parse({
|
||||
id: 91,
|
||||
remark: 'wg-legacy',
|
||||
port: 51820,
|
||||
protocol: 'wireguard',
|
||||
settings: {
|
||||
secretKey: 'iJ2cBkrSGqRwIfYIDIxk7hr5RXfdR93MfJUL7yqkkH8=',
|
||||
peers: [
|
||||
{
|
||||
privateKey: 'QGVlb2dXc1ZTWGw0ZXBzZndsWmtMaUM5MUlNYjBHWFdYbz0=',
|
||||
publicKey: 'DGSYIcEKAUkA7HhzGSjxLZuV67BR3LeyU0BMLJzNVHQ=',
|
||||
allowedIPs: ['10.0.0.9/32'],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const out = genWireguardLinks({ inbound: legacy, remark: 'wg-legacy', fallbackHostname: 'wg.example.test' });
|
||||
const links = out.split('\r\n').filter(Boolean);
|
||||
expect(links).toHaveLength(1);
|
||||
expect(links[0]).toContain('address=10.0.0.9%2F32');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user