import fs from "fs"; import { exec } from "child_process"; import { promisify } from "util"; import { global as logger } from "../logger.js"; import * as wgHelpers from "../lib/wg-helpers.js"; import internalWireguardFs from "./wireguard-fs.js"; import internalAuditLog from "./audit-log.js"; const execAsync = promisify(exec); const WG_INTERFACE_NAME = process.env.WG_INTERFACE_NAME || "wg0"; const WG_DEFAULT_PORT = Number.parseInt(process.env.WG_PORT || "51820", 10); const WG_DEFAULT_MTU = Number.parseInt(process.env.WG_MTU || "1420", 10); const WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || "10.8.0.0/24"; const WG_DEFAULT_DNS = process.env.WG_DNS || "1.1.1.1, 8.8.8.8"; const WG_HOST = process.env.WG_HOST || ""; const WG_DEFAULT_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || "0.0.0.0/0, ::/0"; const WG_DEFAULT_PERSISTENT_KEEPALIVE = Number.parseInt(process.env.WG_PERSISTENT_KEEPALIVE || "25", 10); const WG_CONFIG_DIR = "/etc/wireguard"; let cronTimer = null; let connectionMemoryMap = {}; const internalWireguard = { /** * Get or create the WireGuard interface in DB */ async getOrCreateInterface(knex) { let iface = await knex("wg_interface").first(); if (!iface) { const privateKey = await wgHelpers.generatePrivateKey(); const publicKey = await wgHelpers.getPublicKey(privateKey); // Seed a default config if it doesn't exist const insertData = { name: "wg0", private_key: privateKey, public_key: publicKey, listen_port: 51820, ipv4_cidr: "10.0.0.1/24", mtu: 1420, dns: WG_DEFAULT_DNS, host: WG_HOST, post_up: "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", post_down: "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE", created_on: knex.fn.now(), modified_on: knex.fn.now(), }; const [id] = await knex("wg_interface").insert(insertData); iface = await knex("wg_interface").where("id", id).first(); logger.info("WireGuard interface created with default config"); } return iface; }, /** * Render PostUp and PostDown iptables rules based on interface, isolation, and links */ async renderIptablesRules(knex, iface) { const basePostUp = []; const basePostDown = []; // Default forward and NAT basePostUp.push("iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE"); basePostDown.push("iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE"); // Server Isolation: drop cross-server traffic by default. // Uses -I to insert at position 1, before the ACCEPT rules above. basePostUp.push("iptables -I FORWARD -i %i -o wg+ -j DROP"); basePostDown.push("iptables -D FORWARD -i %i -o wg+ -j DROP"); // Same-interface rule: inserted AFTER the DROP above so it lands at position 1, // placing it BEFORE the wg+ DROP in the chain. // This ensures client-to-client traffic on this interface is evaluated first. if (iface.isolate_clients) { basePostUp.push("iptables -I FORWARD -i %i -o %i -j REJECT"); basePostDown.push("iptables -D FORWARD -i %i -o %i -j REJECT"); } else { basePostUp.push("iptables -I FORWARD -i %i -o %i -j ACCEPT"); basePostDown.push("iptables -D FORWARD -i %i -o %i -j ACCEPT"); } // 2. Fetch linked servers to punch holes in the DROP rule // wg_server_link has interface_id_1 and interface_id_2 const links = await knex("wg_server_link") .where("interface_id_1", iface.id) .orWhere("interface_id_2", iface.id); for (const link of links) { const peerIfaceId = link.interface_id_1 === iface.id ? link.interface_id_2 : link.interface_id_1; const peerIface = await knex("wg_interface").where("id", peerIfaceId).first(); if (peerIface) { basePostUp.push(`iptables -I FORWARD -i %i -o ${peerIface.name} -j ACCEPT`); basePostDown.push(`iptables -D FORWARD -i %i -o ${peerIface.name} -j ACCEPT`); } } return { postUp: basePostUp.join("; "), postDown: basePostDown.join("; "), }; }, /** * Save WireGuard config to /etc/wireguard/wgX.conf and sync */ async saveConfig(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); for (const iface of ifaces) { // 1. Render IPTables Rules dynamically for this interface const { postUp, postDown } = await this.renderIptablesRules(knex, iface); // 2. Generate server interface section const parsed = wgHelpers.parseCIDR(iface.ipv4_cidr); const serverAddress = `${parsed.firstHost}/${parsed.prefix}`; 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, }); // 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`, }); } configContent += "\n"; // 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}`); // 6. Apply iptables isolation rule directly (wg syncconf does not run PostUp/PostDown) await this.syncIptablesRules(iface); // 7. Apply traffic control bandwidth partitions non-blocking this.applyBandwidthLimits(knex, iface).catch((e) => logger.warn(`Skipping QoS on ${iface.name}: ${e.message}`)); } catch (err) { logger.warn(`WireGuard sync failed for ${iface.name}, may need full restart:`, err.message); } } }, /** * Start WireGuard interfaces */ async startup(knex) { try { 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 configs first (generates .conf files dynamically for all wg_interfaces) await this.saveConfig(knex); // 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 } 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 overall:", err.message); } }, /** * Shutdown WireGuard interfaces */ async shutdown(knex) { if (cronTimer) { clearInterval(cronTimer); cronTimer = null; } try { 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.error("WireGuard shutdown failed querying DB:", err.message); } }, /** * Get all clients with live status and interface name correlation */ async getClients(knex, access, accessData) { await this.getOrCreateInterface(knex); // Ensure structure exists const query = 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"); if (access) { query.andWhere("wg_client.owner_user_id", access.token.getUserId(1)); } const dbClients = await query; const clients = dbClients.map((c) => ({ id: c.id, name: c.name, interfaceName: c.interface_name, interfaceId: c.interface_id, enabled: c.enabled === 1 || c.enabled === true, ipv4_address: c.ipv4_address, public_key: c.public_key, allowed_ips: c.allowed_ips, persistent_keepalive: c.persistent_keepalive, created_on: c.created_on, updated_on: c.modified_on, expires_at: c.expires_at, // Live status (populated below) latest_handshake_at: null, endpoint: null, transfer_rx: 0, transfer_tx: 0, })); // 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 } } // Inject Storage Utilization Metrics for (const client of clients) { client.storage_usage_bytes = await internalWireguardFs.getClientStorageUsage(client.ipv4_address); } return clients; }, /** * Create a new WireGuard client */ async createClient(knex, data, access, accessData) { const iface = data.interface_id ? await knex("wg_interface").where("id", data.interface_id).first() : await this.getOrCreateInterface(knex); // Generate keys const privateKey = await wgHelpers.generatePrivateKey(); const publicKey = await wgHelpers.getPublicKey(privateKey); const preSharedKey = await wgHelpers.generatePreSharedKey(); // Allocate IP const existingClients = await knex("wg_client").select("ipv4_address").where("interface_id", iface.id); const allocatedIPs = existingClients.map((c) => c.ipv4_address); const ipv4Address = wgHelpers.findNextAvailableIP(iface.ipv4_cidr, allocatedIPs); if (!ipv4Address) { throw new Error("No available IP addresses remaining in this WireGuard server subnet."); } // Scrub any old junk partitions to prevent leakage await internalWireguardFs.deleteClientDir(ipv4Address); const clientData = { name: data.name || "Unnamed Client", enabled: true, ipv4_address: ipv4Address, private_key: privateKey, public_key: publicKey, pre_shared_key: preSharedKey, allowed_ips: data.allowed_ips || WG_DEFAULT_ALLOWED_IPS, persistent_keepalive: data.persistent_keepalive || WG_DEFAULT_PERSISTENT_KEEPALIVE, expires_at: data.expires_at || null, interface_id: iface.id, owner_user_id: access ? access.token.getUserId(1) : 1, created_on: knex.fn.now(), modified_on: knex.fn.now(), }; const [id] = await knex("wg_client").insert(clientData); // Sync WireGuard config await this.saveConfig(knex); return knex("wg_client").where("id", id).first(); }, /** * Delete a WireGuard client */ async deleteClient(knex, clientId, access, accessData) { const query = knex("wg_client").where("id", clientId); if (access) { query.andWhere("owner_user_id", access.token.getUserId(1)); } const client = await query.first(); if (!client) { throw new Error("Client not found"); } await knex("wg_client").where("id", clientId).del(); // Hard-remove the encrypted partition safely mapped to the ipv4_address since it's deleted await internalWireguardFs.deleteClientDir(client.ipv4_address); await this.saveConfig(knex); return { success: true }; }, /** * Toggle a WireGuard client enabled/disabled */ async toggleClient(knex, clientId, enabled, access, accessData) { const query = knex("wg_client").where("id", clientId); if (access) { query.andWhere("owner_user_id", access.token.getUserId(1)); } const client = await query.first(); if (!client) { throw new Error("Client not found"); } await knex("wg_client").where("id", clientId).update({ enabled: enabled, modified_on: knex.fn.now(), }); await this.saveConfig(knex); return knex("wg_client").where("id", clientId).first(); }, /** * Update a WireGuard client */ async updateClient(knex, clientId, data, access, accessData) { const query = knex("wg_client").where("id", clientId); if (access) { query.andWhere("owner_user_id", access.token.getUserId(1)); } const client = await query.first(); if (!client) { throw new Error("Client not found"); } const updateData = {}; if (data.name !== undefined) updateData.name = data.name; if (data.allowed_ips !== undefined) updateData.allowed_ips = data.allowed_ips; if (data.persistent_keepalive !== undefined) updateData.persistent_keepalive = data.persistent_keepalive; if (data.expires_at !== undefined) updateData.expires_at = data.expires_at; updateData.modified_on = knex.fn.now(); await knex("wg_client").where("id", clientId).update(updateData); await this.saveConfig(knex); return knex("wg_client").where("id", clientId).first(); }, /** * Get client configuration file content */ async getClientConfiguration(knex, clientId) { 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({ clientPrivateKey: client.private_key, clientAddress: `${client.ipv4_address}/32`, dns: iface.dns, mtu: iface.mtu, serverPublicKey: iface.public_key, preSharedKey: client.pre_shared_key, allowedIps: client.allowed_ips, persistentKeepalive: client.persistent_keepalive, endpoint: endpoint, }); }, /** * Get QR code SVG for client config */ async getClientQRCode(knex, clientId) { const config = await this.getClientConfiguration(knex, clientId); return wgHelpers.generateQRCodeSVG(config); }, /** * Create a new WireGuard Interface Endpoint */ async createInterface(knex, data, access, accessData) { const existingIfaces = await knex("wg_interface").select("name", "listen_port"); if (existingIfaces.length >= 100) { throw new Error("Maximum limit of 100 WireGuard servers reached."); } // Find the lowest available index between 0 and 99 const usedPorts = new Set(existingIfaces.map(i => i.listen_port)); let newIndex = 0; while (usedPorts.has(51820 + newIndex)) { newIndex++; } const name = `wg${newIndex}`; const listen_port = 51820 + newIndex; // Attempt to grab /24 subnets, ex 10.8.0.0/24 -> 10.8.1.0/24 const ipv4_cidr = `10.8.${newIndex}.1/24`; // Generate keys const privateKey = await wgHelpers.generatePrivateKey(); const publicKey = await wgHelpers.getPublicKey(privateKey); const insertData = { name, private_key: privateKey, public_key: publicKey, listen_port, ipv4_cidr, mtu: data.mtu || WG_DEFAULT_MTU, dns: data.dns || WG_DEFAULT_DNS, host: data.host || WG_HOST, isolate_clients: data.isolate_clients || false, owner_user_id: access ? access.token.getUserId(1) : 1, post_up: "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE", post_down: "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE", created_on: knex.fn.now(), modified_on: knex.fn.now(), }; const [id] = await knex("wg_interface").insert(insertData); const newIface = await knex("wg_interface").where("id", id).first(); // Regenerate config and restart the new interface seamlessly const parsed = wgHelpers.parseCIDR(newIface.ipv4_cidr); let configContent = wgHelpers.generateServerInterface({ privateKey: newIface.private_key, address: `${parsed.firstHost}/${parsed.prefix}`, listenPort: newIface.listen_port, mtu: newIface.mtu, dns: null, postUp: newIface.post_up, postDown: newIface.post_down, }); fs.writeFileSync(`${WG_CONFIG_DIR}/${name}.conf`, configContent, { mode: 0o600 }); await wgHelpers.wgUp(name); return newIface; }, /** * Update an existing Interface */ async updateInterface(knex, id, data, access, accessData) { const query = knex("wg_interface").where("id", id); if (access) { query.andWhere("owner_user_id", access.token.getUserId(1)); } const iface = await query.first(); if (!iface) throw new Error("Interface not found"); const updateData = { modified_on: knex.fn.now() }; if (data.host !== undefined) updateData.host = data.host; if (data.dns !== undefined) updateData.dns = data.dns; if (data.mtu !== undefined) updateData.mtu = data.mtu; if (data.isolate_clients !== undefined) updateData.isolate_clients = data.isolate_clients; await knex("wg_interface").where("id", id).update(updateData); await this.saveConfig(knex); // This will re-render IPTables and sync return knex("wg_interface").where("id", id).first(); }, /** * Delete an interface */ async deleteInterface(knex, id, access, accessData) { const query = knex("wg_interface").where("id", id); if (access) { query.andWhere("owner_user_id", access.token.getUserId(1)); } const iface = await query.first(); if (!iface) throw new Error("Interface not found"); // Prevent deletion of the initial wg0 interface if it's the only one or a critical one if (iface.name === "wg0") { const otherIfaces = await knex("wg_interface").whereNot("id", id); if (otherIfaces.length === 0) { throw new Error("Cannot delete the initial wg0 interface. It is required."); } } try { await wgHelpers.wgDown(iface.name); if (fs.existsSync(`${WG_CONFIG_DIR}/${iface.name}.conf`)) { fs.unlinkSync(`${WG_CONFIG_DIR}/${iface.name}.conf`); } } catch (e) { logger.warn(`Failed to teardown WG interface ${iface.name}: ${e.message}`); } // Pre-emptively Cascade delete all Clients & Partitions tied to this interface const clients = await knex("wg_client").where("interface_id", iface.id); for (const c of clients) { await internalWireguardFs.deleteClientDir(c.ipv4_address); } await knex("wg_client").where("interface_id", iface.id).del(); // Cascading deletion handles links in DB schema await knex("wg_interface").where("id", id).del(); return { success: true }; }, /** * Update Peering Links between WireGuard Interfaces */ async updateInterfaceLinks(knex, id, linkedServers, access, accessData) { // Verify ownership const query = knex("wg_interface").where("id", id); if (access) { query.andWhere("owner_user_id", access.token.getUserId(1)); } const iface = await query.first(); if (!iface) throw new Error("Interface not found"); // Clean up existing links where this interface is involved await knex("wg_server_link").where("interface_id_1", id).orWhere("interface_id_2", id).del(); // Insert new ones for (const peerId of linkedServers) { if (peerId !== Number(id)) { await knex("wg_server_link").insert({ interface_id_1: id, interface_id_2: peerId }); } } await this.saveConfig(knex); return { success: true }; }, /** * Get the WireGuard interfaces info */ async getInterfacesInfo(knex, access, accessData) { const query = knex("wg_interface").select("*"); if (access) { if (accessData.permission_visibility !== "all") { query.andWhere("owner_user_id", access.token.getUserId(1)); } } const ifaces = await query; const allLinks = await knex("wg_server_link").select("*"); const allClients = await knex("wg_client").select("interface_id", "ipv4_address"); const result = []; for (const i of ifaces) { const links = allLinks.filter(l => l.interface_id_1 === i.id || l.interface_id_2 === i.id); const client_count = allClients.filter(c => c.interface_id === i.id).length; let storage_usage_bytes = 0; for (const c of allClients.filter(c => c.interface_id === i.id)) { storage_usage_bytes += await internalWireguardFs.getClientStorageUsage(c.ipv4_address); } result.push({ id: i.id, name: i.name, public_key: i.public_key, ipv4_cidr: i.ipv4_cidr, listen_port: i.listen_port, mtu: i.mtu, dns: i.dns, host: i.host, isolate_clients: i.isolate_clients, linked_servers: links.map(l => l.interface_id_1 === i.id ? l.interface_id_2 : l.interface_id_1), client_count, storage_usage_bytes }); } return result; }, /** * Apply or remove the client isolation iptables rule for an interface. * Called after every wg syncconf because PostUp/PostDown are not re-executed by syncconf. */ async syncIptablesRules(iface) { const name = iface.name; // Remove both possible same-interface rules first (idempotent) await execAsync(`iptables -D FORWARD -i ${name} -o ${name} -j REJECT 2>/dev/null || true`); await execAsync(`iptables -D FORWARD -i ${name} -o ${name} -j ACCEPT 2>/dev/null || true`); // Re-insert at position 1 so it appears before the wg+ DROP rule in the chain if (iface.isolate_clients) { await execAsync(`iptables -I FORWARD -i ${name} -o ${name} -j REJECT`); logger.info(`Client isolation enabled for ${name}`); } else { await execAsync(`iptables -I FORWARD -i ${name} -o ${name} -j ACCEPT`); logger.info(`Client isolation disabled for ${name}`); } }, /** * Run TC Traffic Control QoS limits on a WireGuard Interface (Bytes per sec) */ async applyBandwidthLimits(knex, iface) { const clients = await knex("wg_client").where("interface_id", iface.id).where("enabled", true); const cmds = []; // Detach old qdiscs gracefully allowing error suppression cmds.push(`tc qdisc del dev ${iface.name} root 2>/dev/null || true`); cmds.push(`tc qdisc del dev ${iface.name} ingress 2>/dev/null || true`); let hasLimits = false; for (let i = 0; i < clients.length; i++) { const client = clients[i]; if (client.tx_limit > 0 || client.rx_limit > 0) { if (!hasLimits) { cmds.push(`tc qdisc add dev ${iface.name} root handle 1: htb default 10`); cmds.push(`tc class add dev ${iface.name} parent 1: classid 1:1 htb rate 10gbit`); cmds.push(`tc qdisc add dev ${iface.name} handle ffff: ingress`); hasLimits = true; } const mark = i + 10; // client.rx_limit (Server -> Client = Download = root qdisc TX) - Rate is Bytes/sec so mult by 8 -> bits, /1000 -> Kbits if (client.rx_limit > 0) { const rateKbit = Math.floor((client.rx_limit * 8) / 1000); cmds.push(`tc class add dev ${iface.name} parent 1:1 classid 1:${mark} htb rate ${rateKbit}kbit`); cmds.push(`tc filter add dev ${iface.name} protocol ip parent 1:0 prio 1 u32 match ip dst ${client.ipv4_address}/32 flowid 1:${mark}`); } // client.tx_limit (Client -> Server = Upload = ingress qdisc RX) if (client.tx_limit > 0) { const rateKbit = Math.floor((client.tx_limit * 8) / 1000); cmds.push(`tc filter add dev ${iface.name} parent ffff: protocol ip u32 match ip src ${client.ipv4_address}/32 police rate ${rateKbit}kbit burst 1m drop flowid :1`); } } } if (hasLimits) { await execAsync(cmds.join(" && ")); } }, /** * Cron job to check client expirations */ startCronJob(knex) { cronTimer = setInterval(async () => { try { const clients = await knex("wg_client").where("enabled", true).whereNotNull("expires_at"); let needsSave = false; for (const client of clients) { if (new Date() > new Date(client.expires_at)) { logger.info(`WireGuard client "${client.name}" (${client.id}) has expired, disabling.`); await knex("wg_client").where("id", client.id).update({ enabled: false, modified_on: knex.fn.now(), }); needsSave = true; } } if (needsSave) { await this.saveConfig(knex); } // Audit Logging Polling const ifaces = await knex("wg_interface").select("name"); const allClients = await knex("wg_client").select("id", "public_key", "name", "owner_user_id"); for (const iface of ifaces) { try { const dump = await wgHelpers.wgDump(iface.name); for (const peer of dump) { const client = allClients.find((c) => c.public_key === peer.publicKey); if (client) { const lastHandshakeTime = new Date(peer.latestHandshakeAt).getTime(); const wasConnected = connectionMemoryMap[client.id] || false; const isConnected = lastHandshakeTime > 0 && (Date.now() - lastHandshakeTime < 3 * 60 * 1000); if (isConnected && !wasConnected) { connectionMemoryMap[client.id] = true; // Log connection (dummy token signature for audit logic) internalAuditLog.add({ token: { getUserId: () => client.owner_user_id } }, { action: "connected", meta: { message: `WireGuard client ${client.name} came online.` }, object_type: "wireguard-client", object_id: client.id }).catch(()=>{}); } else if (!isConnected && wasConnected) { connectionMemoryMap[client.id] = false; // Log disconnection internalAuditLog.add({ token: { getUserId: () => client.owner_user_id } }, { action: "disconnected", meta: { message: `WireGuard client ${client.name} went offline or drifted past TTL.` }, object_type: "wireguard-client", object_id: client.id }).catch(()=>{}); } } } } catch (_) {} } } catch (err) { logger.error("WireGuard cron job error:", err.message); } }, 60 * 1000); // every 60 seconds }, }; export default internalWireguard;