diff --git a/Dockerfile b/Dockerfile index 2695361f..e3c1b71b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,7 +36,8 @@ RUN npm i -g nodemon # Install Linux packages RUN apk add -U --no-cache \ wireguard-tools \ - dumb-init + dumb-init \ + ip6tables # Expose Ports EXPOSE 51820/udp diff --git a/docker-compose.yml b/docker-compose.yml index 0a13accc..a492de47 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ version: "3.8" + services: wg-easy: environment: @@ -10,12 +11,18 @@ services: # - PASSWORD=foobar123 # - WG_PORT=51820 # - WG_DEFAULT_ADDRESS=10.8.0.x - # - WG_DEFAULT_DNS=1.1.1.1 + # - WG_DEFAULT_ADDRESS6=fd42:beef::x + # - WG_DEFAULT_DNS=1.0.0.1 + # - WG_DEFAULT_DNS6=2606:4700:4700::1001 # - WG_MTU=1420 # - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24 image: weejewel/wg-easy container_name: wg-easy + networks: + wg: + ipv4_address: 10.42.42.42 + ipv6_address: fd00:42::42 volumes: - .:/etc/wireguard ports: @@ -28,3 +35,16 @@ services: sysctls: - net.ipv4.ip_forward=1 - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv6.conf.all.disable_ipv6=0 + - net.ipv6.conf.all.forwarding=1 + - net.ipv6.conf.default.forwarding=1 + +networks: + wg: + driver: bridge + enable_ipv6: true + ipam: + driver: default + config: + - subnet: 10.42.42.0/24 + - subnet: fd00:42::/120 diff --git a/src/config.js b/src/config.js index a08aab3b..72cb282c 100644 --- a/src/config.js +++ b/src/config.js @@ -11,9 +11,13 @@ module.exports.WG_PORT = process.env.WG_PORT || 51820; 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 || 'fd80: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_POST_UP = process.env.WG_POST_UP || ` @@ -21,6 +25,11 @@ iptables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x 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; +ip6tables -t nat -A POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS6.replace('x', '')}/120 -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; `.split('\n').join(' '); + module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || ''; diff --git a/src/lib/Server.js b/src/lib/Server.js index e204fa5f..ee75f3a9 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -130,6 +130,11 @@ module.exports = class Server { const { address } = req.body; return WireGuard.updateClientAddress({ clientId, address }); })) + .put('/api/wireguard/client/:clientId/address6', Util.promisify(async req => { + const { clientId } = req.params; + const { address6 } = req.body; + return WireGuard.updateClientAddress6({ clientId, address6 }); + })) .listen(PORT, () => { debug(`Listening on http://0.0.0.0:${PORT}`); diff --git a/src/lib/Util.js b/src/lib/Util.js index 2a47a20e..758f0751 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 032854f1..5285f261 100644 --- a/src/lib/WireGuard.js +++ b/src/lib/WireGuard.js @@ -16,7 +16,9 @@ const { WG_PORT, WG_MTU, WG_DEFAULT_DNS, + WG_DEFAULT_DNS6, WG_DEFAULT_ADDRESS, + WG_DEFAULT_ADDRESS6, WG_PERSISTENT_KEEPALIVE, WG_ALLOWED_IPS, WG_POST_UP, @@ -44,12 +46,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: {}, }; @@ -69,6 +73,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; @@ -92,7 +100,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 = 51820 PostUp = ${WG_POST_UP} PostDown = ${WG_POST_DOWN} @@ -107,7 +115,7 @@ PostDown = ${WG_POST_DOWN} [Peer] PublicKey = ${client.publicKey} PresharedKey = ${client.preSharedKey} -AllowedIPs = ${client.address}/32`; +AllowedIPs = ${client.address}/32, ${client.address6}/32`; } debug('Config saving...'); @@ -133,6 +141,7 @@ AllowedIPs = ${client.address}/32`; name: client.name, enabled: client.enabled, address: client.address, + address6: client.address6, publicKey: client.publicKey, createdAt: new Date(client.createdAt), updatedAt: new Date(client.updatedAt), @@ -191,12 +200,14 @@ AllowedIPs = ${client.address}/32`; 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} -Address = ${client.address}/24 -${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}` : ''} +Address = ${client.address}/24, ${client.address6}/120 +${isDnsSet ? `DNS = ${dnsServers}` : ''} ${WG_MTU ? `MTU = ${WG_MTU}` : ''} [Peer] @@ -243,11 +254,28 @@ Endpoint = ${WG_HOST}:${WG_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 clientId = uuid.v4(); const client = { name, address, + address6, privateKey, publicKey, preSharedKey, @@ -311,5 +339,18 @@ Endpoint = ${WG_HOST}:${WG_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(); + } }; diff --git a/src/www/index.html b/src/www/index.html index 4080c51a..79813cda 100644 --- a/src/www/index.html +++ b/src/www/index.html @@ -153,6 +153,31 @@ + + + + + + {{client.address6}} + + + + + + + + + ยท diff --git a/src/www/js/api.js b/src/www/js/api.js index accbb579..4e41fd69 100644 --- a/src/www/js/api.js +++ b/src/www/js/api.js @@ -117,4 +117,11 @@ class API { }); } + async updateClientAddress6({ clientId, address6 }) { + return this.call({ + method: 'put', + path: `/wireguard/client/${clientId}/address6/`, + body: { address6 }, + }); + } } diff --git a/src/www/js/app.js b/src/www/js/app.js index 137fb229..b4ff191c 100644 --- a/src/www/js/app.js +++ b/src/www/js/app.js @@ -43,6 +43,8 @@ new Vue({ clientEditNameId: null, clientEditAddress: null, clientEditAddressId: null, + clientEditAddress6: null, + clientEditAddress6Id: null, qrcode: null, currentRelease: null, @@ -243,6 +245,11 @@ new Vue({ .catch(err => alert(err.message || err.toString())) .finally(() => this.refresh().catch(console.error)); }, + updateClientAddress6(client, address6) { + this.api.updateClientAddress6({ clientId: client.id, address6 }) + .catch(err => alert(err.message || err.toString())) + .finally(() => this.refresh().catch(console.error)); + }, }, filters: { bytes,