feat(wireguard): WireGuard peer manager UI — QR codes, .conf download, per-client exit node selection (#42)
* feat(wireguard): WireGuard peer manager UI with QR, .conf download, and per-client exit node selection
- models/wg_peer.js — Redis-backed peer store with auto IP allocation (10.100.0.x)
- models/wg_site.js — Redis-backed exit node store (admin-managed sites)
- utils/wg_keys.js — X25519 keypair gen via Node crypto (no wg binary needed)
- utils/wg_conf.js — client wg0.conf renderer
- routes/wireguard.js — REST API: CRUD sites/peers, GET /conf, GET /qr (QRCode PNG)
- views/wireguard.ejs — full dark-mode UI: exit node table, peer table, QR modal,
.conf download, exit node picker per client
- conf/base.js — conf.wireguard block (serverPublicKey, serverEndpoint, dns, poolBase)
- Nav: WireGuard link added (admin-gated)
- No wg binary dep in Node process — key gen is pure JS X25519
* fix(test): update test script to run unit tests without native bcrypt binary dependency in CI
* fix(ui): replace native browser alert/confirm with app.messages in WireGuard view
This commit is contained in:
@@ -37,6 +37,7 @@ module.exports = {
|
||||
nav: [
|
||||
{href: '/dashboard', icon: 'fa-solid fa-gauge-high', label: 'Dashboard', groups: []},
|
||||
{href: '/sessions', icon: 'fa-solid fa-plug-circle-bolt', label: 'Sessions', groups: []},
|
||||
{href: '/wireguard', icon: 'fa-solid fa-shield-halved', label: 'WireGuard', groups: ['admin', 'app_jump_admin']},
|
||||
{href: '/audit', icon: 'fa-solid fa-clipboard-list', label: 'Audit', groups: ['admin', 'app_jump_admin']},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
// Renders a WireGuard client wg0.conf from a peer + site record.
|
||||
//
|
||||
// The generated config is a standard WireGuard config that works with:
|
||||
// - wg-quick (Linux / macOS)
|
||||
// - the official WireGuard iOS / Android apps (via QR code)
|
||||
// - TunnelBear, WireGuard Windows client, etc.
|
||||
|
||||
/**
|
||||
* Build a client wg0.conf string.
|
||||
*
|
||||
* @param {object} peer - WG peer record from wg_peer model
|
||||
* @param {object} site - Exit node record from wg_site model (may be null)
|
||||
* @param {string} serverPub - Gateway server's own public key
|
||||
* @param {string} serverEndpoint - "host:port" for the gateway
|
||||
* @param {string} dns - Optional DNS server to push to client
|
||||
* @returns {string}
|
||||
*/
|
||||
function renderClientConf({ peer, site, serverPub, serverEndpoint, dns }) {
|
||||
const allowedIPs = site
|
||||
? (site.exitAll ? '0.0.0.0/0, ::/0' : site.subnet || '0.0.0.0/0')
|
||||
: '0.0.0.0/0, ::/0'; // no exit site = full tunnel
|
||||
|
||||
const lines = [
|
||||
'[Interface]',
|
||||
`PrivateKey = ${peer.privateKey}`,
|
||||
`Address = ${peer.assignedIP}/32`,
|
||||
];
|
||||
|
||||
if (dns) lines.push(`DNS = ${dns}`);
|
||||
|
||||
lines.push('');
|
||||
lines.push('[Peer]');
|
||||
lines.push(`PublicKey = ${serverPub}`);
|
||||
|
||||
// If client selected an exit site, add preshared key routing hint via
|
||||
// AllowedIPs. Site gateways are peers-of-peers; the server handles routing.
|
||||
if (site) {
|
||||
lines.push(`AllowedIPs = ${allowedIPs}`);
|
||||
} else {
|
||||
lines.push('AllowedIPs = 0.0.0.0/0, ::/0');
|
||||
}
|
||||
|
||||
lines.push(`Endpoint = ${serverEndpoint}`);
|
||||
lines.push('PersistentKeepalive = 25');
|
||||
|
||||
if (peer.note) {
|
||||
lines.unshift(`# ${peer.note}`);
|
||||
}
|
||||
|
||||
return lines.join('\n') + '\n';
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a minimal server-side [Peer] block for wg0.conf (for reference/export).
|
||||
*/
|
||||
function renderServerPeerBlock(peer) {
|
||||
return [
|
||||
`# ${peer.name || peer.id}`,
|
||||
'[Peer]',
|
||||
`PublicKey = ${peer.publicKey}`,
|
||||
`AllowedIPs = ${peer.assignedIP}/32`,
|
||||
'',
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
module.exports = { renderClientConf, renderServerPeerBlock };
|
||||
@@ -0,0 +1,29 @@
|
||||
'use strict';
|
||||
|
||||
// WireGuard key generation using Node's built-in crypto (X25519).
|
||||
// WireGuard keys ARE X25519 keys in raw base64 — no wg binary needed.
|
||||
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Generate a WireGuard keypair.
|
||||
* @returns {{ privateKey: string, publicKey: string }} — base64 encoded
|
||||
*/
|
||||
function generateKeypair() {
|
||||
const { privateKey, publicKey } = crypto.generateKeyPairSync('x25519', {
|
||||
publicKeyEncoding: { type: 'spki', format: 'der' },
|
||||
privateKeyEncoding: { type: 'pkcs8', format: 'der' },
|
||||
});
|
||||
|
||||
// DER-encoded PKCS#8 private key: raw 32-byte X25519 scalar starts at offset 16
|
||||
const rawPriv = privateKey.slice(16, 48);
|
||||
// DER-encoded SPKI public key: raw 32-byte point starts at offset 12
|
||||
const rawPub = publicKey.slice(12, 44);
|
||||
|
||||
return {
|
||||
privateKey: rawPriv.toString('base64'),
|
||||
publicKey: rawPub.toString('base64'),
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { generateKeypair };
|
||||
Reference in New Issue
Block a user