Browse Source

IPv6 support (#1103) testing

Thank you @kaaax0815!
Merge into testing branch
pull/1113/head
Philip H 2 years ago
committed by GitHub
parent
commit
8f457f740a
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      Dockerfile
  2. 2
      README.md
  3. 19
      docker-compose.yml
  4. 33
      src/config.js
  5. 9
      src/lib/Server.js
  6. 7
      src/lib/Util.js
  7. 48
      src/lib/WireGuard.js
  8. 25
      src/www/index.html
  9. 8
      src/www/js/api.js
  10. 7
      src/www/js/app.js

2
Dockerfile

@ -30,12 +30,14 @@ COPY --from=build_node_modules /node_modules /node_modules
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

2
README.md

@ -109,7 +109,9 @@ These options can be configured by setting environment variables using `-e KEY="
| `WG_MTU` | `null` | `1420` | The MTU the clients will use. Server uses default WG MTU. |
| `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_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` | `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_DNS6` | `2606:4700:4700::1111` | `2606:4700:4700::1001, 2606:4700:4700::1111` | 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. |

19
docker-compose.yml

@ -17,7 +17,9 @@ services:
# - WG_PORT=51820
# - WG_CONFIG_PORT=92820
# - WG_DEFAULT_ADDRESS=10.8.0.x
# - WG_DEFAULT_ADDRESS6=fd00::cafe:x
# - WG_DEFAULT_DNS=1.1.1.1
# - WG_DEFAULT_ADDRESS6=fdcc:ad94:bacf:61a4::cafe:x
# - WG_MTU=1420
# - WG_ALLOWED_IPS=192.168.15.0/24, 10.0.1.0/24
# - WG_PERSISTENT_KEEPALIVE=25
@ -30,6 +32,10 @@ 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:
@ -43,3 +49,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: fdcc:ad94:bacf:61a3::/64

33
src/config.js

@ -1,5 +1,6 @@
'use strict';
const childProcess = require('child_process');
const { release } = require('./package.json');
module.exports.RELEASE = release;
@ -14,18 +15,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 || `

9
src/lib/Server.js

@ -210,6 +210,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) => {

7
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) {

48
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(() => { });

25
src/www/index.html

@ -226,6 +226,31 @@
<div v-if="uiTrafficStats"
class="flex gap-2 items-center shrink-0 text-gray-400 dark:text-neutral-400 text-xs mt-px justify-end">
<!-- Address6 -->
<span class="group">
<!-- Show -->
<input v-show="clientEditAddress6Id === client.id" v-model="clientEditAddress6"
v-on:keyup.enter="updateClientAddress6(client, clientEditAddress6); clientEditAddress6 = null; clientEditAddress6Id = null;"
v-on:keyup.escape="clientEditAddress6 = null; clientEditAddress6Id = null;"
:ref="'client-' + client.id + '-address6'"
class="rounded border-2 border-gray-100 focus:border-gray-200 outline-none w-20 text-black" />
<span v-show="clientEditAddress6Id !== client.id"
class="inline-block border-t-2 border-b-2 border-transparent">{{client.address6}}</span>
<!-- Edit -->
<span v-show="clientEditAddress6Id !== client.id"
@click="clientEditAddress6 = client.address6; clientEditAddress6Id = client.id; setTimeout(() => $refs['client-' + client.id + '-address6'][0].select(), 1);"
class="cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity">
<svg xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 inline align-middle opacity-25 hover:opacity-100" fill="none"
viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</span>
</span>
<!-- Transfer TX -->
<div class="min-w-20 md:min-w-24" v-if="client.transferTx">
<span class="flex gap-1" :title="$t('totalDownload') + bytes(client.transferTx)">

8
src/www/js/api.js

@ -138,4 +138,12 @@ class API {
});
}
async updateClientAddress6({ clientId, address6 }) {
return this.call({
method: 'put',
path: `/wireguard/client/${clientId}/address6/`,
body: { address6 },
});
}
}

7
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));
},
toggleTheme() {
const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(this.uiTheme);

Loading…
Cancel
Save