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 path from 'path';
import debug_logger from 'debug';
import debug 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>;
};
import type { NewClient } from '~~/services/database/repositories/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;
}
const DEBUG = debug('WireGuard');
class WireGuard {
async saveConfig() {
const config = await this.getConfig();
await this.__saveConfig(config);
await this.__syncConfig();
await this.#saveWireguardConfig();
await this.#syncWireguardConfig();
}
async __saveConfig(config: Config) {
async #saveWireguardConfig() {
const system = await Database.getSystem();
const clients = await Database.getClients();
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}
PrivateKey = ${system.interface.privateKey}
Address = ${system.interface.address}/24
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(config.clients)) {
for (const [clientId, client] of Object.entries(clients)) {
if (!client.enabled) continue;
result += `
@ -134,49 +46,39 @@ ${
}AllowedIPs = ${client.address}/32`;
}
debug('Config saving...');
await fs.writeFile(
path.join(WG_PATH, 'wg0.json'),
JSON.stringify(config, undefined, 2),
{
mode: 0o660,
}
);
DEBUG('Config saving...');
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
mode: 0o600,
});
debug('Config saved.');
DEBUG('Config saved.');
}
async __syncConfig() {
debug('Config syncing...');
async #syncWireguardConfig() {
DEBUG('Config syncing...');
await exec('wg syncconf wg0 <(wg-quick strip wg0)');
debug('Config synced.');
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,
})
);
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', {
@ -215,8 +117,7 @@ ${
}
async getClient({ clientId }: { clientId: string }) {
const config = await this.getConfig();
const client = config.clients[clientId];
const client = await Database.getClient(clientId);
if (!client) {
throw createError({
statusCode: 404,
@ -228,7 +129,7 @@ ${
}
async getClientConfiguration({ clientId }: { clientId: string }) {
const config = await this.getConfig();
const system = await Database.getSystem();
const client = await this.getClient({ clientId });
return `
@ -239,11 +140,11 @@ ${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\
${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\
[Peer]
PublicKey = ${config.server.publicKey}
PublicKey = ${system.interface.publicKey}
${
client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
}AllowedIPs = ${WG_ALLOWED_IPS}
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
}AllowedIPs = ${client.allowedIPs}
PersistentKeepalive = ${client.persistentKeepalive}
Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}
@ -266,7 +167,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
throw new Error('Missing: Name');
}
const config = await this.getConfig();
const clients = await Database.getClients();
const privateKey = await exec('wg genkey');
const publicKey = await exec(`echo ${privateKey} | wg pubkey`, {
@ -274,10 +175,11 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
});
const preSharedKey = await exec('wg genpsk');
// TODO: cidr
// Calculate next IP
let address;
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());
});
@ -293,22 +195,20 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
// Create Client
const id = crypto.randomUUID();
const client: Client = {
const client: NewClient = {
id,
name,
address,
privateKey,
publicKey,
preSharedKey,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
endpoint: null,
oneTimeLink: null,
oneTimeLinkExpiresAt: null,
expireAt: null,
expiresAt: null,
enabled: true,
allowedIPs: WG_ALLOWED_IPS.split(', '),
persistentKeepalive: Number(WG_PERSISTENT_KEEPALIVE),
};
if (expireDate) {
@ -316,10 +216,10 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
client.expireAt = date.toISOString();
client.expiresAt = date;
}
config.clients[id] = client;
await Database.createClient(client);
await this.saveConfig();
@ -327,48 +227,34 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}
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();
}
await Database.deleteClient(clientId);
await this.saveConfig();
}
async enableClient({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
client.enabled = true;
client.updatedAt = new Date().toISOString();
await Database.toggleClient(clientId, true);
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();
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 }) {
const client = await this.getClient({ clientId });
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date().toISOString();
await Database.deleteOneTimeLink(clientId);
await this.saveConfig();
}
async disableClient({ clientId }: { clientId: string }) {
const client = await this.getClient({ clientId });
client.enabled = false;
client.updatedAt = new Date().toISOString();
await Database.toggleClient(clientId, false);
await this.saveConfig();
}
@ -380,10 +266,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId: string;
name: string;
}) {
const client = await this.getClient({ clientId });
client.name = name;
client.updatedAt = new Date().toISOString();
await Database.updateClientName(clientId, name);
await this.saveConfig();
}
@ -395,8 +278,6 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId: string;
address: string;
}) {
const client = await this.getClient({ clientId });
if (!isValidIPv4(address)) {
throw createError({
statusCode: 400,
@ -404,8 +285,7 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
});
}
client.address = address;
client.updatedAt = new Date().toISOString();
await Database.updateClientAddress(clientId, address);
await this.saveConfig();
}
@ -417,42 +297,38 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
clientId: string;
expireDate: string | null;
}) {
const client = await this.getClient({ clientId });
let updatedDate: Date | null = null;
if (expireDate) {
const date = new Date(expireDate);
date.setHours(23);
date.setMinutes(59);
date.setSeconds(59);
client.expireAt = date.toISOString();
} else {
client.expireAt = null;
updatedDate = date;
}
client.updatedAt = new Date().toISOString();
await this.saveConfig();
}
await Database.updateClientExpirationDate(clientId, updatedDate);
async __reloadConfig() {
await this.__buildConfig();
await this.__syncConfig();
await this.saveConfig();
}
async restoreConfiguration(config: string) {
debug('Starting configuration restore process.');
// 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.');
DEBUG('Configuration restore process completed.'); */
}
// TODO: reimplement database restore
async backupConfiguration() {
debug('Starting configuration backup.');
/* DEBUG('Starting configuration backup.');
const config = await this.getConfig();
const backup = JSON.stringify(config, null, 2);
debug('Configuration backup completed.');
return backup;
DEBUG('Configuration backup completed.');
return backup; */
}
// Shutdown wireguard
@ -461,46 +337,30 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}
async cronJobEveryMinute() {
const config = await this.getConfig();
const clients = await Database.getClients();
const system = await Database.getSystem();
if (!system) {
throw new Error('Invalid Database');
}
let needSaveConfig = false;
// Expires Feature
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.expireAt !== null &&
new Date() > new Date(client.expireAt)
) {
debug(`Client ${client.id} expired.`);
needSaveConfig = true;
client.enabled = false;
client.updatedAt = new Date().toISOString();
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(config.clients)) {
for (const client of Object.values(clients)) {
if (
client.oneTimeLink !== null &&
client.oneTimeLinkExpiresAt !== null &&
new Date() > new Date(client.oneTimeLinkExpiresAt)
new Date() > client.oneTimeLink.expiresAt
) {
debug(`Client ${client.id} One Time Link expired.`);
needSaveConfig = true;
client.oneTimeLink = null;
client.oneTimeLinkExpiresAt = null;
client.updatedAt = new Date().toISOString();
DEBUG(`Client ${client.id} One Time Link expired.`);
await Database.deleteOneTimeLink(client.id);
}
}
}
if (needSaveConfig) {
await this.saveConfig();
}
}
async getMetrics() {
@ -582,7 +442,7 @@ 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.');
DEBUG('Running Cron Job failed.');
console.error(err);
});
setTimeout(cronJobEveryMinute, 60 * 1000);

1
src/services/database/lowdb.ts

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

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

@ -83,6 +83,7 @@ export async function run1(db: Low<Database>) {
},
},
users: [],
clients: [],
};
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 { User, UserRepository } from './user';
@ -6,12 +12,14 @@ export type Database = {
migrations: string[];
system: System;
users: User[];
clients: Record<string, Client>;
};
export const DEFAULT_DATABASE: Database = {
migrations: [],
system: null as never,
users: [],
clients: {},
};
/**
@ -22,7 +30,7 @@ export const DEFAULT_DATABASE: Database = {
*
*/
export abstract class DatabaseProvider
implements SystemRepository, UserRepository
implements SystemRepository, UserRepository, ClientRepository
{
/**
* Connects to the database.
@ -44,6 +52,23 @@ export abstract class DatabaseProvider
): Promise<void>;
abstract updateUser(user: User): 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