mirror of https://github.com/wg-easy/wg-easy
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
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;
|
|
|