fix(vpn-manager): persist WireGuard private keys for valid client exports and QR codes

This commit is contained in:
2026-03-31 00:08:54 +00:00
parent 6efd986406
commit ab4310b775
4 changed files with 29 additions and 6 deletions

View File

@@ -1,5 +1,11 @@
# Changelog # Changelog
## 2026-03-31 - 11.20.1 - fix(vpn-manager)
persist WireGuard private keys for valid client exports and QR codes
- Store each client's WireGuard private key when creating and rotating keys.
- Inject the stored private key into exported WireGuard configs so generated configs are complete and scannable.
## 2026-03-30 - 11.20.0 - feat(vpn-ui) ## 2026-03-30 - 11.20.0 - feat(vpn-ui)
add QR code export for WireGuard client configurations add QR code export for WireGuard client configurations

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '11.20.0', version: '11.20.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }

View File

@@ -46,6 +46,8 @@ interface IPersistedClient {
assignedIp?: string; assignedIp?: string;
noisePublicKey: string; noisePublicKey: string;
wgPublicKey: string; wgPublicKey: string;
/** WireGuard private key — stored so exports and QR codes produce valid configs */
wgPrivateKey?: string;
createdAt: number; createdAt: number;
updatedAt: number; updatedAt: number;
expiresAt?: string; expiresAt?: string;
@@ -188,7 +190,7 @@ export class VpnManager {
description: opts.description, description: opts.description,
}); });
// Persist client entry (without private keys) // Persist client entry (including WG private key for export/QR)
const persisted: IPersistedClient = { const persisted: IPersistedClient = {
clientId: bundle.entry.clientId, clientId: bundle.entry.clientId,
enabled: bundle.entry.enabled ?? true, enabled: bundle.entry.enabled ?? true,
@@ -197,6 +199,7 @@ export class VpnManager {
assignedIp: bundle.entry.assignedIp, assignedIp: bundle.entry.assignedIp,
noisePublicKey: bundle.entry.publicKey, noisePublicKey: bundle.entry.publicKey,
wgPublicKey: bundle.entry.wgPublicKey || '', wgPublicKey: bundle.entry.wgPublicKey || '',
wgPrivateKey: bundle.secrets?.wgPrivateKey,
createdAt: Date.now(), createdAt: Date.now(),
updatedAt: Date.now(), updatedAt: Date.now(),
expiresAt: bundle.entry.expiresAt, expiresAt: bundle.entry.expiresAt,
@@ -265,11 +268,12 @@ export class VpnManager {
if (!this.vpnServer) throw new Error('VPN server not running'); if (!this.vpnServer) throw new Error('VPN server not running');
const bundle = await this.vpnServer.rotateClientKey(clientId); const bundle = await this.vpnServer.rotateClientKey(clientId);
// Update persisted entry with new public keys // Update persisted entry with new keys (including private key for export/QR)
const client = this.clients.get(clientId); const client = this.clients.get(clientId);
if (client) { if (client) {
client.noisePublicKey = bundle.entry.publicKey; client.noisePublicKey = bundle.entry.publicKey;
client.wgPublicKey = bundle.entry.wgPublicKey || ''; client.wgPublicKey = bundle.entry.wgPublicKey || '';
client.wgPrivateKey = bundle.secrets?.wgPrivateKey;
client.updatedAt = Date.now(); client.updatedAt = Date.now();
await this.persistClient(client); await this.persistClient(client);
} }
@@ -278,11 +282,24 @@ export class VpnManager {
} }
/** /**
* Export a client config (without secrets). * Export a client config. Injects the stored WG private key for complete configs.
*/ */
public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> { public async exportClientConfig(clientId: string, format: 'smartvpn' | 'wireguard'): Promise<string> {
if (!this.vpnServer) throw new Error('VPN server not running'); if (!this.vpnServer) throw new Error('VPN server not running');
return this.vpnServer.exportClientConfig(clientId, format); let config = await this.vpnServer.exportClientConfig(clientId, format);
// Inject stored WG private key so exports produce valid, scannable configs
if (format === 'wireguard') {
const persisted = this.clients.get(clientId);
if (persisted?.wgPrivateKey) {
config = config.replace(
'[Interface]\n',
`[Interface]\nPrivateKey = ${persisted.wgPrivateKey}\n`,
);
}
}
return config;
} }
// ── Tag-based access control ─────────────────────────────────────────── // ── Tag-based access control ───────────────────────────────────────────

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@serve.zone/dcrouter', name: '@serve.zone/dcrouter',
version: '11.20.0', version: '11.20.1',
description: 'A multifaceted routing service handling mail and SMS delivery functions.' description: 'A multifaceted routing service handling mail and SMS delivery functions.'
} }