Files
docker-wireguard/src/lib/WireGuard.js
adrien a082a40bf6 config: Add support for custom client port configuration
This commit introduces the ability to specify a custom port for the client
configuration. This feature is particularly useful when the WireGuard server
is behind a port forwarding setup, allowing clients to connect using the
correct port number.

With this change, users can now define the desired client port in the
configuration file, ensuring seamless connectivity even in scenarios where
the client's listening port differs from the standard WireGuard port.
2024-06-07 12:53:54 +02:00

328 lines
8.4 KiB
JavaScript

'use strict';
const fs = require('node:fs/promises');
const path = require('path');
const debug = require('debug')('WireGuard');
const crypto = require('node:crypto');
const QRCode = require('qrcode');
const Util = require('./Util');
const ServerError = require('./ServerError');
const {
WG_PATH,
WG_HOST,
WG_PORT,
WG_CONFIG_PORT,
WG_MTU,
WG_DEFAULT_DNS,
WG_DEFAULT_ADDRESS,
WG_PERSISTENT_KEEPALIVE,
WG_ALLOWED_IPS,
WG_PRE_UP,
WG_POST_UP,
WG_PRE_DOWN,
WG_POST_DOWN,
} = require('../config');
module.exports = class WireGuard {
async getConfig() {
if (!this.__configPromise) {
this.__configPromise = Promise.resolve().then(async () => {
if (!WG_HOST) {
throw new Error('WG_HOST Environment Variable Not Set!');
}
debug('Loading configuration...');
let config;
try {
config = await fs.readFile(path.join(WG_PATH, 'wg0.json'), 'utf8');
config = JSON.parse(config);
debug('Configuration loaded.');
} catch (err) {
const privateKey = await Util.exec('wg genkey');
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
log: 'echo ***hidden*** | wg pubkey',
});
const address = WG_DEFAULT_ADDRESS.replace('x', '1');
config = {
server: {
privateKey,
publicKey,
address,
},
clients: {},
};
debug('Configuration generated.');
}
await this.__saveConfig(config);
await Util.exec('wg-quick down wg0').catch(() => { });
await Util.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();
return config;
});
}
return this.__configPromise;
}
async saveConfig() {
const config = await this.getConfig();
await this.__saveConfig(config);
await this.__syncConfig();
}
async __saveConfig(config) {
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}
`;
for (const [clientId, client] of Object.entries(config.clients)) {
if (!client.enabled) continue;
result += `
# Client: ${client.name} (${clientId})
[Peer]
PublicKey = ${client.publicKey}
${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
}AllowedIPs = ${client.address}/32`;
}
debug('Config saving...');
await fs.writeFile(path.join(WG_PATH, 'wg0.json'), JSON.stringify(config, false, 2), {
mode: 0o660,
});
await fs.writeFile(path.join(WG_PATH, 'wg0.conf'), result, {
mode: 0o600,
});
debug('Config saved.');
}
async __syncConfig() {
debug('Config syncing...');
await Util.exec('wg syncconf wg0 <(wg-quick strip wg0)');
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),
allowedIPs: client.allowedIPs,
downloadableConfig: 'privateKey' in client,
persistentKeepalive: null,
latestHandshakeAt: null,
transferRx: null,
transferTx: null,
}));
// Loop WireGuard status
const dump = await Util.exec('wg show wg0 dump', {
log: false,
});
dump
.trim()
.split('\n')
.slice(1)
.forEach((line) => {
const [
publicKey,
preSharedKey, // eslint-disable-line no-unused-vars
endpoint, // eslint-disable-line no-unused-vars
allowedIps, // eslint-disable-line no-unused-vars
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.transferRx = Number(transferRx);
client.transferTx = Number(transferTx);
client.persistentKeepalive = persistentKeepalive;
});
return clients;
}
async getClient({ clientId }) {
const config = await this.getConfig();
const client = config.clients[clientId];
if (!client) {
throw new ServerError(`Client Not Found: ${clientId}`, 404);
}
return client;
}
async getClientConfiguration({ clientId }) {
const config = await this.getConfig();
const client = await this.getClient({ clientId });
return `
[Interface]
PrivateKey = ${client.privateKey ? `${client.privateKey}` : 'REPLACE_ME'}
Address = ${client.address}/24
${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\
${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\
[Peer]
PublicKey = ${config.server.publicKey}
${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
}AllowedIPs = ${WG_ALLOWED_IPS}
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}
async getClientQRCodeSVG({ clientId }) {
const config = await this.getClientConfiguration({ clientId });
return QRCode.toString(config, {
type: 'svg',
width: 512,
});
}
async createClient({ name }) {
if (!name) {
throw new Error('Missing: Name');
}
const config = await this.getConfig();
const privateKey = await Util.exec('wg genkey');
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`);
const preSharedKey = await Util.exec('wg genpsk');
// Calculate next IP
let address;
for (let i = 2; i < 255; i++) {
const client = Object.values(config.clients).find((client) => {
return client.address === WG_DEFAULT_ADDRESS.replace('x', i);
});
if (!client) {
address = WG_DEFAULT_ADDRESS.replace('x', i);
break;
}
}
if (!address) {
throw new Error('Maximum number of clients reached.');
}
// Create Client
const id = crypto.randomUUID();
const client = {
id,
name,
address,
privateKey,
publicKey,
preSharedKey,
createdAt: new Date(),
updatedAt: new Date(),
enabled: true,
};
config.clients[id] = client;
await this.saveConfig();
return client;
}
async deleteClient({ clientId }) {
const config = await this.getConfig();
if (config.clients[clientId]) {
delete config.clients[clientId];
await this.saveConfig();
}
}
async enableClient({ clientId }) {
const client = await this.getClient({ clientId });
client.enabled = true;
client.updatedAt = new Date();
await this.saveConfig();
}
async disableClient({ clientId }) {
const client = await this.getClient({ clientId });
client.enabled = false;
client.updatedAt = new Date();
await this.saveConfig();
}
async updateClientName({ clientId, name }) {
const client = await this.getClient({ clientId });
client.name = name;
client.updatedAt = new Date();
await this.saveConfig();
}
async updateClientAddress({ clientId, address }) {
const client = await this.getClient({ clientId });
if (!Util.isValidIPv4(address)) {
throw new ServerError(`Invalid Address: ${address}`, 400);
}
client.address = address;
client.updatedAt = new Date();
await this.saveConfig();
}
// Shutdown wireguard
async Shutdown() {
await Util.exec('wg-quick down wg0').catch(() => { });
}
};