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.
269 lines
7.4 KiB
269 lines
7.4 KiB
import fs from 'node:fs/promises';
|
|
import debug from 'debug';
|
|
import { encodeQR } from 'qr';
|
|
import type { InterfaceType } from '#db/repositories/interface/types';
|
|
|
|
const WG_DEBUG = debug('WireGuard');
|
|
|
|
class WireGuard {
|
|
/**
|
|
* Save and sync config
|
|
*/
|
|
async saveConfig() {
|
|
const wgInterface = await Database.interfaces.get();
|
|
await this.#saveWireguardConfig(wgInterface);
|
|
await this.#syncWireguardConfig(wgInterface);
|
|
}
|
|
|
|
/**
|
|
* Generates and saves WireGuard config from database
|
|
*
|
|
* Make sure to pass an updated InterfaceType object
|
|
*/
|
|
async #saveWireguardConfig(wgInterface: InterfaceType) {
|
|
const clients = await Database.clients.getAll();
|
|
const hooks = await Database.hooks.get();
|
|
|
|
const result = [];
|
|
result.push(
|
|
wg.generateServerInterface(wgInterface, hooks, {
|
|
enableIpv6: !WG_ENV.DISABLE_IPV6,
|
|
})
|
|
);
|
|
|
|
for (const client of clients) {
|
|
if (!client.enabled) {
|
|
continue;
|
|
}
|
|
result.push(
|
|
wg.generateServerPeer(client, {
|
|
enableIpv6: !WG_ENV.DISABLE_IPV6,
|
|
})
|
|
);
|
|
}
|
|
|
|
result.push('');
|
|
|
|
WG_DEBUG('Saving Config...');
|
|
await fs.writeFile(
|
|
`/etc/wireguard/${wgInterface.name}.conf`,
|
|
result.join('\n\n'),
|
|
{
|
|
mode: 0o600,
|
|
}
|
|
);
|
|
WG_DEBUG('Config saved successfully.');
|
|
}
|
|
|
|
async #syncWireguardConfig(wgInterface: InterfaceType) {
|
|
WG_DEBUG('Syncing Config...');
|
|
await wg.sync(wgInterface.name);
|
|
WG_DEBUG('Config synced successfully.');
|
|
}
|
|
|
|
async getClientsForUser(userId: ID) {
|
|
const wgInterface = await Database.interfaces.get();
|
|
|
|
const dbClients = await Database.clients.getForUser(userId);
|
|
|
|
const clients = dbClients.map((client) => ({
|
|
...client,
|
|
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 wg.dump(wgInterface.name);
|
|
dump.forEach(
|
|
({ publicKey, latestHandshakeAt, endpoint, transferRx, transferTx }) => {
|
|
const client = clients.find((client) => client.publicKey === publicKey);
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
client.latestHandshakeAt = latestHandshakeAt;
|
|
client.endpoint = endpoint;
|
|
client.transferRx = transferRx;
|
|
client.transferTx = transferTx;
|
|
}
|
|
);
|
|
|
|
return clients;
|
|
}
|
|
|
|
async dumpByPublicKey(publicKey: string) {
|
|
const wgInterface = await Database.interfaces.get();
|
|
|
|
const dump = await wg.dump(wgInterface.name);
|
|
const clientDump = dump.find(
|
|
({ publicKey: dumpPublicKey }) => dumpPublicKey === publicKey
|
|
);
|
|
|
|
return clientDump;
|
|
}
|
|
|
|
async getAllClients() {
|
|
const wgInterface = await Database.interfaces.get();
|
|
const dbClients = await Database.clients.getAllPublic();
|
|
const clients = dbClients.map((client) => ({
|
|
...client,
|
|
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 wg.dump(wgInterface.name);
|
|
dump.forEach(
|
|
({ publicKey, latestHandshakeAt, endpoint, transferRx, transferTx }) => {
|
|
const client = clients.find((client) => client.publicKey === publicKey);
|
|
if (!client) {
|
|
return;
|
|
}
|
|
|
|
client.latestHandshakeAt = latestHandshakeAt;
|
|
client.endpoint = endpoint;
|
|
client.transferRx = transferRx;
|
|
client.transferTx = transferTx;
|
|
}
|
|
);
|
|
|
|
return clients;
|
|
}
|
|
|
|
async getClientConfiguration({ clientId }: { clientId: ID }) {
|
|
const wgInterface = await Database.interfaces.get();
|
|
const userConfig = await Database.userConfigs.get();
|
|
|
|
const client = await Database.clients.get(clientId);
|
|
|
|
if (!client) {
|
|
throw new Error('Client not found');
|
|
}
|
|
|
|
return wg.generateClientConfig(wgInterface, userConfig, client, {
|
|
enableIpv6: !WG_ENV.DISABLE_IPV6,
|
|
});
|
|
}
|
|
|
|
async getClientQRCodeSVG({ clientId }: { clientId: ID }) {
|
|
const config = await this.getClientConfiguration({ clientId });
|
|
return encodeQR(config, 'svg', {
|
|
ecc: 'high',
|
|
scale: 2,
|
|
encoding: 'byte',
|
|
});
|
|
}
|
|
|
|
async Startup() {
|
|
WG_DEBUG('Starting WireGuard...');
|
|
// let as it has to refetch if keys change
|
|
let wgInterface = await Database.interfaces.get();
|
|
|
|
// default interface has no keys
|
|
if (
|
|
wgInterface.privateKey === '---default---' &&
|
|
wgInterface.publicKey === '---default---'
|
|
) {
|
|
WG_DEBUG('Generating new Wireguard Keys...');
|
|
const privateKey = await wg.generatePrivateKey();
|
|
const publicKey = await wg.getPublicKey(privateKey);
|
|
|
|
await Database.interfaces.updateKeyPair(privateKey, publicKey);
|
|
wgInterface = await Database.interfaces.get();
|
|
WG_DEBUG('New Wireguard Keys generated successfully.');
|
|
}
|
|
WG_DEBUG(`Starting Wireguard Interface ${wgInterface.name}...`);
|
|
await this.#saveWireguardConfig(wgInterface);
|
|
await wg.down(wgInterface.name).catch(() => {});
|
|
await wg.up(wgInterface.name).catch((err) => {
|
|
if (
|
|
err &&
|
|
err.message &&
|
|
err.message.includes(`Cannot find device "${wgInterface.name}"`)
|
|
) {
|
|
throw new Error(
|
|
`WireGuard exited with the error: Cannot find device "${wgInterface.name}"\nThis usually means that your host's kernel does not support WireGuard!`,
|
|
{ cause: err.message }
|
|
);
|
|
}
|
|
|
|
throw err;
|
|
});
|
|
await this.#syncWireguardConfig(wgInterface);
|
|
WG_DEBUG(`Wireguard Interface ${wgInterface.name} started successfully.`);
|
|
|
|
WG_DEBUG('Starting Cron Job...');
|
|
await this.startCronJob();
|
|
WG_DEBUG('Cron Job started successfully.');
|
|
}
|
|
|
|
// TODO: handle as worker_thread
|
|
async startCronJob() {
|
|
setIntervalImmediately(() => {
|
|
this.cronJob().catch((err) => {
|
|
WG_DEBUG('Running Cron Job failed.');
|
|
console.error(err);
|
|
});
|
|
}, 60 * 1000);
|
|
}
|
|
|
|
// Shutdown wireguard
|
|
async Shutdown() {
|
|
const wgInterface = await Database.interfaces.get();
|
|
await wg.down(wgInterface.name).catch(() => {});
|
|
}
|
|
|
|
async Restart() {
|
|
const wgInterface = await Database.interfaces.get();
|
|
await wg.restart(wgInterface.name);
|
|
}
|
|
|
|
async cronJob() {
|
|
const clients = await Database.clients.getAll();
|
|
let needsSave = false;
|
|
// Expires Feature
|
|
for (const client of clients) {
|
|
if (client.enabled !== true) continue;
|
|
if (
|
|
client.expiresAt !== null &&
|
|
new Date() > new Date(client.expiresAt)
|
|
) {
|
|
WG_DEBUG(`Client ${client.id} expired.`);
|
|
await Database.clients.toggle(client.id, false);
|
|
needsSave = true;
|
|
}
|
|
}
|
|
// One Time Link Feature
|
|
for (const client of clients) {
|
|
if (
|
|
client.oneTimeLink !== null &&
|
|
new Date() > new Date(client.oneTimeLink.expiresAt)
|
|
) {
|
|
WG_DEBUG(`OneTimeLink for Client ${client.id} expired.`);
|
|
await Database.oneTimeLinks.delete(client.id);
|
|
// otl does not need wireguard sync
|
|
}
|
|
}
|
|
|
|
if (needsSave) {
|
|
await this.saveConfig();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (OLD_ENV.PASSWORD || OLD_ENV.PASSWORD_HASH) {
|
|
throw new Error(
|
|
`
|
|
You are using an invalid Configuration for wg-easy
|
|
Please follow the instructions on https://wg-easy.github.io/wg-easy/latest/advanced/migrate/from-14-to-15/ to migrate
|
|
`
|
|
);
|
|
}
|
|
|
|
// TODO: make static or object
|
|
|
|
export default new WireGuard();
|
|
|