You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

590 lines
16 KiB

import fs from 'node:fs/promises';
import path from 'path';
import debug_logger 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>;
};
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;
}
async saveConfig() {
const config = await this.getConfig();
await this.__saveConfig(config);
await this.__syncConfig();
}
async __saveConfig(config: Config) {
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}
`;
for (const [clientId, client] of Object.entries(config.clients)) {
if (!client.enabled) continue;
result += `
# Client: ${client.name} (${clientId})
[Peer]
PublicKey = ${client.publicKey}
${
client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
}AllowedIPs = ${client.address}/32`;
}
debug('Config saving...');
await fs.writeFile(
path.join(WG_PATH, 'wg0.json'),
JSON.stringify(config, undefined, 2),
{
mode: 0o660,
}
);
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
mode: 0o600,
});
debug('Config saved.');
}
async __syncConfig() {
debug('Config syncing...');
await exec('wg syncconf wg0 <(wg-quick strip wg0)');
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,
})
);
// Loop WireGuard status
const dump = await exec('wg show wg0 dump', {
log: false,
});
dump
.trim()
.split('\n')
.slice(1)
.forEach((line) => {
const [
publicKey,
_preSharedKey,
endpoint,
_allowedIps,
latestHandshakeAt,
transferRx,
transferTx,
persistentKeepalive,
] = line.split('\t');
const client = clients.find((client) => client.publicKey === publicKey);
if (!client) return;
client.latestHandshakeAt =
latestHandshakeAt === '0'
? null
: new Date(Number(`${latestHandshakeAt}000`));
client.endpoint = endpoint === '(none)' ? null : (endpoint ?? null);
client.transferRx = Number(transferRx);
client.transferTx = Number(transferTx);
client.persistentKeepalive = persistentKeepalive ?? null;
});
return clients;
}
async getClient({ clientId }: { clientId: string }) {
const config = await this.getConfig();
const client = config.clients[clientId];
if (!client) {
throw createError({
statusCode: 404,
statusMessage: `Client Not Found: ${clientId}`,
});
}
return client;
}
async getClientConfiguration({ clientId }: { clientId: string }) {
const config = await this.getConfig();
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` : ''}\
[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}`;
}
async getClientQRCodeSVG({ clientId }: { clientId: string }) {
const config = await this.getClientConfiguration({ clientId });
return QRCode.toString(config, {
type: 'svg',
width: 512,
});
}
async createClient({
name,
expireDate,
}: {
name: string;
expireDate: string | null;
}) {
if (!name) {
throw new Error('Missing: Name');
}
const config = await this.getConfig();
const privateKey = await exec('wg genkey');
const publicKey = await exec(`echo ${privateKey} | wg pubkey`, {
log: 'echo ***hidden*** | wg pubkey',
});
const preSharedKey = await exec('wg genpsk');
// 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());
});
if (!client) {
address = WG_DEFAULT_ADDRESS.replace('x', i.toString());
break;
}
}
if (!address) {
throw new Error('Maximum number of clients reached.');
}
// Create Client
const id = crypto.randomUUID();
const client: Client = {
id,
name,
address,
privateKey,
publicKey,
preSharedKey,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
endpoint: null,
oneTimeLink: null,
oneTimeLinkExpiresAt: null,
expireAt: null,
enabled: true,
};
if (expireDate) {
const date = new Date(expireDate);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
client.expireAt = date.toISOString();
}
config.clients[id] = client;
await this.saveConfig();
return client;
}
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();
}
}
async enableClient({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
client.enabled = true;
client.updatedAt = new Date().toISOString();
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();
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 this.saveConfig();
}
async disableClient({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
client.enabled = false;
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
async updateClientName({
clientId,
name,
}: {
clientId: string;
name: string;
}) {
const client = await this.getClient({ clientId });
client.name = name;
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
async updateClientAddress({
clientId,
address,
}: {
clientId: string;
address: string;
}) {
const client = await this.getClient({ clientId });
if (!isValidIPv4(address)) {
throw createError({
statusCode: 400,
statusMessage: `Invalid Address: ${address}`,
});
}
client.address = address;
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
async updateClientExpireDate({
clientId,
expireDate,
}: {
clientId: string;
expireDate: string | null;
}) {
const client = await this.getClient({ clientId });
if (expireDate) {
const date = new Date(expireDate);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
client.expireAt = date.toISOString();
} else {
client.expireAt = null;
}
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
async __reloadConfig() {
await this.__buildConfig();
await this.__syncConfig();
}
async restoreConfiguration(config: string) {
debug('Starting configuration restore process.');
// TODO: sanitize config
const _config = JSON.parse(config);
await this.__saveConfig(_config);
await this.__reloadConfig();
debug('Configuration restore process completed.');
}
async backupConfiguration() {
debug('Starting configuration backup.');
const config = await this.getConfig();
const backup = JSON.stringify(config, null, 2);
debug('Configuration backup completed.');
return backup;
}
// Shutdown wireguard
async Shutdown() {
await exec('wg-quick down wg0').catch(() => {});
}
async cronJobEveryMinute() {
const config = await this.getConfig();
let needSaveConfig = false;
// Expires Feature
if (WG_ENABLE_EXPIRES_TIME === 'true') {
for (const client of Object.values(config.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();
}
}
}
// One Time Link Feature
if (WG_ENABLE_ONE_TIME_LINKS === 'true') {
for (const client of Object.values(config.clients)) {
if (
client.oneTimeLink !== null &&
client.oneTimeLinkExpiresAt !== null &&
new Date() > new Date(client.oneTimeLinkExpiresAt)
) {
debug(`Client ${client.id} One Time Link expired.`);
needSaveConfig = true;
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date().toISOString();
}
}
}
if (needSaveConfig) {
await this.saveConfig();
}
}
async getMetrics() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
let wireguardSentBytes = '';
let wireguardReceivedBytes = '';
let wireguardLatestHandshakeSeconds = '';
for (const client of Object.values(clients)) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
wireguardSentBytes += `wireguard_sent_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferTx)}\n`;
wireguardReceivedBytes += `wireguard_received_bytes{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${Number(client.transferRx)}\n`;
wireguardLatestHandshakeSeconds += `wireguard_latest_handshake_seconds{interface="wg0",enabled="${client.enabled}",address="${client.address}",name="${client.name}"} ${client.latestHandshakeAt ? (new Date().getTime() - new Date(client.latestHandshakeAt).getTime()) / 1000 : 0}\n`;
}
let returnText = '# HELP wg-easy and wireguard metrics\n';
returnText += '\n# HELP wireguard_configured_peers\n';
returnText += '# TYPE wireguard_configured_peers gauge\n';
returnText += `wireguard_configured_peers{interface="wg0"} ${Number(wireguardPeerCount)}\n`;
returnText += '\n# HELP wireguard_enabled_peers\n';
returnText += '# TYPE wireguard_enabled_peers gauge\n';
returnText += `wireguard_enabled_peers{interface="wg0"} ${Number(wireguardEnabledPeersCount)}\n`;
returnText += '\n# HELP wireguard_connected_peers\n';
returnText += '# TYPE wireguard_connected_peers gauge\n';
returnText += `wireguard_connected_peers{interface="wg0"} ${Number(wireguardConnectedPeersCount)}\n`;
returnText += '\n# HELP wireguard_sent_bytes Bytes sent to the peer\n';
returnText += '# TYPE wireguard_sent_bytes counter\n';
returnText += `${wireguardSentBytes}`;
returnText +=
'\n# HELP wireguard_received_bytes Bytes received from the peer\n';
returnText += '# TYPE wireguard_received_bytes counter\n';
returnText += `${wireguardReceivedBytes}`;
returnText +=
'\n# HELP wireguard_latest_handshake_seconds UNIX timestamp seconds of the last handshake\n';
returnText += '# TYPE wireguard_latest_handshake_seconds gauge\n';
returnText += `${wireguardLatestHandshakeSeconds}`;
return returnText;
}
async getMetricsJSON() {
const clients = await this.getClients();
let wireguardPeerCount = 0;
let wireguardEnabledPeersCount = 0;
let wireguardConnectedPeersCount = 0;
for (const client of Object.values(clients)) {
wireguardPeerCount++;
if (client.enabled === true) {
wireguardEnabledPeersCount++;
}
if (client.endpoint !== null) {
wireguardConnectedPeersCount++;
}
}
return {
wireguard_configured_peers: Number(wireguardPeerCount),
wireguard_enabled_peers: Number(wireguardEnabledPeersCount),
wireguard_connected_peers: Number(wireguardConnectedPeersCount),
};
}
}
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();
export default inst;