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.
 
 
 
 
 

497 lines
14 KiB

import fs from 'node:fs/promises';
import path from 'path';
import debug from 'debug';
import crypto from 'node:crypto';
import QRCode from 'qrcode';
import CRC32 from 'crc-32';
import type { NewClient } from '~~/services/database/repositories/client';
import ip from 'ip';
const DEBUG = debug('WireGuard');
class WireGuard {
async saveConfig() {
await this.#saveWireguardConfig();
await this.#syncWireguardConfig();
}
async #saveWireguardConfig() {
const system = await Database.getSystem();
const clients = await Database.getClients();
const cidrBlock = ip.cidrSubnet(
system.userConfig.addressRange
).subnetMaskLength;
let result = `
# Note: Do not edit this file directly.
# Your changes will be overwritten!
# Server
[Interface]
PrivateKey = ${system.interface.privateKey}
Address = ${system.interface.address}/${cidrBlock}
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(clients)) {
if (!client.enabled) continue;
result += `
# Client: ${client.name} (${clientId})
[Peer]
PublicKey = ${client.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${client.address}/32`;
}
DEBUG('Config saving...');
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
mode: 0o600,
});
DEBUG('Config saved.');
}
async #syncWireguardConfig() {
DEBUG('Config syncing...');
await exec('wg syncconf wg0 <(wg-quick strip wg0)');
DEBUG('Config synced.');
}
async getClients() {
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', {
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 client = await Database.getClient(clientId);
if (!client) {
throw createError({
statusCode: 404,
statusMessage: `Client Not Found: ${clientId}`,
});
}
return client;
}
async getClientConfiguration({ clientId }: { clientId: string }) {
const system = await Database.getSystem();
const client = await this.getClient({ clientId });
return `
[Interface]
PrivateKey = ${client.privateKey}
Address = ${client.address}
DNS = ${system.userConfig.defaultDns.join(',')}
MTU = ${system.userConfig.mtu}
[Peer]
PublicKey = ${system.interface.publicKey}
PresharedKey = ${client.preSharedKey}
AllowedIPs = ${client.allowedIPs}
PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${system.wgHost}:${system.wgConfigPort}`;
}
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 system = await Database.getSystem();
const clients = await Database.getClients();
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
const cidr = ip.cidrSubnet(system.userConfig.addressRange);
let address;
for (
let i = ip.toLong(cidr.firstAddress) + 1;
i <= ip.toLong(cidr.lastAddress) - 1;
i++
) {
const currentIp = ip.fromLong(i);
const client = Object.values(clients).find((client) => {
return client.address === currentIp;
});
if (!client) {
address = currentIp;
break;
}
}
if (!address) {
throw new Error('Maximum number of clients reached.');
}
// Create Client
const id = crypto.randomUUID();
const client: NewClient = {
id,
name,
address,
privateKey,
publicKey,
preSharedKey,
endpoint: null,
oneTimeLink: null,
expiresAt: null,
enabled: true,
allowedIPs: system.userConfig.allowedIps,
persistentKeepalive: system.userConfig.persistentKeepalive,
};
if (expireDate) {
const date = new Date(expireDate);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
client.expiresAt = date;
}
await Database.createClient(client);
await this.saveConfig();
return client;
}
async deleteClient({ clientId }: { clientId: string }) {
await Database.deleteClient(clientId);
await this.saveConfig();
}
async enableClient({ clientId }: { clientId: string }) {
await Database.toggleClient(clientId, true);
await this.saveConfig();
}
async generateOneTimeLink({ clientId }: { clientId: string }) {
const key = `${clientId}-${Math.floor(Math.random() * 1000)}`;
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 }) {
await Database.deleteOneTimeLink(clientId);
await this.saveConfig();
}
async disableClient({ clientId }: { clientId: string }) {
await Database.toggleClient(clientId, false);
await this.saveConfig();
}
async updateClientName({
clientId,
name,
}: {
clientId: string;
name: string;
}) {
await Database.updateClientName(clientId, name);
await this.saveConfig();
}
async updateClientAddress({
clientId,
address,
}: {
clientId: string;
address: string;
}) {
if (!ip.isV4Format(address)) {
throw createError({
statusCode: 400,
statusMessage: `Invalid Address: ${address}`,
});
}
await Database.updateClientAddress(clientId, address);
await this.saveConfig();
}
async updateClientExpireDate({
clientId,
expireDate,
}: {
clientId: string;
expireDate: string | null;
}) {
let updatedDate: Date | null = null;
if (expireDate) {
const date = new Date(expireDate);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
updatedDate = date;
}
await Database.updateClientExpirationDate(clientId, updatedDate);
await this.saveConfig();
}
// TODO: reimplement database restore
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.'); */
}
// TODO: reimplement database restore
async backupConfiguration() {
/* DEBUG('Starting configuration backup.');
const config = await this.getConfig();
const backup = JSON.stringify(config, null, 2);
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
async Shutdown() {
await exec('wg-quick down wg0').catch(() => {});
}
async cronJob() {
const clients = await Database.getClients();
const system = await Database.getSystem();
// Expires Feature
if (system.clientExpiration.enabled) {
for (const client of Object.values(clients)) {
if (client.enabled !== true) continue;
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(clients)) {
if (
client.oneTimeLink !== null &&
new Date() > client.oneTimeLink.expiresAt
) {
DEBUG(`Client ${client.id} One Time Link expired.`);
await Database.deleteOneTimeLink(client.id);
}
}
}
}
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();
inst.Startup().catch((v) => {
console.error(v);
process.exit(1);
});
export default inst;