2026-03-07 13:49:44 +00:00
import fs from "fs" ;
2026-03-10 06:09:51 +00:00
import { exec } from "child_process" ;
import { promisify } from "util" ;
2026-03-07 13:49:44 +00:00
import { global as logger } from "../logger.js" ;
import * as wgHelpers from "../lib/wg-helpers.js" ;
2026-03-10 06:09:51 +00:00
import internalWireguardFs from "./wireguard-fs.js" ;
import internalAuditLog from "./audit-log.js" ;
const execAsync = promisify ( exec ) ;
2026-03-07 13:49:44 +00:00
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 ;
2026-03-10 06:09:51 +00:00
let connectionMemoryMap = { } ;
2026-03-07 13:49:44 +00:00
const internalWireguard = {
/ * *
* Get or create the WireGuard interface in DB
* /
async getOrCreateInterface ( knex ) {
let iface = await knex ( "wg_interface" ) . first ( ) ;
if ( ! iface ) {
2026-03-08 02:33:24 +00:00
// Seed a default config if it doesn't exist
const insertData = {
name : "wg0" ,
listen _port : 51820 ,
ipv4 _cidr : "10.0.0.1/24" ,
mtu : 1420 ,
2026-03-07 13:49:44 +00:00
dns : WG _DEFAULT _DNS ,
host : WG _HOST ,
2026-03-08 02:33:24 +00:00
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" ,
2026-03-07 13:49:44 +00:00
created _on : knex . fn . now ( ) ,
modified _on : knex . fn . now ( ) ,
2026-03-08 02:33:24 +00:00
} ;
const [ id ] = await knex ( "wg_interface" ) . insert ( insertData ) ;
2026-03-07 13:49:44 +00:00
iface = await knex ( "wg_interface" ) . where ( "id" , id ) . first ( ) ;
2026-03-08 02:33:24 +00:00
logger . info ( "WireGuard interface created with default config" ) ;
2026-03-07 13:49:44 +00:00
}
return iface ;
} ,
2026-03-08 02:33:24 +00:00
/ * *
* 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" ) ;
// Client Isolation: Prevent clients on this interface from communicating with each other
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" ) ;
}
// Server Isolation (Default DROP) & Server Peering
// 1. By default, prevent this interface from talking to ANY OTHER wg+ interfaces
basePostUp . push ( "iptables -I FORWARD -i %i -o wg+ -j DROP" ) ;
basePostDown . push ( "iptables -D FORWARD -i %i -o wg+ -j DROP" ) ;
// 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 ( "; " ) ,
} ;
} ,
2026-03-07 13:49:44 +00:00
/ * *
2026-03-08 03:58:19 +00:00
* Save WireGuard config to / etc / wireguard / wgX . conf and sync
2026-03-07 13:49:44 +00:00
* /
async saveConfig ( knex ) {
2026-03-08 03:58:19 +00:00
await this . getOrCreateInterface ( knex ) ; // Ensure at least wg0 exists
const ifaces = await knex ( "wg_interface" ) . select ( "*" ) ;
2026-03-07 13:49:44 +00:00
const clients = await knex ( "wg_client" ) . where ( "enabled" , true ) ;
2026-03-08 03:58:19 +00:00
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 ,
2026-03-07 13:49:44 +00:00
} ) ;
2026-03-08 03:58:19 +00:00
// 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" ;
2026-03-07 13:49:44 +00:00
2026-03-08 03:58:19 +00:00
// 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 } ` ) ;
2026-03-07 13:49:44 +00:00
2026-03-08 03:58:19 +00:00
// 5. Sync config
try {
await wgHelpers . wgSync ( iface . name ) ;
logger . info ( ` WireGuard config synced for ${ iface . name } ` ) ;
2026-03-10 06:09:51 +00:00
// 6. Apply traffic control bandwidth partitions non-blocking
this . applyBandwidthLimits ( knex , iface ) . catch ( ( e ) => logger . warn ( ` Skipping QoS on ${ iface . name } : ${ e . message } ` ) ) ;
2026-03-08 03:58:19 +00:00
} catch ( err ) {
logger . warn ( ` WireGuard sync failed for ${ iface . name } , may need full restart: ` , err . message ) ;
}
2026-03-07 13:49:44 +00:00
}
} ,
/ * *
2026-03-08 03:58:19 +00:00
* Start WireGuard interfaces
2026-03-07 13:49:44 +00:00
* /
async startup ( knex ) {
try {
2026-03-08 03:58:19 +00:00
await this . getOrCreateInterface ( knex ) ; // ensure at least wg0
2026-03-07 13:49:44 +00:00
// Ensure config dir exists
if ( ! fs . existsSync ( WG _CONFIG _DIR ) ) {
fs . mkdirSync ( WG _CONFIG _DIR , { recursive : true } ) ;
}
2026-03-08 03:58:19 +00:00
// Save configs first (generates .conf files dynamically for all wg_interfaces)
2026-03-07 13:49:44 +00:00
await this . saveConfig ( knex ) ;
2026-03-08 03:58:19 +00:00
// 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
}
2026-03-07 13:49:44 +00:00
2026-03-08 03:58:19 +00:00
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 ) ;
}
}
2026-03-07 13:49:44 +00:00
// Start cron job for expiration
this . startCronJob ( knex ) ;
} catch ( err ) {
2026-03-08 03:58:19 +00:00
logger . error ( "WireGuard startup failed overall:" , err . message ) ;
2026-03-07 13:49:44 +00:00
}
} ,
/ * *
2026-03-08 03:58:19 +00:00
* Shutdown WireGuard interfaces
2026-03-07 13:49:44 +00:00
* /
async shutdown ( knex ) {
if ( cronTimer ) {
clearInterval ( cronTimer ) ;
cronTimer = null ;
}
try {
2026-03-08 03:58:19 +00:00
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 ) ;
}
2026-03-07 13:49:44 +00:00
}
} catch ( err ) {
2026-03-08 03:58:19 +00:00
logger . error ( "WireGuard shutdown failed querying DB:" , err . message ) ;
2026-03-07 13:49:44 +00:00
}
} ,
/ * *
2026-03-08 03:58:19 +00:00
* Get all clients with live status and interface name correlation
2026-03-07 13:49:44 +00:00
* /
2026-03-10 03:58:08 +00:00
async getClients ( knex , access , accessData ) {
2026-03-08 03:58:19 +00:00
await this . getOrCreateInterface ( knex ) ; // Ensure structure exists
2026-03-10 03:39:46 +00:00
const query = knex ( "wg_client" )
2026-03-08 03:58:19 +00:00
. 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" ) ;
2026-03-07 13:49:44 +00:00
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 03:39:46 +00:00
query . andWhere ( "wg_client.owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
const dbClients = await query ;
2026-03-07 13:49:44 +00:00
const clients = dbClients . map ( ( c ) => ( {
id : c . id ,
name : c . name ,
2026-03-08 03:58:19 +00:00
interfaceName : c . interface _name ,
2026-03-08 13:55:47 +00:00
interfaceId : c . interface _id ,
2026-03-07 13:49:44 +00:00
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 ,
} ) ) ;
2026-03-08 03:58:19 +00:00
// 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 ;
}
2026-03-07 13:49:44 +00:00
}
2026-03-08 03:58:19 +00:00
} catch ( _ ) {
// WireGuard might be off or particular interface fails
2026-03-07 13:49:44 +00:00
}
}
2026-03-10 06:09:51 +00:00
// Inject Storage Utilization Metrics
for ( const client of clients ) {
client . storage _usage _bytes = await internalWireguardFs . getClientStorageUsage ( client . ipv4 _address ) ;
}
2026-03-07 13:49:44 +00:00
return clients ;
} ,
/ * *
* Create a new WireGuard client
* /
2026-03-10 03:58:08 +00:00
async createClient ( knex , data , access , accessData ) {
2026-03-08 03:39:17 +00:00
const iface = data . interface _id
? await knex ( "wg_interface" ) . where ( "id" , data . interface _id ) . first ( )
: await this . getOrCreateInterface ( knex ) ;
2026-03-07 13:49:44 +00:00
// Generate keys
const privateKey = await wgHelpers . generatePrivateKey ( ) ;
const publicKey = await wgHelpers . getPublicKey ( privateKey ) ;
const preSharedKey = await wgHelpers . generatePreSharedKey ( ) ;
// Allocate IP
2026-03-08 03:39:17 +00:00
const existingClients = await knex ( "wg_client" ) . select ( "ipv4_address" ) . where ( "interface_id" , iface . id ) ;
2026-03-07 13:49:44 +00:00
const allocatedIPs = existingClients . map ( ( c ) => c . ipv4 _address ) ;
const ipv4Address = wgHelpers . findNextAvailableIP ( iface . ipv4 _cidr , allocatedIPs ) ;
2026-03-10 04:25:40 +00:00
if ( ! ipv4Address ) {
throw new Error ( "No available IP addresses remaining in this WireGuard server subnet." ) ;
}
2026-03-10 06:09:51 +00:00
// Scrub any old junk partitions to prevent leakage
await internalWireguardFs . deleteClientDir ( ipv4Address ) ;
2026-03-07 13:49:44 +00:00
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 ,
2026-03-08 03:39:17 +00:00
interface _id : iface . id ,
2026-03-10 03:39:46 +00:00
owner _user _id : access ? access . token . getUserId ( 1 ) : 1 ,
2026-03-07 13:49:44 +00:00
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
* /
2026-03-10 03:58:08 +00:00
async deleteClient ( knex , clientId , access , accessData ) {
2026-03-10 03:39:46 +00:00
const query = knex ( "wg_client" ) . where ( "id" , clientId ) ;
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 03:39:46 +00:00
query . andWhere ( "owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
const client = await query . first ( ) ;
2026-03-07 13:49:44 +00:00
if ( ! client ) {
throw new Error ( "Client not found" ) ;
}
await knex ( "wg_client" ) . where ( "id" , clientId ) . del ( ) ;
2026-03-10 06:09:51 +00:00
// Hard-remove the encrypted partition safely mapped to the ipv4_address since it's deleted
await internalWireguardFs . deleteClientDir ( client . ipv4 _address ) ;
2026-03-07 13:49:44 +00:00
await this . saveConfig ( knex ) ;
return { success : true } ;
} ,
/ * *
* Toggle a WireGuard client enabled / disabled
* /
2026-03-10 03:58:08 +00:00
async toggleClient ( knex , clientId , enabled , access , accessData ) {
2026-03-10 03:39:46 +00:00
const query = knex ( "wg_client" ) . where ( "id" , clientId ) ;
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 03:39:46 +00:00
query . andWhere ( "owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
const client = await query . first ( ) ;
2026-03-07 13:49:44 +00:00
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
* /
2026-03-10 03:58:08 +00:00
async updateClient ( knex , clientId , data , access , accessData ) {
2026-03-10 03:39:46 +00:00
const query = knex ( "wg_client" ) . where ( "id" , clientId ) ;
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 03:39:46 +00:00
query . andWhere ( "owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
const client = await query . first ( ) ;
2026-03-07 13:49:44 +00:00
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" ) ;
}
2026-03-08 03:58:19 +00:00
const iface = await knex ( "wg_interface" ) . where ( "id" , client . interface _id ) . first ( ) ;
if ( ! iface ) {
throw new Error ( "Interface not found for this client" ) ;
}
2026-03-07 13:49:44 +00:00
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 ) ;
} ,
2026-03-08 03:39:17 +00:00
/ * *
* Create a new WireGuard Interface Endpoint
* /
2026-03-10 03:58:08 +00:00
async createInterface ( knex , data , access , accessData ) {
2026-03-08 03:39:17 +00:00
const existingIfaces = await knex ( "wg_interface" ) . select ( "name" , "listen_port" ) ;
2026-03-10 04:25:40 +00:00
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 ++ ;
}
2026-03-08 03:39:17 +00:00
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 ,
2026-03-10 03:39:46 +00:00
owner _user _id : access ? access . token . getUserId ( 1 ) : 1 ,
2026-03-08 03:39:17 +00:00
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
* /
2026-03-10 03:58:08 +00:00
async updateInterface ( knex , id , data , access , accessData ) {
2026-03-10 03:39:46 +00:00
const query = knex ( "wg_interface" ) . where ( "id" , id ) ;
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 03:39:46 +00:00
query . andWhere ( "owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
const iface = await query . first ( ) ;
2026-03-08 03:39:17 +00:00
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
* /
2026-03-10 03:58:08 +00:00
async deleteInterface ( knex , id , access , accessData ) {
2026-03-10 03:39:46 +00:00
const query = knex ( "wg_interface" ) . where ( "id" , id ) ;
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 03:39:46 +00:00
query . andWhere ( "owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
const iface = await query . first ( ) ;
2026-03-08 03:39:17 +00:00
if ( ! iface ) throw new Error ( "Interface not found" ) ;
2026-03-10 06:09:51 +00:00
// 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." ) ;
}
}
2026-03-08 03:39:17 +00:00
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 } ` ) ;
}
2026-03-10 06:09:51 +00:00
// 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
2026-03-08 03:39:17 +00:00
await knex ( "wg_interface" ) . where ( "id" , id ) . del ( ) ;
return { success : true } ;
} ,
/ * *
* Update Peering Links between WireGuard Interfaces
* /
2026-03-10 03:58:08 +00:00
async updateInterfaceLinks ( knex , id , linkedServers , access , accessData ) {
2026-03-10 03:39:46 +00:00
// Verify ownership
const query = knex ( "wg_interface" ) . where ( "id" , id ) ;
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 03:39:46 +00:00
query . andWhere ( "owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
const iface = await query . first ( ) ;
if ( ! iface ) throw new Error ( "Interface not found" ) ;
2026-03-08 03:39:17 +00:00
// 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 } ;
} ,
2026-03-07 13:49:44 +00:00
/ * *
2026-03-08 02:47:20 +00:00
* Get the WireGuard interfaces info
2026-03-07 13:49:44 +00:00
* /
2026-03-10 03:58:08 +00:00
async getInterfacesInfo ( knex , access , accessData ) {
2026-03-10 03:39:46 +00:00
const query = knex ( "wg_interface" ) . select ( "*" ) ;
2026-03-10 04:25:40 +00:00
if ( access ) {
2026-03-10 06:09:51 +00:00
if ( accessData . permission _visibility !== "all" ) {
query . andWhere ( "owner_user_id" , access . token . getUserId ( 1 ) ) ;
}
2026-03-10 03:39:46 +00:00
}
const ifaces = await query ;
2026-03-08 02:47:20 +00:00
const allLinks = await knex ( "wg_server_link" ) . select ( "*" ) ;
2026-03-10 06:09:51 +00:00
const allClients = await knex ( "wg_client" ) . select ( "interface_id" , "ipv4_address" ) ;
2026-03-08 02:47:20 +00:00
2026-03-10 06:09:51 +00:00
const result = [ ] ;
for ( const i of ifaces ) {
2026-03-08 02:47:20 +00:00
const links = allLinks . filter ( l => l . interface _id _1 === i . id || l . interface _id _2 === i . id ) ;
2026-03-10 06:09:51 +00:00
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 ( {
2026-03-08 02:47:20 +00:00
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 ) ,
2026-03-10 06:09:51 +00:00
client _count ,
storage _usage _bytes
} ) ;
}
return result ;
} ,
/ * *
* 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 ( " && " ) ) ;
}
2026-03-07 13:49:44 +00:00
} ,
/ * *
* 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 ) ;
}
2026-03-10 06:09:51 +00:00
// 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 ( _ ) { }
}
2026-03-07 13:49:44 +00:00
} catch ( err ) {
logger . error ( "WireGuard cron job error:" , err . message ) ;
}
} , 60 * 1000 ) ; // every 60 seconds
} ,
} ;
export default internalWireguard ;