From f9d687c131c072e77cad2162dfd3548025e32854 Mon Sep 17 00:00:00 2001 From: xtcnet Date: Sun, 8 Mar 2026 10:58:19 +0700 Subject: [PATCH] fix: resolve multi-server iptables bridging and hook audit logging --- backend/internal/wireguard.js | 164 ++++++++++++++++++++-------------- backend/routes/wireguard.js | 59 ++++++++++++ 2 files changed, 156 insertions(+), 67 deletions(-) diff --git a/backend/internal/wireguard.js b/backend/internal/wireguard.js index d4aaf4f..5689e68 100644 --- a/backend/internal/wireguard.js +++ b/backend/internal/wireguard.js @@ -87,86 +87,100 @@ const internalWireguard = { }, /** - * Save WireGuard config to /etc/wireguard/wg0.conf and sync + * Save WireGuard config to /etc/wireguard/wgX.conf and sync */ async saveConfig(knex) { - const iface = await this.getOrCreateInterface(knex); + await this.getOrCreateInterface(knex); // Ensure at least wg0 exists + + const ifaces = await knex("wg_interface").select("*"); const clients = await knex("wg_client").where("enabled", true); - // Generate server interface section - const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr); - const serverAddress = `${parsed.firstHost}/${parsed.prefix}`; + for (const iface of ifaces) { + // 1. Render IPTables Rules dynamically for this interface + const { postUp, postDown } = await this.renderIptablesRules(knex, iface); - let configContent = wgHelpers.generateServerInterface({ - privateKey: iface.private_key, - address: serverAddress, - listenPort: iface.listen_port, - mtu: iface.mtu, - dns: null, // DNS is for clients, not server - postUp: iface.post_up, - postDown: iface.post_down, - }); + // 2. Generate server interface section + const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr); + const serverAddress = `${parsed.firstHost}/${parsed.prefix}`; - // Generate peer sections for each enabled client - for (const client of clients) { - configContent += "\n\n" + wgHelpers.generateServerPeer({ - publicKey: client.public_key, - preSharedKey: client.pre_shared_key, - allowedIps: `${client.ipv4_address}/32`, + let configContent = wgHelpers.generateServerInterface({ + privateKey: iface.private_key, + address: serverAddress, + listenPort: iface.listen_port, + mtu: iface.mtu, + dns: null, // DNS is for clients, not server + postUp: postUp, + postDown: postDown, }); - } - configContent += "\n"; + // 3. Generate peer sections for each enabled client ON THIS SERVER + const ifaceClients = clients.filter(c => c.interface_id === iface.id); + for (const client of ifaceClients) { + configContent += "\n\n" + wgHelpers.generateServerPeer({ + publicKey: client.public_key, + preSharedKey: client.pre_shared_key, + allowedIps: `${client.ipv4_address}/32`, + }); + } - // Write config file - const configPath = `${WG_CONFIG_DIR}/${iface.name}.conf`; - fs.writeFileSync(configPath, configContent, { mode: 0o600 }); - logger.info(`WireGuard config saved to ${configPath}`); + configContent += "\n"; - // Sync config - try { - await wgHelpers.wgSync(iface.name); - logger.info("WireGuard config synced"); - } catch (err) { - logger.warn("WireGuard sync failed, may need full restart:", err.message); + // 4. Write config file + const configPath = `${WG_CONFIG_DIR}/${iface.name}.conf`; + fs.writeFileSync(configPath, configContent, { mode: 0o600 }); + logger.info(`WireGuard config saved to ${configPath}`); + + // 5. Sync config + try { + await wgHelpers.wgSync(iface.name); + logger.info(`WireGuard config synced for ${iface.name}`); + } catch (err) { + logger.warn(`WireGuard sync failed for ${iface.name}, may need full restart:`, err.message); + } } }, /** - * Start WireGuard interface + * Start WireGuard interfaces */ async startup(knex) { try { - const iface = await this.getOrCreateInterface(knex); + await this.getOrCreateInterface(knex); // ensure at least wg0 // Ensure config dir exists if (!fs.existsSync(WG_CONFIG_DIR)) { fs.mkdirSync(WG_CONFIG_DIR, { recursive: true }); } - // Save config first + // Save configs first (generates .conf files dynamically for all wg_interfaces) await this.saveConfig(knex); - // Bring down if already up, then up - try { - await wgHelpers.wgDown(iface.name); - } catch (_) { - // Ignore if not up - } + // Bring down/up all interfaces sequentially + const ifaces = await knex("wg_interface").select("name", "listen_port"); + for (const iface of ifaces) { + try { + await wgHelpers.wgDown(iface.name); + } catch (_) { + // Ignore if not up + } - await wgHelpers.wgUp(iface.name); - logger.info(`WireGuard interface ${iface.name} started on port ${iface.listen_port}`); + try { + await wgHelpers.wgUp(iface.name); + logger.info(`WireGuard interface ${iface.name} started on port ${iface.listen_port}`); + } catch (err) { + logger.error(`WireGuard startup failed for ${iface.name}:`, err.message); + } + } // Start cron job for expiration this.startCronJob(knex); } catch (err) { - logger.error("WireGuard startup failed:", err.message); - logger.warn("WireGuard features will be unavailable. Ensure the host supports WireGuard kernel module."); + logger.error("WireGuard startup failed overall:", err.message); } }, /** - * Shutdown WireGuard interface + * Shutdown WireGuard interfaces */ async shutdown(knex) { if (cronTimer) { @@ -174,26 +188,35 @@ const internalWireguard = { cronTimer = null; } try { - const iface = await knex("wg_interface").first(); - if (iface) { - await wgHelpers.wgDown(iface.name); - logger.info(`WireGuard interface ${iface.name} stopped`); + const ifaces = await knex("wg_interface").select("name"); + for (const iface of ifaces) { + try { + await wgHelpers.wgDown(iface.name); + logger.info(`WireGuard interface ${iface.name} stopped`); + } catch (err) { + logger.warn(`WireGuard shutdown warning for ${iface.name}:`, err.message); + } } } catch (err) { - logger.warn("WireGuard shutdown warning:", err.message); + logger.error("WireGuard shutdown failed querying DB:", err.message); } }, /** - * Get all clients with live status + * Get all clients with live status and interface name correlation */ async getClients(knex) { - const iface = await this.getOrCreateInterface(knex); - const dbClients = await knex("wg_client").orderBy("created_on", "desc"); + await this.getOrCreateInterface(knex); // Ensure structure exists + + const dbClients = await knex("wg_client") + .join("wg_interface", "wg_client.interface_id", "=", "wg_interface.id") + .select("wg_client.*", "wg_interface.name as interface_name") + .orderBy("wg_client.created_on", "desc"); const clients = dbClients.map((c) => ({ id: c.id, name: c.name, + interfaceName: c.interface_name, enabled: c.enabled === 1 || c.enabled === true, ipv4_address: c.ipv4_address, public_key: c.public_key, @@ -209,20 +232,23 @@ const internalWireguard = { transfer_tx: 0, })); - // Get live WireGuard status - try { - const dump = await wgHelpers.wgDump(iface.name); - for (const peer of dump) { - const client = clients.find((c) => c.public_key === peer.publicKey); - if (client) { - client.latest_handshake_at = peer.latestHandshakeAt; - client.endpoint = peer.endpoint; - client.transfer_rx = peer.transferRx; - client.transfer_tx = peer.transferTx; + // Get live WireGuard status from ALL interfaces + const ifaces = await knex("wg_interface").select("name"); + for (const iface of ifaces) { + try { + const dump = await wgHelpers.wgDump(iface.name); + for (const peer of dump) { + const client = clients.find((c) => c.public_key === peer.publicKey); + if (client) { + client.latest_handshake_at = peer.latestHandshakeAt; + client.endpoint = peer.endpoint; + client.transfer_rx = peer.transferRx; + client.transfer_tx = peer.transferTx; + } } + } catch (_) { + // WireGuard might be off or particular interface fails } - } catch (_) { - // WireGuard may not be running } return clients; @@ -329,11 +355,15 @@ const internalWireguard = { * Get client configuration file content */ async getClientConfiguration(knex, clientId) { - const iface = await this.getOrCreateInterface(knex); const client = await knex("wg_client").where("id", clientId).first(); if (!client) { throw new Error("Client not found"); } + + const iface = await knex("wg_interface").where("id", client.interface_id).first(); + if (!iface) { + throw new Error("Interface not found for this client"); + } const endpoint = `${iface.host || "YOUR_SERVER_IP"}:${iface.listen_port}`; diff --git a/backend/routes/wireguard.js b/backend/routes/wireguard.js index a685c1c..e6c876a 100644 --- a/backend/routes/wireguard.js +++ b/backend/routes/wireguard.js @@ -1,5 +1,7 @@ import express from "express"; import internalWireguard from "../internal/wireguard.js"; +import internalAuditLog from "../internal/audit-log.js"; +import jwtdecode from "../lib/express/jwt-decode.js"; import db from "../db.js"; const router = express.Router({ @@ -8,6 +10,9 @@ const router = express.Router({ mergeParams: true, }); +// Protect all WireGuard routes +router.use(jwtdecode()); + /** * GET /api/wireguard * Get WireGuard interfaces info @@ -30,6 +35,12 @@ router.post("/", async (req, res, next) => { try { const knex = db(); const iface = await internalWireguard.createInterface(knex, req.body); + await internalAuditLog.add(res.locals.access, { + action: "created", + object_type: "wireguard-server", + object_id: iface.id, + meta: req.body, + }); res.status(201).json(iface); } catch (err) { next(err); @@ -44,6 +55,12 @@ router.put("/:id", async (req, res, next) => { try { const knex = db(); const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body); + await internalAuditLog.add(res.locals.access, { + action: "updated", + object_type: "wireguard-server", + object_id: iface.id, + meta: req.body, + }); res.status(200).json(iface); } catch (err) { next(err); @@ -58,6 +75,12 @@ router.delete("/:id", async (req, res, next) => { try { const knex = db(); const result = await internalWireguard.deleteInterface(knex, req.params.id); + await internalAuditLog.add(res.locals.access, { + action: "deleted", + object_type: "wireguard-server", + object_id: req.params.id, + meta: {}, + }); res.status(200).json(result); } catch (err) { next(err); @@ -72,6 +95,12 @@ router.post("/:id/links", async (req, res, next) => { try { const knex = db(); const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || []); + await internalAuditLog.add(res.locals.access, { + action: "updated", + object_type: "wireguard-server-links", + object_id: req.params.id, + meta: req.body, + }); res.status(200).json(result); } catch (err) { next(err); @@ -100,6 +129,12 @@ router.post("/client", async (req, res, next) => { try { const knex = db(); const client = await internalWireguard.createClient(knex, req.body); + await internalAuditLog.add(res.locals.access, { + action: "created", + object_type: "wireguard-client", + object_id: client.id, + meta: req.body, + }); res.status(201).json(client); } catch (err) { next(err); @@ -131,6 +166,12 @@ router.put("/client/:id", async (req, res, next) => { try { const knex = db(); const client = await internalWireguard.updateClient(knex, req.params.id, req.body); + await internalAuditLog.add(res.locals.access, { + action: "updated", + object_type: "wireguard-client", + object_id: client.id, + meta: req.body, + }); res.status(200).json(client); } catch (err) { next(err); @@ -145,6 +186,12 @@ router.delete("/client/:id", async (req, res, next) => { try { const knex = db(); const result = await internalWireguard.deleteClient(knex, req.params.id); + await internalAuditLog.add(res.locals.access, { + action: "deleted", + object_type: "wireguard-client", + object_id: req.params.id, + meta: {}, + }); res.status(200).json(result); } catch (err) { next(err); @@ -159,6 +206,12 @@ router.post("/client/:id/enable", async (req, res, next) => { try { const knex = db(); const client = await internalWireguard.toggleClient(knex, req.params.id, true); + await internalAuditLog.add(res.locals.access, { + action: "enabled", + object_type: "wireguard-client", + object_id: client.id, + meta: {}, + }); res.status(200).json(client); } catch (err) { next(err); @@ -173,6 +226,12 @@ router.post("/client/:id/disable", async (req, res, next) => { try { const knex = db(); const client = await internalWireguard.toggleClient(knex, req.params.id, false); + await internalAuditLog.add(res.locals.access, { + action: "disabled", + object_type: "wireguard-client", + object_id: client.id, + meta: {}, + }); res.status(200).json(client); } catch (err) { next(err);