fix: resolve multi-server iptables bridging and hook audit logging

This commit is contained in:
xtcnet 2026-03-08 10:58:19 +07:00
parent dd8dd605f1
commit f9d687c131
2 changed files with 156 additions and 67 deletions

View file

@ -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) { 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); const clients = await knex("wg_client").where("enabled", true);
// Generate server interface section for (const iface of ifaces) {
const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr); // 1. Render IPTables Rules dynamically for this interface
const serverAddress = `${parsed.firstHost}/${parsed.prefix}`; const { postUp, postDown } = await this.renderIptablesRules(knex, iface);
let configContent = wgHelpers.generateServerInterface({ // 2. Generate server interface section
privateKey: iface.private_key, const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr);
address: serverAddress, const serverAddress = `${parsed.firstHost}/${parsed.prefix}`;
listenPort: iface.listen_port,
mtu: iface.mtu,
dns: null, // DNS is for clients, not server
postUp: iface.post_up,
postDown: iface.post_down,
});
// Generate peer sections for each enabled client let configContent = wgHelpers.generateServerInterface({
for (const client of clients) { privateKey: iface.private_key,
configContent += "\n\n" + wgHelpers.generateServerPeer({ address: serverAddress,
publicKey: client.public_key, listenPort: iface.listen_port,
preSharedKey: client.pre_shared_key, mtu: iface.mtu,
allowedIps: `${client.ipv4_address}/32`, 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 configContent += "\n";
const configPath = `${WG_CONFIG_DIR}/${iface.name}.conf`;
fs.writeFileSync(configPath, configContent, { mode: 0o600 });
logger.info(`WireGuard config saved to ${configPath}`);
// Sync config // 4. Write config file
try { const configPath = `${WG_CONFIG_DIR}/${iface.name}.conf`;
await wgHelpers.wgSync(iface.name); fs.writeFileSync(configPath, configContent, { mode: 0o600 });
logger.info("WireGuard config synced"); logger.info(`WireGuard config saved to ${configPath}`);
} catch (err) {
logger.warn("WireGuard sync failed, may need full restart:", err.message); // 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) { async startup(knex) {
try { try {
const iface = await this.getOrCreateInterface(knex); await this.getOrCreateInterface(knex); // ensure at least wg0
// Ensure config dir exists // Ensure config dir exists
if (!fs.existsSync(WG_CONFIG_DIR)) { if (!fs.existsSync(WG_CONFIG_DIR)) {
fs.mkdirSync(WG_CONFIG_DIR, { recursive: true }); 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); await this.saveConfig(knex);
// Bring down if already up, then up // Bring down/up all interfaces sequentially
try { const ifaces = await knex("wg_interface").select("name", "listen_port");
await wgHelpers.wgDown(iface.name); for (const iface of ifaces) {
} catch (_) { try {
// Ignore if not up await wgHelpers.wgDown(iface.name);
} } catch (_) {
// Ignore if not up
}
await wgHelpers.wgUp(iface.name); try {
logger.info(`WireGuard interface ${iface.name} started on port ${iface.listen_port}`); 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 // Start cron job for expiration
this.startCronJob(knex); this.startCronJob(knex);
} catch (err) { } catch (err) {
logger.error("WireGuard startup failed:", err.message); logger.error("WireGuard startup failed overall:", err.message);
logger.warn("WireGuard features will be unavailable. Ensure the host supports WireGuard kernel module.");
} }
}, },
/** /**
* Shutdown WireGuard interface * Shutdown WireGuard interfaces
*/ */
async shutdown(knex) { async shutdown(knex) {
if (cronTimer) { if (cronTimer) {
@ -174,26 +188,35 @@ const internalWireguard = {
cronTimer = null; cronTimer = null;
} }
try { try {
const iface = await knex("wg_interface").first(); const ifaces = await knex("wg_interface").select("name");
if (iface) { for (const iface of ifaces) {
await wgHelpers.wgDown(iface.name); try {
logger.info(`WireGuard interface ${iface.name} stopped`); 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) { } 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) { async getClients(knex) {
const iface = await this.getOrCreateInterface(knex); await this.getOrCreateInterface(knex); // Ensure structure exists
const dbClients = await knex("wg_client").orderBy("created_on", "desc");
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) => ({ const clients = dbClients.map((c) => ({
id: c.id, id: c.id,
name: c.name, name: c.name,
interfaceName: c.interface_name,
enabled: c.enabled === 1 || c.enabled === true, enabled: c.enabled === 1 || c.enabled === true,
ipv4_address: c.ipv4_address, ipv4_address: c.ipv4_address,
public_key: c.public_key, public_key: c.public_key,
@ -209,20 +232,23 @@ const internalWireguard = {
transfer_tx: 0, transfer_tx: 0,
})); }));
// Get live WireGuard status // Get live WireGuard status from ALL interfaces
try { const ifaces = await knex("wg_interface").select("name");
const dump = await wgHelpers.wgDump(iface.name); for (const iface of ifaces) {
for (const peer of dump) { try {
const client = clients.find((c) => c.public_key === peer.publicKey); const dump = await wgHelpers.wgDump(iface.name);
if (client) { for (const peer of dump) {
client.latest_handshake_at = peer.latestHandshakeAt; const client = clients.find((c) => c.public_key === peer.publicKey);
client.endpoint = peer.endpoint; if (client) {
client.transfer_rx = peer.transferRx; client.latest_handshake_at = peer.latestHandshakeAt;
client.transfer_tx = peer.transferTx; 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; return clients;
@ -329,12 +355,16 @@ const internalWireguard = {
* Get client configuration file content * Get client configuration file content
*/ */
async getClientConfiguration(knex, clientId) { async getClientConfiguration(knex, clientId) {
const iface = await this.getOrCreateInterface(knex);
const client = await knex("wg_client").where("id", clientId).first(); const client = await knex("wg_client").where("id", clientId).first();
if (!client) { if (!client) {
throw new Error("Client not found"); 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}`; const endpoint = `${iface.host || "YOUR_SERVER_IP"}:${iface.listen_port}`;
return wgHelpers.generateClientConfig({ return wgHelpers.generateClientConfig({

View file

@ -1,5 +1,7 @@
import express from "express"; import express from "express";
import internalWireguard from "../internal/wireguard.js"; 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"; import db from "../db.js";
const router = express.Router({ const router = express.Router({
@ -8,6 +10,9 @@ const router = express.Router({
mergeParams: true, mergeParams: true,
}); });
// Protect all WireGuard routes
router.use(jwtdecode());
/** /**
* GET /api/wireguard * GET /api/wireguard
* Get WireGuard interfaces info * Get WireGuard interfaces info
@ -30,6 +35,12 @@ router.post("/", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const iface = await internalWireguard.createInterface(knex, req.body); 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); res.status(201).json(iface);
} catch (err) { } catch (err) {
next(err); next(err);
@ -44,6 +55,12 @@ router.put("/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const iface = await internalWireguard.updateInterface(knex, req.params.id, req.body); 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); res.status(200).json(iface);
} catch (err) { } catch (err) {
next(err); next(err);
@ -58,6 +75,12 @@ router.delete("/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const result = await internalWireguard.deleteInterface(knex, req.params.id); 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); res.status(200).json(result);
} catch (err) { } catch (err) {
next(err); next(err);
@ -72,6 +95,12 @@ router.post("/:id/links", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const result = await internalWireguard.updateInterfaceLinks(knex, req.params.id, req.body.linked_servers || []); 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); res.status(200).json(result);
} catch (err) { } catch (err) {
next(err); next(err);
@ -100,6 +129,12 @@ router.post("/client", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.createClient(knex, req.body); 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); res.status(201).json(client);
} catch (err) { } catch (err) {
next(err); next(err);
@ -131,6 +166,12 @@ router.put("/client/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.updateClient(knex, req.params.id, req.body); 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); res.status(200).json(client);
} catch (err) { } catch (err) {
next(err); next(err);
@ -145,6 +186,12 @@ router.delete("/client/:id", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const result = await internalWireguard.deleteClient(knex, req.params.id); 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); res.status(200).json(result);
} catch (err) { } catch (err) {
next(err); next(err);
@ -159,6 +206,12 @@ router.post("/client/:id/enable", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.toggleClient(knex, req.params.id, true); 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); res.status(200).json(client);
} catch (err) { } catch (err) {
next(err); next(err);
@ -173,6 +226,12 @@ router.post("/client/:id/disable", async (req, res, next) => {
try { try {
const knex = db(); const knex = db();
const client = await internalWireguard.toggleClient(knex, req.params.id, false); 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); res.status(200).json(client);
} catch (err) { } catch (err) {
next(err); next(err);