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) {
|
||||
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,12 +355,16 @@ 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}`;
|
||||
|
||||
return wgHelpers.generateClientConfig({
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue