@ -1,127 +1,39 @@
import fs from 'node:fs/promises' ;
import path from 'path' ;
import debug_logger from 'debug' ;
import debug from 'debug' ;
import crypto from 'node:crypto' ;
import QRCode from 'qrcode' ;
import CRC32 from 'crc-32' ;
const debug = debug_logger ( 'WireGuard' ) ;
type Server = {
privateKey : string ;
publicKey : string ;
address : string ;
} ;
type Client = {
id : string ;
name : string ;
address : string ;
privateKey : string ;
publicKey : string ;
preSharedKey : string ;
createdAt : string ;
updatedAt : string ;
expireAt : string | null ;
endpoint : string | null ;
enabled : boolean ;
allowedIPs? : never ;
oneTimeLink : string | null ;
oneTimeLinkExpiresAt : string | null ;
} ;
type Config = {
server : Server ;
clients : Record < string , Client > ;
} ;
import type { NewClient } from '~~/services/database/repositories/client' ;
class WireGuard {
# configCache : Config | null = null ;
async __buildConfig() {
if ( ! WG_HOST ) {
throw new Error ( 'WG_HOST Environment Variable Not Set!' ) ;
}
debug ( 'Loading configuration...' ) ;
this . # configCache = null ;
try {
const config = await fs . readFile ( path . join ( WG_PATH , 'wg0.json' ) , 'utf8' ) ;
const parsedConfig = JSON . parse ( config ) ;
debug ( 'Configuration loaded.' ) ;
return parsedConfig as Config ;
} catch {
const privateKey = await exec ( 'wg genkey' ) ;
const publicKey = await exec ( ` echo ${ privateKey } | wg pubkey ` , {
log : 'echo ***hidden*** | wg pubkey' ,
} ) ;
const address = WG_DEFAULT_ADDRESS . replace ( 'x' , '1' ) ;
const config : Config = {
server : {
privateKey ,
publicKey ,
address ,
} ,
clients : { } ,
} ;
debug ( 'Configuration generated.' ) ;
return config ;
}
}
async getConfig ( ) : Promise < Config > {
if ( this . # configCache !== null ) {
return this . # configCache ;
}
const config = await this . __buildConfig ( ) ;
await this . __saveConfig ( config ) ;
await exec ( 'wg-quick down wg0' ) . catch ( ( ) = > { } ) ;
await exec ( 'wg-quick up wg0' ) . catch ( ( err ) = > {
if (
err &&
err . message &&
err . message . includes ( 'Cannot find device "wg0"' )
) {
throw new Error (
'WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!'
) ;
}
throw err ;
} ) ;
// await Util.exec(`iptables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ' + WG_DEVICE + ' -j MASQUERADE`);
// await Util.exec('iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT');
// await Util.exec('iptables -A FORWARD -i wg0 -j ACCEPT');
// await Util.exec('iptables -A FORWARD -o wg0 -j ACCEPT');
await this . __syncConfig ( ) ;
this . # configCache = config ;
return this . # configCache ;
}
const DEBUG = debug ( 'WireGuard' ) ;
class WireGuard {
async saveConfig() {
const config = await this . getConfig ( ) ;
await this . __saveConfig ( config ) ;
await this . __syncConfig ( ) ;
await this . # saveWireguardConfig ( ) ;
await this . # syncWireguardConfig ( ) ;
}
async __saveConfig ( config : Config ) {
async # saveWireguardConfig() {
const system = await Database . getSystem ( ) ;
const clients = await Database . getClients ( ) ;
let result = `
# Note : Do not edit this file directly .
# Your changes will be overwritten !
# Server
[ Interface ]
PrivateKey = $ { config . server . privateKey }
Address = $ { config . server . address } / 24
ListenPort = $ { WG_PORT }
PreUp = $ { WG_PRE_UP }
PostUp = $ { WG_POST_UP }
PreDown = $ { WG_PRE_DOWN }
PostDown = $ { WG_POST_DOWN }
PrivateKey = $ { system . interface . privateKey }
Address = $ { system . interface . address } / 24
ListenPort = $ { system . wgPort }
PreUp = $ { system . iptables . PreUp }
PostUp = $ { system . iptables . PostUp }
PreDown = $ { system . iptables . PreDown }
PostDown = $ { system . iptables . PostDown }
` ;
for ( const [ clientId , client ] of Object . entries ( config . c lients ) ) {
for ( const [ clientId , client ] of Object . entries ( clients ) ) {
if ( ! client . enabled ) continue ;
result += `
@ -134,49 +46,39 @@ ${
} AllowedIPs = $ { client . address } / 32 ` ;
}
debug ( 'Config saving...' ) ;
await fs . writeFile (
path . join ( WG_PATH , 'wg0.json' ) ,
JSON . stringify ( config , undefined , 2 ) ,
{
mode : 0o660 ,
}
) ;
DEBUG ( 'Config saving...' ) ;
await fs . writeFile ( path . join ( WG_PATH , 'wg0.conf' ) , result , {
mode : 0o600 ,
} ) ;
debug ( 'Config saved.' ) ;
DEBUG ( 'Config saved.' ) ;
}
async __ syncConfig() {
debug ( 'Config syncing...' ) ;
async # syncWireguardConfig() {
DEBUG ( 'Config syncing...' ) ;
await exec ( 'wg syncconf wg0 <(wg-quick strip wg0)' ) ;
debug ( 'Config synced.' ) ;
DEBUG ( 'Config synced.' ) ;
}
async getClients() {
const config = await this . getConfig ( ) ;
const clients = Object . entries ( config . clients ) . map (
( [ clientId , client ] ) = > ( {
id : clientId ,
name : client.name ,
enabled : client.enabled ,
address : client.address ,
publicKey : client.publicKey ,
createdAt : new Date ( client . createdAt ) ,
updatedAt : new Date ( client . updatedAt ) ,
expireAt : client.expireAt !== null ? new Date ( client . expireAt ) : null ,
allowedIPs : client.allowedIPs ,
oneTimeLink : client.oneTimeLink ? ? null ,
oneTimeLinkExpiresAt : client.oneTimeLinkExpiresAt ? ? null ,
downloadableConfig : 'privateKey' in client ,
persistentKeepalive : null as string | null ,
latestHandshakeAt : null as Date | null ,
endpoint : null as string | null ,
transferRx : null as number | null ,
transferTx : null as number | null ,
} )
) ;
const dbClients = await Database . getClients ( ) ;
const clients = Object . entries ( dbClients ) . map ( ( [ clientId , client ] ) = > ( {
id : clientId ,
name : client.name ,
enabled : client.enabled ,
address : client.address ,
publicKey : client.publicKey ,
createdAt : new Date ( client . createdAt ) ,
updatedAt : new Date ( client . updatedAt ) ,
expiresAt : client.expiresAt ,
allowedIPs : client.allowedIPs ,
oneTimeLink : client.oneTimeLink ,
downloadableConfig : 'privateKey' in client ,
persistentKeepalive : null as string | null ,
latestHandshakeAt : null as Date | null ,
endpoint : null as string | null ,
transferRx : null as number | null ,
transferTx : null as number | null ,
} ) ) ;
// Loop WireGuard status
const dump = await exec ( 'wg show wg0 dump' , {
@ -215,8 +117,7 @@ ${
}
async getClient ( { clientId } : { clientId : string } ) {
const config = await this . getConfig ( ) ;
const client = config . clients [ clientId ] ;
const client = await Database . getClient ( clientId ) ;
if ( ! client ) {
throw createError ( {
statusCode : 404 ,
@ -228,23 +129,22 @@ ${
}
async getClientConfiguration ( { clientId } : { clientId : string } ) {
const config = await this . getConfig ( ) ;
const system = await Database . getSystem ( ) ;
const client = await this . getClient ( { clientId } ) ;
return `
[ Interface ]
PrivateKey = $ { client . privateKey ? ` ${ client . privateKey } ` : 'REPLACE_ME' }
Address = $ { client . address } / 24
$ { WG_DEFAULT_DNS ? ` DNS = ${ WG_DEFAULT_DNS } \ n ` : '' } \
$ { WG_MTU ? ` MTU = ${ WG_MTU } \ n ` : '' } \
DNS = $ { system . userConfig . defaultDns . join ( ',' ) }
MTU = $ { system . userConfig . mtu }
[ Peer ]
PublicKey = $ { config . server . publicKey }
$ {
client . preSharedKey ? ` PresharedKey = ${ client . preSharedKey } \ n ` : ''
} AllowedIPs = $ { WG_ALLOWED_IPS }
PersistentKeepalive = $ { WG_PERSISTENT_KEEPALIVE }
Endpoint = $ { WG_HOST } : $ { WG_CONFIG_PORT } ` ;
PublicKey = $ { system . interface . publicKey }
PresharedKey = $ { client . preSharedKey }
AllowedIPs = $ { client . allowedIPs }
PersistentKeepalive = $ { client . persistentKeepalive }
Endpoint = $ { system . wgHost } : $ { system . wgConfigPort } ` ;
}
async getClientQRCodeSVG ( { clientId } : { clientId : string } ) {
@ -266,7 +166,8 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
throw new Error ( 'Missing: Name' ) ;
}
const config = await this . getConfig ( ) ;
const system = await Database . getSystem ( ) ;
const clients = await Database . getClients ( ) ;
const privateKey = await exec ( 'wg genkey' ) ;
const publicKey = await exec ( ` echo ${ privateKey } | wg pubkey ` , {
@ -274,15 +175,19 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
} ) ;
const preSharedKey = await exec ( 'wg genpsk' ) ;
// TODO: cidr
// Calculate next IP
let address ;
for ( let i = 2 ; i < 255 ; i ++ ) {
const client = Object . values ( config . clients ) . find ( ( client ) = > {
return client . address === WG_DEFAULT_ADDRESS . replace ( 'x' , i . toString ( ) ) ;
const client = Object . values ( clients ) . find ( ( client ) = > {
return (
client . address ===
system . userConfig . addressRange . replace ( 'x' , i . toString ( ) )
) ;
} ) ;
if ( ! client ) {
address = WG_DEFAULT_ADDRESS . replace ( 'x' , i . toString ( ) ) ;
address = system . userConfig . addressRange . replace ( 'x' , i . toString ( ) ) ;
break ;
}
}
@ -293,22 +198,20 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
// Create Client
const id = crypto . randomUUID ( ) ;
const client : Client = {
const client : NewClient = {
id ,
name ,
address ,
privateKey ,
publicKey ,
preSharedKey ,
createdAt : new Date ( ) . toISOString ( ) ,
updatedAt : new Date ( ) . toISOString ( ) ,
endpoint : null ,
oneTimeLink : null ,
oneTimeLinkExpiresAt : null ,
expireAt : null ,
expiresAt : null ,
enabled : true ,
allowedIPs : system.userConfig.allowedIps ,
persistentKeepalive : system.userConfig.persistentKeepalive ,
} ;
if ( expireDate ) {
@ -316,10 +219,10 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
date . setHours ( 23 ) ;
date . setMinutes ( 59 ) ;
date . setSeconds ( 59 ) ;
client . expireAt = date . toISOString ( ) ;
client . expires At = date ;
}
config . clients [ id ] = client ;
await Database . createClient ( client ) ;
await this . saveConfig ( ) ;
@ -327,50 +230,34 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}
async deleteClient ( { clientId } : { clientId : string } ) {
const config = await this . getConfig ( ) ;
if ( config . clients [ clientId ] ) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete config . clients [ clientId ] ;
await this . saveConfig ( ) ;
}
await Database . deleteClient ( clientId ) ;
await this . saveConfig ( ) ;
}
async enableClient ( { clientId } : { clientId : string } ) {
const client = await this . getClient ( { clientId } ) ;
client . enabled = true ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
await Database . toggleClient ( clientId , true ) ;
await this . saveConfig ( ) ;
}
async generateOneTimeLink ( { clientId } : { clientId : string } ) {
const client = await this . getClient ( { clientId } ) ;
const key = ` ${ clientId } - ${ Math . floor ( Math . random ( ) * 1000 ) } ` ;
client . oneTimeLink = Math . abs ( CRC32 . str ( key ) ) . toString ( 16 ) ;
client . oneTimeLinkExpiresAt = new Date (
Date . now ( ) + 5 * 60 * 1000
) . toISOString ( ) ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
const oneTimeLink = Math . abs ( CRC32 . str ( key ) ) . toString ( 16 ) ;
const expiresAt = new Date ( Date . now ( ) + 5 * 60 * 1000 ) ;
await Database . createOneTimeLink ( clientId , {
oneTimeLink ,
expiresAt ,
} ) ;
await this . saveConfig ( ) ;
}
async eraseOneTimeLink ( { clientId } : { clientId : string } ) {
const client = await this . getClient ( { clientId } ) ;
// client.oneTimeLink = null;
client . oneTimeLinkExpiresAt = new Date (
Date . now ( ) + 10 * 1000
) . toISOString ( ) ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
await Database . deleteOneTimeLink ( clientId ) ;
await this . saveConfig ( ) ;
}
async disableClient ( { clientId } : { clientId : string } ) {
const client = await this . getClient ( { clientId } ) ;
client . enabled = false ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
await Database . toggleClient ( clientId , false ) ;
await this . saveConfig ( ) ;
}
@ -382,10 +269,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId : string ;
name : string ;
} ) {
const client = await this . getClient ( { clientId } ) ;
client . name = name ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
await Database . updateClientName ( clientId , name ) ;
await this . saveConfig ( ) ;
}
@ -397,8 +281,6 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId : string ;
address : string ;
} ) {
const client = await this . getClient ( { clientId } ) ;
if ( ! isValidIPv4 ( address ) ) {
throw createError ( {
statusCode : 400 ,
@ -406,8 +288,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
} ) ;
}
client . address = address ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
await Database . updateClientAddress ( clientId , address ) ;
await this . saveConfig ( ) ;
}
@ -419,42 +300,81 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId : string ;
expireDate : string | null ;
} ) {
const client = await this . getClient ( { clientId } ) ;
let updatedDate : Date | null = null ;
if ( expireDate ) {
const date = new Date ( expireDate ) ;
date . setHours ( 23 ) ;
date . setMinutes ( 59 ) ;
date . setSeconds ( 59 ) ;
client . expireAt = date . toISOString ( ) ;
} else {
client . expireAt = null ;
updatedDate = date ;
}
client . updatedAt = new Date ( ) . toISOString ( ) ;
await this . saveConfig ( ) ;
}
await Database . updateClientExpirationDate ( clientId , updatedDate ) ;
async __reloadConfig() {
await this . __buildConfig ( ) ;
await this . __syncConfig ( ) ;
await this . saveConfig ( ) ;
}
async restoreConfiguration ( config : string ) {
debug ( 'Starting configuration restore process.' ) ;
// TODO: reimplement database restore
async restoreConfiguration ( _config : string ) {
/ * D E B U G ( ' S t a r t i n g c o n f i g u r a t i o n r e s t o r e p r o c e s s . ' ) ;
// TODO: sanitize config
const _config = JSON . parse ( config ) ;
await this . __saveConfig ( _config ) ;
await this . __reloadConfig ( ) ;
debug ( 'Configuration restore process completed.' ) ;
DEBUG ( 'Configuration restore process completed.' ) ; * /
}
// TODO: reimplement database restore
async backupConfiguration() {
debug ( 'Starting configuration backup.' ) ;
/ * D E B U G ( ' S t a r t i n g c o n f i g u r a t i o n b a c k u p . ' ) ;
const config = await this . getConfig ( ) ;
const backup = JSON . stringify ( config , null , 2 ) ;
debug ( 'Configuration backup completed.' ) ;
return backup ;
DEBUG ( 'Configuration backup completed.' ) ;
return backup ; * /
}
async Startup() {
// TODO: improve this
await new Promise ( ( res ) = > {
function wait() {
if ( Database . connected ) {
return res ( true ) ;
}
}
setTimeout ( wait , 1000 ) ;
} ) ;
DEBUG ( 'Starting Wireguard' ) ;
await this . # saveWireguardConfig ( ) ;
await exec ( 'wg-quick down wg0' ) . catch ( ( ) = > { } ) ;
await exec ( 'wg-quick up wg0' ) . catch ( ( err ) = > {
if (
err &&
err . message &&
err . message . includes ( 'Cannot find device "wg0"' )
) {
throw new Error (
'WireGuard exited with the error: Cannot find device "wg0"\nThis usually means that your host\'s kernel does not support WireGuard!'
) ;
}
throw err ;
} ) ;
await this . # syncWireguardConfig ( ) ;
DEBUG ( 'Wireguard started successfully' ) ;
DEBUG ( 'Starting Cron Job' ) ;
await this . startCronJob ( ) ;
}
async startCronJob() {
await this . cronJob ( ) . catch ( ( err ) = > {
DEBUG ( 'Running Cron Job failed.' ) ;
console . error ( err ) ;
} ) ;
setTimeout ( ( ) = > {
this . startCronJob ( ) ;
} , 60 * 1000 ) ;
}
// Shutdown wireguard
@ -462,47 +382,31 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
await exec ( 'wg-quick down wg0' ) . catch ( ( ) = > { } ) ;
}
async cronJobEveryMinute () {
const config = await this . getConfig ( ) ;
async cronJob() {
const clients = await Database . getClients ( ) ;
const system = await Database . getSystem ( ) ;
if ( ! system ) {
throw new Error ( 'Invalid Database' ) ;
}
let needSaveConfig = false ;
// Expires Feature
if ( system . clientExpiration . enabled ) {
for ( const client of Object . values ( config . c lients ) ) {
for ( const client of Object . values ( clients ) ) {
if ( client . enabled !== true ) continue ;
if (
client . expireAt !== null &&
new Date ( ) > new Date ( client . expireAt )
) {
debug ( ` Client ${ client . id } expired. ` ) ;
needSaveConfig = true ;
client . enabled = false ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
if ( client . expiresAt !== null && new Date ( ) > client . expiresAt ) {
DEBUG ( ` Client ${ client . id } expired. ` ) ;
await Database . toggleClient ( client . id , false ) ;
}
}
}
// One Time Link Feature
if ( system . oneTimeLinks . enabled ) {
for ( const client of Object . values ( config . c lients ) ) {
for ( const client of Object . values ( clients ) ) {
if (
client . oneTimeLink !== null &&
client . oneTimeLinkExpiresAt !== null &&
new Date ( ) > new Date ( client . oneTimeLinkExpiresAt )
new Date ( ) > client . oneTimeLink . expiresAt
) {
debug ( ` Client ${ client . id } One Time Link expired. ` ) ;
needSaveConfig = true ;
client . oneTimeLink = null ;
client . oneTimeLinkExpiresAt = null ;
client . updatedAt = new Date ( ) . toISOString ( ) ;
DEBUG ( ` Client ${ client . id } One Time Link expired. ` ) ;
await Database . deleteOneTimeLink ( client . id ) ;
}
}
}
if ( needSaveConfig ) {
await this . saveConfig ( ) ;
}
}
async getMetrics() {
@ -580,15 +484,9 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}
const inst = new WireGuard ( ) ;
// This also has to also start the WireGuard Server
async function cronJobEveryMinute() {
await inst . cronJobEveryMinute ( ) . catch ( ( err ) = > {
debug ( 'Running Cron Job failed.' ) ;
console . error ( err ) ;
} ) ;
setTimeout ( cronJobEveryMinute , 60 * 1000 ) ;
}
cronJobEveryMinute ( ) ;
inst . Startup ( ) . catch ( ( v ) = > {
console . error ( v ) ;
process . exit ( 1 ) ;
} ) ;
export default inst ;