From 03dd903e07ed7f8f4260c73172d7885a915b22c3 Mon Sep 17 00:00:00 2001 From: William Mantly Date: Sun, 9 Aug 2026 00:08:55 -0400 Subject: [PATCH] feat(wireguard): bootstrap server keypair and default site exit node, fix UI confirmation actionMessage and query token auth (#43) --- nodejs/bin/www | 4 +- nodejs/middleware/auth.js | 3 +- nodejs/package.json | 2 +- nodejs/routes/wireguard.js | 6 + nodejs/services/wg_bootstrap.js | 56 ++ nodejs/test/unit/wireguard.test.js | 42 ++ nodejs/views/wireguard.ejs | 979 +++++++++++------------------ 7 files changed, 488 insertions(+), 604 deletions(-) create mode 100644 nodejs/services/wg_bootstrap.js create mode 100644 nodejs/test/unit/wireguard.test.js diff --git a/nodejs/bin/www b/nodejs/bin/www index 2036ce8..3b91099 100644 --- a/nodejs/bin/www +++ b/nodejs/bin/www @@ -15,8 +15,10 @@ const { Server } = require('socket.io'); // createOidcClient), so the fetch MUST resolve before require('../models'). // Fail-soft: if OpenBao is unreachable, init() leaves conf as the file-loaded // fallback and boot continues from ./config/jump-secrets.js. -require('@simpleworkjs/bao-conf').init({ path: 'jump-host', conf }).then(() => { +require('@simpleworkjs/bao-conf').init({ path: 'jump-host', conf }).then(async () => { require('../models'); + const { bootstrapWireguard } = require('../services/wg_bootstrap'); + await bootstrapWireguard(); const app = require('../app'); const middleware = require('../middleware/auth'); diff --git a/nodejs/middleware/auth.js b/nodejs/middleware/auth.js index 11286f8..16e9402 100644 --- a/nodejs/middleware/auth.js +++ b/nodejs/middleware/auth.js @@ -27,7 +27,8 @@ async function auth(req, res, next){ return next(); } - req.token = await Auth.checkToken(req.header('auth-token')); + const tokStr = req.header('auth-token') || req.query.token; + req.token = await Auth.checkToken(tokStr); req.user = req.token.user; req.groups = typeof req.token.groupsArray === 'function' ? req.token.groupsArray() : []; return next(); diff --git a/nodejs/package.json b/nodejs/package.json index fe6238e..ea80097 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -14,7 +14,7 @@ "scripts": { "start": "node ./bin/www", "dev": "npx nodemon --ignore public/ ./bin/www", - "test": "NODE_ENV=test node --test --test-force-exit test/unit/access.test.js test/unit/host_keys.test.js test/unit/no_native_dialogs.test.js test/unit/target_match.test.js test/unit/username_grammar.test.js", + "test": "NODE_ENV=test node --test --test-force-exit test/unit/access.test.js test/unit/host_keys.test.js test/unit/no_native_dialogs.test.js test/unit/target_match.test.js test/unit/username_grammar.test.js test/unit/wireguard.test.js", "test:unit": "NODE_ENV=test node --test --test-force-exit test/unit/*.test.js", "test:integration": "NODE_ENV=test node --test --test-force-exit test/integration/*.test.js" }, diff --git a/nodejs/routes/wireguard.js b/nodejs/routes/wireguard.js index 45e980c..6a5175f 100644 --- a/nodejs/routes/wireguard.js +++ b/nodejs/routes/wireguard.js @@ -35,6 +35,12 @@ function gwConf() { }; } +// ── Gateway Info ───────────────────────────────────────────────────────────── + +router.get('/gateway-info', async (req, res) => { + res.json(gwConf()); +}); + // ── Sites ─────────────────────────────────────────────────────────────────── router.get('/sites', async (req, res, next) => { diff --git a/nodejs/services/wg_bootstrap.js b/nodejs/services/wg_bootstrap.js new file mode 100644 index 0000000..57338fe --- /dev/null +++ b/nodejs/services/wg_bootstrap.js @@ -0,0 +1,56 @@ +'use strict'; + +const conf = require('@simpleworkjs/conf'); +const { getRedis } = require('../models'); +const { generateKeypair } = require('../utils/wg_keys'); +const wgSite = require('../models/wg_site'); + +async function bootstrapWireguard() { + try { + const redis = await getRedis(); + const P = conf.redis.prefix || ''; + const keypairKey = `${P}wg_gateway_keypair`; + + // 1. Ensure Gateway WireGuard Keypair + let keypairData = await redis.hGetAll(keypairKey); + if (!keypairData || !keypairData.publicKey) { + const kp = generateKeypair(); + keypairData = { + privateKey: kp.privateKey, + publicKey: kp.publicKey, + createdAt: String(Date.now()), + }; + await redis.hSet(keypairKey, keypairData); + console.log('[bootstrap] Generated fresh WireGuard Gateway keypair.'); + } + + if (!conf.wireguard) conf.wireguard = {}; + conf.wireguard.serverPublicKey = keypairData.publicKey; + conf.wireguard.serverPrivateKey = keypairData.privateKey; + + if (!conf.wireguard.serverEndpoint) { + const domain = conf.domain || 'suite.vm42.us'; + conf.wireguard.serverEndpoint = `${domain}:51820`; + } + + // 2. Ensure Default Site Exit Node ("This Site") + const existingSites = await wgSite.list().catch(() => []); + if (!existingSites || existingSites.length === 0) { + const siteName = conf.siteName || process.env.CFG_SITE_NAME || '718it'; + const defaultSite = await wgSite.create({ + name: `${siteName} (This Site)`, + endpoint: conf.wireguard.serverEndpoint, + publicKey: keypairData.publicKey, + subnet: '0.0.0.0/0', + exitAll: true, + siteId: siteName, + note: 'Default local site exit node initialized during bootstrap', + }, 'bootstrap'); + console.log(`[bootstrap] Initialized default WireGuard exit node '${defaultSite.name}' (${defaultSite.id}).`); + } + } catch (err) { + console.error('[bootstrap] WireGuard bootstrap error:', err.message); + } +} + +module.exports = { bootstrapWireguard }; diff --git a/nodejs/test/unit/wireguard.test.js b/nodejs/test/unit/wireguard.test.js new file mode 100644 index 0000000..c900787 --- /dev/null +++ b/nodejs/test/unit/wireguard.test.js @@ -0,0 +1,42 @@ +'use strict'; + +const test = require('node:test'); +const assert = require('node:assert/strict'); +const { generateKeypair } = require('../../utils/wg_keys'); +const { renderClientConf } = require('../../utils/wg_conf'); + +test('generateKeypair returns valid base64 WireGuard keypair', () => { + const kp = generateKeypair(); + assert.ok(kp.privateKey, 'privateKey should exist'); + assert.ok(kp.publicKey, 'publicKey should exist'); + assert.notEqual(kp.privateKey, kp.publicKey); + // WireGuard raw X25519 base64 keys are 44 characters ending with '=' + assert.equal(kp.privateKey.length, 44); + assert.equal(kp.publicKey.length, 44); +}); + +test('renderClientConf generates valid wg0 client configuration', () => { + const peer = { + name: 'test-phone', + assignedIP: '10.100.0.5', + privateKey: 'c3VwZXJzZWNyZXRwcml2YXRla2V5MTIzNDU2Nzg5MDE=', + }; + const site = { + subnet: '192.168.1.0/24', + exitAll: false, + }; + const confStr = renderClientConf({ + peer, + site, + serverPub: 'c2VydmVycHVibGlja2V5MTIzNDU2Nzg5MDEyMzQ1Njc=', + serverEndpoint: 'gw.theta42.com:51820', + dns: '1.1.1.1', + }); + + assert.ok(confStr.includes('[Interface]')); + assert.ok(confStr.includes('PrivateKey = c3VwZXJzZWNyZXRwcml2YXRla2V5MTIzNDU2Nzg5MDE=')); + assert.ok(confStr.includes('Address = 10.100.0.5/32')); + assert.ok(confStr.includes('[Peer]')); + assert.ok(confStr.includes('PublicKey = c2VydmVycHVibGlja2V5MTIzNDU2Nzg5MDEyMzQ1Njc=')); + assert.ok(confStr.includes('Endpoint = gw.theta42.com:51820')); +}); diff --git a/nodejs/views/wireguard.ejs b/nodejs/views/wireguard.ejs index 014e4f2..c072934 100644 --- a/nodejs/views/wireguard.ejs +++ b/nodejs/views/wireguard.ejs @@ -1,679 +1,456 @@ <%- include('top') %> + - - -
- - -
-
-
-

WireGuard Mesh

-

Manage client profiles, exit nodes, and VPN tunnels

-
-
- - -
-
- Gateway Endpoint - loading… -
-
- Server Public Key - loading… -
-
- DNS Pushed to Clients - loading… -
-
- -
- - -
-
-
-

Exit Nodes

- -
- -
-
No exit nodes yet
-
-
-
- - -
-
-
-

Client Peers

- -
- -
-
No peers yet
-
-
-
- -
- -
- - - - -