diff --git a/Dockerfile b/Dockerfile index c9238f3..044ec0b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,16 +34,18 @@ RUN chmod +x /bin/wgpw RUN apk add --no-cache \ dpkg \ dumb-init \ + ip6tables \ iptables \ iptables-legacy \ wireguard-tools # Use iptables-legacy RUN update-alternatives --install /sbin/iptables iptables /sbin/iptables-legacy 10 --slave /sbin/iptables-restore iptables-restore /sbin/iptables-legacy-restore --slave /sbin/iptables-save iptables-save /sbin/iptables-legacy-save +RUN update-alternatives --install /sbin/ip6tables ip6tables /sbin/ip6tables-legacy 10 --slave /sbin/ip6tables-restore ip6tables-restore /sbin/ip6tables-legacy-restore --slave /sbin/ip6tables-save ip6tables-save /sbin/ip6tables-legacy-save # Set Environment ENV DEBUG=Server,WireGuard # Run Web UI WORKDIR /app -CMD ["/usr/bin/dumb-init", "node", "server.js"] \ No newline at end of file +CMD ["/usr/bin/dumb-init", "node", "server.js"] diff --git a/README.md b/README.md index be8a32c..065a71f 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ To automatically install & run wg-easy, simply run: -v ~/.wg-easy:/etc/wireguard \ -p 51820:51820/udp \ -p 51821:51821/tcp \ + --privileged=true \ --cap-add=NET_ADMIN \ --cap-add=SYS_MODULE \ --sysctl="net.ipv4.conf.all.src_valid_mark=1" \ @@ -107,11 +108,14 @@ These options can be configured by setting environment variables using `-e KEY=" | `WG_HOST` | - | `vpn.myserver.com` | The public hostname of your VPN server. | | `WG_DEVICE` | `eth0` | `ens6f0` | Ethernet device the wireguard traffic should be forwarded through. | | `WG_PORT` | `51820` | `12345` | The public UDP port of your VPN server. WireGuard will listen on that (othwise default) inside the Docker container. | -| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy) +| `WG_CONFIG_PORT`| `51820` | `12345` | The UDP port used on [Home Assistant Plugin](https://github.com/adriy-be/homeassistant-addons-jdeath/tree/main/wgeasy) | `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. | +| `WG_SRV_MTU` | `1420` | `1384` | The MTU the Server will use. | | `WG_PERSISTENT_KEEPALIVE` | `0` | `25` | Value in seconds to keep the "connection" open. If this value is 0, then connections won't be kept alive. | | `WG_DEFAULT_ADDRESS` | `10.8.0.x` | `10.6.0.x` | Clients IP address range. | -| `WG_DEFAULT_DNS` | `1.1.1.1` | `8.8.8.8, 8.8.4.4` | DNS server clients will use. If set to blank value, clients will not use any DNS. | +| `WG_DEFAULT_ADDRESS6` | `fdcc:ad94:bacf:61a4::cafe:x` | `fdcc:ad94:bacf:61a4::42:x` | Clients IPv6 address range. Has to be a valid IPv6 ULA address. | +| `WG_DEFAULT_DNS` | `84.200.69.80` | `84.200.69.80, 84.200.70.40` | DNS server clients will use. If set to blank value, clients will not use any DNS. | +| `WG_DEFAULT_DNS6` | `2001:1608:10:25::1c04:b12f` | `2001:1608:10:25::1c04:b12f, 2001:1608:10:25::9249:d69b` | DNSv6 server clients will use. | | `WG_ALLOWED_IPS` | `0.0.0.0/0, ::/0` | `192.168.15.0/24, 10.0.1.0/24` | Allowed IPs clients will use. | | `WG_PRE_UP` | `...` | - | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L19) for the default value. | | `WG_POST_UP` | `...` | `iptables ...` | See [config.js](https://github.com/wg-easy/wg-easy/blob/master/src/config.js#L20) for the default value. | diff --git a/docker-compose.yml b/docker-compose.yml index dd450ed..b2a5520 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,8 +17,11 @@ services: # - WG_PORT=51820 # - WG_CONFIG_PORT=92820 # - WG_DEFAULT_ADDRESS=10.8.0.x - # - WG_DEFAULT_DNS=1.1.1.1 + # - WG_DEFAULT_ADDRESS6=fdcc:ad94:bacf:61a4::cafe:x + # - WG_DEFAULT_DNS=84.200.69.80 + # - WG_DEFAULT_DNS6=2001:1608:10:25::1c04:b12f # - WG_MTU=1420 + # - WG_SRV_MTU=1420 # - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24 # - WG_PERSISTENT_KEEPALIVE=25 # - WG_PRE_UP=echo "Pre Up" > /etc/wireguard/pre-up.txt @@ -30,16 +33,34 @@ services: image: ghcr.io/wg-easy/wg-easy container_name: wg-easy + networks: + wg: + ipv4_address: 10.42.42.42 + ipv6_address: fdcc:ad94:bacf:61a3::2a volumes: - etc_wireguard:/etc/wireguard ports: - "51820:51820/udp" - "51821:51821/tcp" + privileged: true restart: unless-stopped cap_add: - NET_ADMIN - SYS_MODULE - # - NET_RAW # ⚠️ Uncomment if using Podman + # - NET_RAW # ⚠️ Uncomment if using Podman 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: fdcc:ad94:bacf:61a3::/64 diff --git a/src/config.js b/src/config.js index 9785053..dfd9ee0 100644 --- a/src/config.js +++ b/src/config.js @@ -1,5 +1,6 @@ 'use strict'; +const childProcess = require('child_process'); const { release: { version } } = require('./package.json'); module.exports.RELEASE = version; @@ -12,28 +13,60 @@ module.exports.WG_HOST = process.env.WG_HOST; module.exports.WG_PORT = process.env.WG_PORT || '51820'; module.exports.WG_CONFIG_PORT = process.env.WG_CONFIG_PORT || process.env.WG_PORT || '51820'; module.exports.WG_MTU = process.env.WG_MTU || null; +module.exports.WG_SRV_MTU = process.env.WG_SRV_MTU || '1420'; 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'; + : '84.200.69.80'; +module.exports.WG_DEFAULT_DNS6 = typeof process.env.WG_DEFAULT_DNS6 === 'string' + ? process.env.WG_DEFAULT_DNS6 + : '2001:1608:10:25::1c04:b12f'; module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '0.0.0.0/0, ::/0'; +// 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_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(' '); +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 ${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;`; + + 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 ${module.exports.WG_DEVICE} -j MASQUERADE; + ip6tables -A INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -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.replace(/\n\s*/g, ' ').trim(); +} module.exports.WG_PRE_DOWN = process.env.WG_PRE_DOWN || ''; -module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN || ` -iptables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; -iptables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; -iptables -D FORWARD -i wg0 -j ACCEPT; -iptables -D FORWARD -o wg0 -j ACCEPT; -`.split('\n').join(' '); +module.exports.WG_POST_DOWN = process.env.WG_POST_DOWN; +if (!process.env.WG_POST_DOWN) { + module.exports.WG_POST_DOWN = ` + iptables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE; + iptables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; + iptables -D FORWARD -i wg0 -j ACCEPT; + iptables -D FORWARD -o wg0 -j ACCEPT;`; + + if (modules.includes('ip6table_nat')) { + module.exports.WG_POST_DOWN += ` + ip6tables -t nat -D POSTROUTING -s ${module.exports.WG_DEFAULT_ADDRESS6.replace('x', '0')}/64 -o ${module.exports.WG_DEVICE} -j MASQUERADE; + ip6tables -D INPUT -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT; + ip6tables -D FORWARD -i wg0 -j ACCEPT; + ip6tables -D FORWARD -o wg0 -j ACCEPT;`; + } + + module.exports.WG_POST_DOWN = module.exports.WG_POST_DOWN.replace(/\n\s*/g, ' ').trim(); +} module.exports.LANG = process.env.LANG || 'en'; module.exports.UI_TRAFFIC_STATS = process.env.UI_TRAFFIC_STATS || 'false'; module.exports.UI_CHART_TYPE = process.env.UI_CHART_TYPE || 0; diff --git a/src/lib/Server.js b/src/lib/Server.js index 7f06da5..e364bc1 100644 --- a/src/lib/Server.js +++ b/src/lib/Server.js @@ -233,6 +233,15 @@ module.exports = class Server { const { address } = await readBody(event); await WireGuard.updateClientAddress({ clientId, address }); return { success: true }; + })) + .put('/api/wireguard/client/:clientId/address6', defineEventHandler(async (event) => { + const clientId = getRouterParam(event, 'clientId'); + if (clientId === '__proto__' || clientId === 'constructor' || clientId === 'prototype') { + throw createError({ status: 403 }); + } + const { address6 } = await readBody(event); + await WireGuard.updateClientAddress6({ clientId, address6 }); + return { success: true }; })); const safePathJoin = (base, target) => { diff --git a/src/lib/Util.js b/src/lib/Util.js index cc6e89c..d63ef64 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 adf6ca9..ed2be7f 100644 --- a/src/lib/WireGuard.js +++ b/src/lib/WireGuard.js @@ -15,8 +15,11 @@ const { WG_PORT, WG_CONFIG_PORT, WG_MTU, + WG_SRV_MTU, WG_DEFAULT_DNS, + WG_DEFAULT_DNS6, WG_DEFAULT_ADDRESS, + WG_DEFAULT_ADDRESS6, WG_PERSISTENT_KEEPALIVE, WG_ALLOWED_IPS, WG_PRE_UP, @@ -45,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: {}, }; @@ -100,7 +105,8 @@ module.exports = class WireGuard { # Server [Interface] PrivateKey = ${config.server.privateKey} -Address = ${config.server.address}/24 +Address = ${config.server.address}/24, ${config.server.address6}/64 +${WG_SRV_MTU ? `MTU = ${WG_SRV_MTU}` : ''} ListenPort = ${WG_PORT} PreUp = ${WG_PRE_UP} PostUp = ${WG_POST_UP} @@ -117,7 +123,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...'); @@ -143,6 +149,7 @@ ${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : '' 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), @@ -201,11 +208,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 +Address = ${client.address}/24, ${client.address6}/64 +${isDnsSet ? `DNS = ${dnsServers}\n` : ''}\ ${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\ ${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\ @@ -255,12 +265,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, @@ -327,6 +354,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 Address: ${address6}`, 400); + } + + client.address6 = address6; + client.updatedAt = new Date(); + + await this.saveConfig(); + } + async __reloadConfig() { await this.__buildConfig(); await this.__syncConfig(); diff --git a/src/www/index.html b/src/www/index.html index 236a8f3..595a1aa 100644 --- a/src/www/index.html +++ b/src/www/index.html @@ -43,7 +43,7 @@ - @@ -225,7 +225,7 @@ {{client.transferTxCurrent | bytes}}/s - + · @@ -248,6 +248,31 @@
+ + + + + + {{client.address6}} + + + + + + + + +
diff --git a/src/www/js/api.js b/src/www/js/api.js index 9006f5a..239d7e1 100644 --- a/src/www/js/api.js +++ b/src/www/js/api.js @@ -138,6 +138,14 @@ class API { }); } + async updateClientAddress6({ clientId, address6 }) { + return this.call({ + method: 'put', + path: `/wireguard/client/${clientId}/address6/`, + body: { address6 }, + }); + } + async restoreConfiguration(file) { return this.call({ method: 'put', diff --git a/src/www/js/app.js b/src/www/js/app.js index 61bb7c0..fe3551c 100644 --- a/src/www/js/app.js +++ b/src/www/js/app.js @@ -63,6 +63,8 @@ new Vue({ clientEditNameId: null, clientEditAddress: null, clientEditAddressId: null, + clientEditAddress6: null, + clientEditAddress6Id: null, qrcode: null, currentRelease: null, @@ -299,6 +301,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)); + }, restoreConfig(e) { e.preventDefault(); const file = e.currentTarget.files.item(0); diff --git a/wg-easy.service b/wg-easy.service index bcdf72f..a9eedf9 100644 --- a/wg-easy.service +++ b/wg-easy.service @@ -6,7 +6,7 @@ After=network-online.target nss-lookup.target Environment="WG_HOST=raspberrypi.local" # Change this to your host's public address or static public ip. Environment="PASSWORD=REPLACEME" # When set, requires a password when logging in to the Web UI, to disable add a hashtag #Environment="WG_DEFAULT_ADDRESS=10.0.8.x" #Clients IP address range. -#Environment="WG_DEFAULT_DNS=10.0.8.1, 1.1.1.1" #DNS server clients will use. If set to blank value, clients will not use any DNS. +#Environment="WG_DEFAULT_DNS=10.0.8.1, 84.200.69.80" #DNS server clients will use. If set to blank value, clients will not use any DNS. #Environment="WG_ALLOWED_IPS=0.0.0.0/0,::/0" #Allowed IPs clients will use. #Environment="WG_DEVICE=ens1" #Ethernet device the wireguard traffic should be forwarded through. #Environment="PORT=80" #TCP port for Web UI. Default : 51821