Browse Source

update wireguard

pull/1345/head
Bernd Storath 11 months ago
parent
commit
cc0e96b5a4
  1. 322
      src/server/utils/WireGuard.ts
  2. 1
      src/services/database/lowdb.ts
  3. 1
      src/services/database/migrations/1.ts
  4. 43
      src/services/database/repositories/client.ts
  5. 27
      src/services/database/repositories/database.ts

322
src/server/utils/WireGuard.ts

@ -1,127 +1,39 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'path'; import path from 'path';
import debug_logger from 'debug'; import debug from 'DEBUG';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import CRC32 from 'crc-32'; import CRC32 from 'crc-32';
const debug = debug_logger('WireGuard'); import type { NewClient } from '~~/services/database/repositories/client';
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 { const DEBUG = debug('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;
}
class WireGuard {
async saveConfig() { async saveConfig() {
const config = await this.getConfig(); await this.#saveWireguardConfig();
await this.__saveConfig(config); await this.#syncWireguardConfig();
await this.__syncConfig();
} }
async __saveConfig(config: Config) { async #saveWireguardConfig() {
const system = await Database.getSystem();
const clients = await Database.getClients();
let result = ` let result = `
# Note: Do not edit this file directly. # Note: Do not edit this file directly.
# Your changes will be overwritten! # Your changes will be overwritten!
# Server # Server
[Interface] [Interface]
PrivateKey = ${config.server.privateKey} PrivateKey = ${system.interface.privateKey}
Address = ${config.server.address}/24 Address = ${system.interface.address}/24
ListenPort = ${WG_PORT} ListenPort = ${system.wgPort}
PreUp = ${WG_PRE_UP} PreUp = ${system.iptables.PreUp}
PostUp = ${WG_POST_UP} PostUp = ${system.iptables.PostUp}
PreDown = ${WG_PRE_DOWN} PreDown = ${system.iptables.PreDown}
PostDown = ${WG_POST_DOWN} PostDown = ${system.iptables.PostDown}
`; `;
for (const [clientId, client] of Object.entries(config.clients)) { for (const [clientId, client] of Object.entries(clients)) {
if (!client.enabled) continue; if (!client.enabled) continue;
result += ` result += `
@ -134,49 +46,39 @@ ${
}AllowedIPs = ${client.address}/32`; }AllowedIPs = ${client.address}/32`;
} }
debug('Config saving...'); 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, { await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
mode: 0o600, mode: 0o600,
}); });
debug('Config saved.'); DEBUG('Config saved.');
} }
async __syncConfig() { async #syncWireguardConfig() {
debug('Config syncing...'); DEBUG('Config syncing...');
await exec('wg syncconf wg0 <(wg-quick strip wg0)'); await exec('wg syncconf wg0 <(wg-quick strip wg0)');
debug('Config synced.'); DEBUG('Config synced.');
} }
async getClients() { async getClients() {
const config = await this.getConfig(); const dbClients = await Database.getClients();
const clients = Object.entries(config.clients).map( const clients = Object.entries(dbClients).map(([clientId, client]) => ({
([clientId, client]) => ({ id: clientId,
id: clientId, name: client.name,
name: client.name, enabled: client.enabled,
enabled: client.enabled, address: client.address,
address: client.address, publicKey: client.publicKey,
publicKey: client.publicKey, createdAt: new Date(client.createdAt),
createdAt: new Date(client.createdAt), updatedAt: new Date(client.updatedAt),
updatedAt: new Date(client.updatedAt), expiresAt: client.expiresAt,
expireAt: client.expireAt !== null ? new Date(client.expireAt) : null, allowedIPs: client.allowedIPs,
allowedIPs: client.allowedIPs, oneTimeLink: client.oneTimeLink,
oneTimeLink: client.oneTimeLink ?? null, downloadableConfig: 'privateKey' in client,
oneTimeLinkExpiresAt: client.oneTimeLinkExpiresAt ?? null, persistentKeepalive: null as string | null,
downloadableConfig: 'privateKey' in client, latestHandshakeAt: null as Date | null,
persistentKeepalive: null as string | null, endpoint: null as string | null,
latestHandshakeAt: null as Date | null, transferRx: null as number | null,
endpoint: null as string | null, transferTx: null as number | null,
transferRx: null as number | null, }));
transferTx: null as number | null,
})
);
// Loop WireGuard status // Loop WireGuard status
const dump = await exec('wg show wg0 dump', { const dump = await exec('wg show wg0 dump', {
@ -215,8 +117,7 @@ ${
} }
async getClient({ clientId }: { clientId: string }) { async getClient({ clientId }: { clientId: string }) {
const config = await this.getConfig(); const client = await Database.getClient(clientId);
const client = config.clients[clientId];
if (!client) { if (!client) {
throw createError({ throw createError({
statusCode: 404, statusCode: 404,
@ -228,7 +129,7 @@ ${
} }
async getClientConfiguration({ clientId }: { clientId: string }) { async getClientConfiguration({ clientId }: { clientId: string }) {
const config = await this.getConfig(); const system = await Database.getSystem();
const client = await this.getClient({ clientId }); const client = await this.getClient({ clientId });
return ` return `
@ -239,11 +140,11 @@ ${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\
${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\ ${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\
[Peer] [Peer]
PublicKey = ${config.server.publicKey} PublicKey = ${system.interface.publicKey}
${ ${
client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
}AllowedIPs = ${WG_ALLOWED_IPS} }AllowedIPs = ${client.allowedIPs}
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE} PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
} }
@ -266,7 +167,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
throw new Error('Missing: Name'); throw new Error('Missing: Name');
} }
const config = await this.getConfig(); const clients = await Database.getClients();
const privateKey = await exec('wg genkey'); const privateKey = await exec('wg genkey');
const publicKey = await exec(`echo ${privateKey} | wg pubkey`, { const publicKey = await exec(`echo ${privateKey} | wg pubkey`, {
@ -274,10 +175,11 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}); });
const preSharedKey = await exec('wg genpsk'); const preSharedKey = await exec('wg genpsk');
// TODO: cidr
// Calculate next IP // Calculate next IP
let address; let address;
for (let i = 2; i < 255; i++) { for (let i = 2; i < 255; i++) {
const client = Object.values(config.clients).find((client) => { const client = Object.values(clients).find((client) => {
return client.address === WG_DEFAULT_ADDRESS.replace('x', i.toString()); return client.address === WG_DEFAULT_ADDRESS.replace('x', i.toString());
}); });
@ -293,22 +195,20 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
// Create Client // Create Client
const id = crypto.randomUUID(); const id = crypto.randomUUID();
const client: Client = {
const client: NewClient = {
id, id,
name, name,
address, address,
privateKey, privateKey,
publicKey, publicKey,
preSharedKey, preSharedKey,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
endpoint: null, endpoint: null,
oneTimeLink: null, oneTimeLink: null,
oneTimeLinkExpiresAt: null, expiresAt: null,
expireAt: null,
enabled: true, enabled: true,
allowedIPs: WG_ALLOWED_IPS.split(', '),
persistentKeepalive: Number(WG_PERSISTENT_KEEPALIVE),
}; };
if (expireDate) { if (expireDate) {
@ -316,10 +216,10 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
date.setHours(23); date.setHours(23);
date.setMinutes(59); date.setMinutes(59);
date.setSeconds(59); date.setSeconds(59);
client.expireAt = date.toISOString(); client.expiresAt = date;
} }
config.clients[id] = client; await Database.createClient(client);
await this.saveConfig(); await this.saveConfig();
@ -327,48 +227,34 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
} }
async deleteClient({ clientId }: { clientId: string }) { async deleteClient({ clientId }: { clientId: string }) {
const config = await this.getConfig(); await Database.deleteClient(clientId);
await this.saveConfig();
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 }) { async enableClient({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId }); await Database.toggleClient(clientId, true);
client.enabled = true;
client.updatedAt = new Date().toISOString();
await this.saveConfig(); await this.saveConfig();
} }
async generateOneTimeLink({ clientId }: { clientId: string }) { async generateOneTimeLink({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
const key = `${clientId}-${Math.floor(Math.random() * 1000)}`; const key = `${clientId}-${Math.floor(Math.random() * 1000)}`;
client.oneTimeLink = Math.abs(CRC32.str(key)).toString(16); const oneTimeLink = Math.abs(CRC32.str(key)).toString(16);
client.oneTimeLinkExpiresAt = new Date( const expiresAt = new Date(Date.now() + 5 * 60 * 1000);
Date.now() + 5 * 60 * 1000 await Database.createOneTimeLink(clientId, {
).toISOString(); oneTimeLink,
client.updatedAt = new Date().toISOString(); expiresAt,
});
await this.saveConfig(); await this.saveConfig();
} }
async eraseOneTimeLink({ clientId }: { clientId: string }) { async eraseOneTimeLink({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId }); await Database.deleteOneTimeLink(clientId);
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date().toISOString();
await this.saveConfig(); await this.saveConfig();
} }
async disableClient({ clientId }: { clientId: string }) { async disableClient({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId }); await Database.toggleClient(clientId, false);
client.enabled = false;
client.updatedAt = new Date().toISOString();
await this.saveConfig(); await this.saveConfig();
} }
@ -380,10 +266,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId: string; clientId: string;
name: string; name: string;
}) { }) {
const client = await this.getClient({ clientId }); await Database.updateClientName(clientId, name);
client.name = name;
client.updatedAt = new Date().toISOString();
await this.saveConfig(); await this.saveConfig();
} }
@ -395,8 +278,6 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId: string; clientId: string;
address: string; address: string;
}) { }) {
const client = await this.getClient({ clientId });
if (!isValidIPv4(address)) { if (!isValidIPv4(address)) {
throw createError({ throw createError({
statusCode: 400, statusCode: 400,
@ -404,8 +285,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}); });
} }
client.address = address; await Database.updateClientAddress(clientId, address);
client.updatedAt = new Date().toISOString();
await this.saveConfig(); await this.saveConfig();
} }
@ -417,42 +297,38 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId: string; clientId: string;
expireDate: string | null; expireDate: string | null;
}) { }) {
const client = await this.getClient({ clientId }); let updatedDate: Date | null = null;
if (expireDate) { if (expireDate) {
const date = new Date(expireDate); const date = new Date(expireDate);
date.setHours(23); date.setHours(23);
date.setMinutes(59); date.setMinutes(59);
date.setSeconds(59); date.setSeconds(59);
client.expireAt = date.toISOString(); updatedDate = date;
} else {
client.expireAt = null;
} }
client.updatedAt = new Date().toISOString();
await this.saveConfig(); await Database.updateClientExpirationDate(clientId, updatedDate);
}
async __reloadConfig() { await this.saveConfig();
await this.__buildConfig();
await this.__syncConfig();
} }
async restoreConfiguration(config: string) { // TODO: reimplement database restore
debug('Starting configuration restore process.'); async restoreConfiguration(_config: string) {
/* DEBUG('Starting configuration restore process.');
// TODO: sanitize config // TODO: sanitize config
const _config = JSON.parse(config); const _config = JSON.parse(config);
await this.__saveConfig(_config); await this.__saveConfig(_config);
await this.__reloadConfig(); await this.__reloadConfig();
debug('Configuration restore process completed.'); DEBUG('Configuration restore process completed.'); */
} }
// TODO: reimplement database restore
async backupConfiguration() { async backupConfiguration() {
debug('Starting configuration backup.'); /* DEBUG('Starting configuration backup.');
const config = await this.getConfig(); const config = await this.getConfig();
const backup = JSON.stringify(config, null, 2); const backup = JSON.stringify(config, null, 2);
debug('Configuration backup completed.'); DEBUG('Configuration backup completed.');
return backup; return backup; */
} }
// Shutdown wireguard // Shutdown wireguard
@ -461,46 +337,30 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
} }
async cronJobEveryMinute() { async cronJobEveryMinute() {
const config = await this.getConfig(); const clients = await Database.getClients();
const system = await Database.getSystem(); const system = await Database.getSystem();
if (!system) {
throw new Error('Invalid Database');
}
let needSaveConfig = false;
// Expires Feature // Expires Feature
if (system.clientExpiration.enabled) { if (system.clientExpiration.enabled) {
for (const client of Object.values(config.clients)) { for (const client of Object.values(clients)) {
if (client.enabled !== true) continue; if (client.enabled !== true) continue;
if ( if (client.expiresAt !== null && new Date() > client.expiresAt) {
client.expireAt !== null && DEBUG(`Client ${client.id} expired.`);
new Date() > new Date(client.expireAt) await Database.toggleClient(client.id, false);
) {
debug(`Client ${client.id} expired.`);
needSaveConfig = true;
client.enabled = false;
client.updatedAt = new Date().toISOString();
} }
} }
} }
// One Time Link Feature // One Time Link Feature
if (system.oneTimeLinks.enabled) { if (system.oneTimeLinks.enabled) {
for (const client of Object.values(config.clients)) { for (const client of Object.values(clients)) {
if ( if (
client.oneTimeLink !== null && client.oneTimeLink !== null &&
client.oneTimeLinkExpiresAt !== null && new Date() > client.oneTimeLink.expiresAt
new Date() > new Date(client.oneTimeLinkExpiresAt)
) { ) {
debug(`Client ${client.id} One Time Link expired.`); DEBUG(`Client ${client.id} One Time Link expired.`);
needSaveConfig = true; await Database.deleteOneTimeLink(client.id);
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date().toISOString();
} }
} }
} }
if (needSaveConfig) {
await this.saveConfig();
}
} }
async getMetrics() { async getMetrics() {
@ -582,7 +442,7 @@ const inst = new WireGuard();
// This also has to also start the WireGuard Server // This also has to also start the WireGuard Server
async function cronJobEveryMinute() { async function cronJobEveryMinute() {
await inst.cronJobEveryMinute().catch((err) => { await inst.cronJobEveryMinute().catch((err) => {
debug('Running Cron Job failed.'); DEBUG('Running Cron Job failed.');
console.error(err); console.error(err);
}); });
setTimeout(cronJobEveryMinute, 60 * 1000); setTimeout(cronJobEveryMinute, 60 * 1000);

1
src/services/database/lowdb.ts

@ -57,6 +57,7 @@ export default class LowDB extends DatabaseProvider {
return system; return system;
} }
// TODO: return copy to avoid mutation (everywhere)
async getUsers() { async getUsers() {
return this.#db.data.users; return this.#db.data.users;
} }

1
src/services/database/migrations/1.ts

@ -83,6 +83,7 @@ export async function run1(db: Low<Database>) {
}, },
}, },
users: [], users: [],
clients: [],
}; };
db.data = database; db.data = database;

43
src/services/database/repositories/client.ts

@ -0,0 +1,43 @@
export type OneTimeLink = {
oneTimeLink: string;
expiresAt: Date;
};
export type Client = {
id: string;
name: string;
address: string;
privateKey: string;
publicKey: string;
preSharedKey: string;
expiresAt: Date | null;
endpoint: string | null;
allowedIPs: string[];
oneTimeLink: OneTimeLink | null;
createdAt: Date;
updatedAt: Date;
enabled: boolean;
persistentKeepalive: number;
};
export type NewClient = Omit<Client, 'createdAt' | 'updatedAt'>;
/**
* Interface for client-related database operations.
* This interface provides methods for managing client data.
*/
export interface ClientRepository {
getClients(): Promise<Record<string, Client>>;
getClient(id: string): Promise<Client | undefined>;
createClient(client: NewClient): Promise<void>;
deleteClient(id: string): Promise<void>;
toggleClient(id: string, enable: boolean): Promise<void>;
updateClientName(id: string, name: string): Promise<void>;
updateClientAddress(id: string, address: string): Promise<void>;
updateClientExpirationDate(
id: string,
expirationDate: Date | null
): Promise<void>;
deleteOneTimeLink(id: string): Promise<void>;
createOneTimeLink(id: string, oneTimeLink: OneTimeLink): Promise<void>;
}

27
src/services/database/repositories/database.ts

@ -1,3 +1,9 @@
import type {
ClientRepository,
Client,
NewClient,
OneTimeLink,
} from './client';
import type { System, SystemRepository } from './system'; import type { System, SystemRepository } from './system';
import type { User, UserRepository } from './user'; import type { User, UserRepository } from './user';
@ -6,12 +12,14 @@ export type Database = {
migrations: string[]; migrations: string[];
system: System; system: System;
users: User[]; users: User[];
clients: Record<string, Client>;
}; };
export const DEFAULT_DATABASE: Database = { export const DEFAULT_DATABASE: Database = {
migrations: [], migrations: [],
system: null as never, system: null as never,
users: [], users: [],
clients: {},
}; };
/** /**
@ -22,7 +30,7 @@ export const DEFAULT_DATABASE: Database = {
* *
*/ */
export abstract class DatabaseProvider export abstract class DatabaseProvider
implements SystemRepository, UserRepository implements SystemRepository, UserRepository, ClientRepository
{ {
/** /**
* Connects to the database. * Connects to the database.
@ -44,6 +52,23 @@ export abstract class DatabaseProvider
): Promise<void>; ): Promise<void>;
abstract updateUser(user: User): Promise<void>; abstract updateUser(user: User): Promise<void>;
abstract deleteUser(id: string): Promise<void>; abstract deleteUser(id: string): Promise<void>;
abstract getClients(): Promise<Record<string, Client>>;
abstract getClient(id: string): Promise<Client | undefined>;
abstract createClient(client: NewClient): Promise<void>;
abstract deleteClient(id: string): Promise<void>;
abstract toggleClient(id: string, enable: boolean): Promise<void>;
abstract updateClientName(id: string, name: string): Promise<void>;
abstract updateClientAddress(id: string, address: string): Promise<void>;
abstract updateClientExpirationDate(
id: string,
expirationDate: Date | null
): Promise<void>;
abstract deleteOneTimeLink(id: string): Promise<void>;
abstract createOneTimeLink(
id: string,
oneTimeLink: OneTimeLink
): Promise<void>;
} }
/** /**

Loading…
Cancel
Save