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) {
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}`;

View file

@ -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);