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:
MHSanaei
2026-06-28 00:44:38 +02:00
parent 33aada0c7c
commit 9c8cd08f90
50 changed files with 2160 additions and 258 deletions
+6 -10
View File
@@ -263,24 +263,20 @@ export interface WireguardInboundSeed {
mtu?: number;
secretKey?: string;
noKernelTun?: boolean;
peerPrivateKey?: string;
}
// WireGuard is multi-client now: a new inbound holds only the server identity
// (secretKey/mtu) and starts with no clients. Clients (peers) are added later
// through the client modal, which generates each one's keypair and a unique
// tunnel address. peers stays empty for backward-compatible parsing.
export function createDefaultWireguardInboundSettings(
seed: WireguardInboundSeed = {},
): WireguardInboundSettings {
const peerKp = seed.peerPrivateKey
? { privateKey: seed.peerPrivateKey, publicKey: Wireguard.generateKeypair(seed.peerPrivateKey).publicKey }
: Wireguard.generateKeypair();
return {
mtu: seed.mtu ?? 1420,
secretKey: seed.secretKey ?? Wireguard.generateKeypair().privateKey,
peers: [{
privateKey: peerKp.privateKey,
publicKey: peerKp.publicKey,
allowedIPs: ['10.0.0.2/32'],
keepAlive: 0,
}],
peers: [],
clients: [],
noKernelTun: seed.noKernelTun ?? false,
};
}
@@ -6,6 +6,7 @@ import {
TrojanClientSchema,
VlessClientSchema,
VmessClientSchema,
WireguardClientSchema,
} from '@/schemas/protocols/inbound';
import type { StreamSettings } from '@/schemas/api/inbound';
import type { Sniffing } from '@/schemas/primitives';
@@ -234,6 +235,7 @@ function clientSchemaForProtocol(protocol: string): z.ZodType | null {
case 'trojan': return TrojanClientSchema;
case 'shadowsocks': return ShadowsocksClientSchema;
case 'hysteria': return HysteriaClientSchema;
case 'wireguard': return WireguardClientSchema;
default: return null;
}
}
+23 -4
View File
@@ -1126,14 +1126,30 @@ export interface GenWireguardFanoutInput {
fallbackHostname: string;
}
// WireGuard is multi-client: each client is one accepted peer. The canonical
// store is settings.clients; legacy single-config inbounds (pre-migration) are
// still rendered from settings.peers. Both carry the privateKey/allowedIPs/
// preSharedKey/keepAlive the link and .conf need, so they project to the same
// peer shape and reuse genWireguardLink/genWireguardConfig unchanged.
function wgRenderPeers(settings: WireguardInboundSettings): WireguardInboundPeer[] {
const clients = settings.clients ?? [];
if (clients.length > 0) {
return clients.map((c) => ({ ...c, publicKey: c.publicKey ?? '' }));
}
return settings.peers;
}
export function genWireguardLinks(input: GenWireguardFanoutInput): string {
const { inbound, remark = '', hostOverride = '', fallbackHostname } = input;
if (inbound.protocol !== 'wireguard') return '';
const addr = resolveAddr(inbound, hostOverride, fallbackHostname);
const sep = '-';
return inbound.settings.peers
const baseSettings = inbound.settings as WireguardInboundSettings;
const peers = wgRenderPeers(baseSettings);
const settings: WireguardInboundSettings = { ...baseSettings, peers };
return peers
.map((p, i) => genWireguardLink({
settings: inbound.settings as WireguardInboundSettings,
settings,
address: addr,
port: inbound.port,
remark: `${remark}${sep}${i + 1}${wgPeerCommentSuffix(p)}`,
@@ -1147,9 +1163,12 @@ export function genWireguardConfigs(input: GenWireguardFanoutInput): string {
if (inbound.protocol !== 'wireguard') return '';
const addr = resolveAddr(inbound, hostOverride, fallbackHostname);
const sep = '-';
return inbound.settings.peers
const baseSettings = inbound.settings as WireguardInboundSettings;
const peers = wgRenderPeers(baseSettings);
const settings: WireguardInboundSettings = { ...baseSettings, peers };
return peers
.map((p, i) => genWireguardConfig({
settings: inbound.settings as WireguardInboundSettings,
settings,
address: addr,
port: inbound.port,
remark: `${remark}${sep}${i + 1}${wgPeerCommentSuffix(p)}`,