diff --git a/src/config.js b/src/config.js index bf7b84b4..bf83f027 100644 --- a/src/config.js +++ b/src/config.js @@ -14,18 +14,36 @@ module.exports.WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_POR module.exports.WG_MTU = process.env.WG_MTU || null; module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '0'; module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.8.0.x'; +module.exports.WG_DEFAULT_ADDRESS6 = process.env.WG_DEFAULT_ADDRESS6 || 'fdcc:ad94:bacf:61a4::cafe:x'; module.exports.WG_DEFAULT_DNS = typeof process.env.WG_DEFAULT_DNS === 'string' ? process.env.WG_DEFAULT_DNS : '1.1.1.1'; +module.exports.WG_DEFAULT_DNS6 = typeof process.env.WG_DEFAULT_DNS6 === 'string' + ? process.env.WG_DEFAULT_DNS6 + : '2606:4700:4700::1111'; module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0'; -module.exports.WG_PRE_UP = process.env.WG_PRE_UP || ''; -module.exports.WG_POST_UP = process.env.WG_POST_UP || ` -iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; -iptables -A INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; -iptables -A FORWARD -i wg0 -j ACCEPT; -iptables -A FORWARD -o wg0 -j ACCEPT; -`.split('\n').join(' '); +// Set WG_POST_UP to allow IPv6 NAT and forwarding only if the required kernel module is available +const modules = childProcess.execSync('lsmod', { + shell: 'bash', +}); +module.exports.WG_POST_UP = process.env.WG_POST_UP; +if (!process.env.WG_POST_UP) { + module.exports.WG_POST_UP = ` + iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o eth0 -j MASQUERADE; + iptables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT; + iptables -A FORWARD -i wg0 -j ACCEPT; + iptables -A FORWARD -o wg0 -j ACCEPT;`; + + if (modules.includes('ip6table_nat')) { + module.exports.WG_POST_UP += `ip6tables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS6.replace('x', '0')}/64 -o eth0 -j MASQUERADE; + ip6tables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT; + ip6tables -A FORWARD -i wg0 -j ACCEPT; + ip6tables -A FORWARD -o wg0 -j ACCEPT;`; + } + + module.exports.WG_POST_UP = module.exports.WG_POST_UP.split('\n').join(' '); +} module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || ''; module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || ` diff --git a/src/lib/Server.js b/src/lib/Server.js index 1b306128..cceae757 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -210,6 +210,14 @@ module.exports = class Server { const { address } = await readBody(event); await WireGuard.updateClientAddress({ clientId, address }); return { success: true }; + })) + .put('/api/wireguard/client/:clientId/address6', Util.promisify(async (event) => { + const clientId = getRouterParam(event, 'clientId'); + if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { + throw createError({ status: 403 }); + } + const { address6 } = await readBody(event); + return WireGuard.updateClientAddress6({ clientId, address6 }); })); const safePathJoin = (base, target) => { diff --git a/src/lib/Util.js b/src/lib/Util.js index cc6e89c2..d63ef641 100644 --- a/src/lib/Util.js +++ b/src/lib/Util.js @@ -17,6 +17,13 @@ module.exports = class Util { return true; } + static isValidIPv6(str) { + // Regex source : https://stackoverflow.com/a/17871737 + const regex = new RegExp('^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$'); + const matches = str.match(regex); + return !!matches; + } + static promisify(fn) { // eslint-disable-next-line func-names return function(req, res) { diff --git a/src/lib/WireGuard.js b/src/lib/WireGuard.js index d4f32c5f..b8fbdd3a 100644 --- a/src/lib/WireGuard.js +++ b/src/lib/WireGuard.js @@ -16,7 +16,9 @@ const { WG_CONFIG_PORT, WG_MTU, WG_DEFAULT_DNS, + WG_DEFAULT_DNS6, WG_DEFAULT_ADDRESS, + WG_DEFAULT_ADDRESS6, WG_PERSISTENT_KEEPALIVE, WG_ALLOWED_IPS, WG_PRE_UP, @@ -46,12 +48,14 @@ module.exports = class WireGuard { log: 'echo ***hidden*** | wg pubkey', }); const address = WG_DEFAULT_ADDRESS.replace('x', '1'); + const address6 = WG_DEFAULT_ADDRESS6.replace('x', '1'); config = { server: { privateKey, publicKey, address, + address6, }, clients: {}, }; @@ -71,6 +75,10 @@ module.exports = class WireGuard { // 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 Util.exec(`ip6tables -t nat -A POSTROUTING -s ${WG_DEFAULT_ADDRESS6.replace('x', '')}/120 -o eth0 -j MASQUERADE`); + // await Util.exec('ip6tables -A INPUT -p udp -m udp --dport 51820 -j ACCEPT'); + // await Util.exec('ip6tables -A FORWARD -i wg0 -j ACCEPT'); + // await Util.exec('ip6tables -A FORWARD -o wg0 -j ACCEPT'); await this.__syncConfig(); return config; @@ -94,7 +102,7 @@ module.exports = class WireGuard { # Server [Interface] PrivateKey = ${config.server.privateKey} -Address = ${config.server.address}/24 +Address = ${config.server.address}/24, ${config.server.address6}/120 ListenPort = ${WG_PORT} PreUp = ${WG_PRE_UP} PostUp = ${WG_POST_UP} @@ -111,7 +119,7 @@ PostDown = ${WG_POST_DOWN} [Peer] PublicKey = ${client.publicKey} ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' -}AllowedIPs = ${client.address}/32`; +}AllowedIPs = ${client.address}/32, ${client.address6}/128`; } debug('Config saving...'); @@ -195,12 +203,14 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' async getClientConfiguration({ clientId }) { const config = await this.getConfig(); const client = await this.getClient({ clientId }); + const isDnsSet = WG_DEFAULT_DNS || WG_DEFAULT_DNS6; + const dnsServers = [WG_DEFAULT_DNS, WG_DEFAULT_DNS6].filter((item) => !!item).join(', '); return ` [Interface] PrivateKey = ${client.privateKey ? `${client.privateKey}` : 'REPLACE_ME'} -Address = ${client.address}/24 -${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\ +Address = ${client.address}/24, ${client.address6}/64 +${isDnsSet ? `DNS = ${dnsServers}\n` : ''}\ ${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\ [Peer] @@ -247,12 +257,29 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; throw new Error('Maximum number of clients reached.'); } + let address6; + for (let i = 2; i < 255; i++) { + const client = Object.values(config.clients).find((client) => { + return client.address6 === WG_DEFAULT_ADDRESS6.replace('x', i.toString(16)); + }); + + if (!client) { + address6 = WG_DEFAULT_ADDRESS6.replace('x', i.toString(16)); + break; + } + } + + if (!address6) { + throw new Error('Maximum number of clients reached.'); + } + // Create Client const id = crypto.randomUUID(); const client = { id, name, address, + address6, privateKey, publicKey, preSharedKey, @@ -319,6 +346,19 @@ Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`; await this.saveConfig(); } + async updateClientAddress6({ clientId, address6 }) { + const client = await this.getClient({ clientId }); + + if (!Util.isValidIPv6(address6)) { + throw new ServerError(`Invalid Address6: ${address6}`, 400); + } + + client.address6 = address6; + client.updatedAt = new Date(); + + await this.saveConfig(); + } + // Shutdown wireguard async Shutdown() { await Util.exec('wg-quick down wg0').catch(() => { });