fix: resolve multi-server iptables bridging and hook audit logging
This commit is contained in:
parent
dd8dd605f1
commit
f9d687c131
2 changed files with 156 additions and 67 deletions
|
|
@ -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,11 +355,15 @@ 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}`;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue