Browse Source

ipv6 enabling

pull/2294/head
SachinAsus 2 years ago
parent
commit
59901f5c43
  1. 50
      src/config.js
  2. 104
      src/lib/Server.js
  3. 7
      src/lib/Util.js
  4. 156
      src/lib/WireGuard.js
  5. 18
      src/wgsh/wg0-post-up.sh
  6. 13
      src/wgsh/wg0-pre-down.sh
  7. 15
      src/www/css/app.css
  8. 43
      src/www/index.html
  9. 8
      src/www/js/api.js
  10. 21
      src/www/js/app.js
  11. 46
      src/www/js/i18n.js
  12. 8
      src/www/js/vendor/apexcharts.min.js

50
src/config.js

@ -1,31 +1,61 @@
'use strict';
const { release } = require('./package.json');
const path = require('path');
const dirFullPath = path.resolve(__dirname);
const childProcess = require('child_process');
const { release } = require('../package.json');
module.exports.RELEASE = release;
module.exports.PORT = process.env.PORT || '51821';
module.exports.WEBUI_HOST = process.env.WEBUI_HOST || '0.0.0.0';
module.exports.PASSWORD = process.env.PASSWORD || '';
module.exports.PASSWORD_HASH = process.env.PASSWORD_HASH || '';
module.exports.WG_PATH = process.env.WG_PATH || '/etc/wireguard/';
module.exports.WG_DEVICE = process.env.WG_DEVICE || 'ens3';
module.exports.WG_HOST = process.env.WG_HOST || 'oracle-daily.duckdns.org';
module.exports.WG_PORT = 51820;
module.exports.WG_MTU = process.env.WG_MTU || 1412;
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || 25;
module.exports.WG_DEFAULT_ADDRESS = process.env.WG_DEFAULT_ADDRESS || '10.250.1.x';
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 || '1412';
module.exports.WG_PERSISTENT_KEEPALIVE = process.env.WG_PERSISTENT_KEEPALIVE || '25';
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
: '10.250.0.2';
module.exports.WG_ALLOWED_IPS = process.env.WG_ALLOWED_IPS || '10.250.0.0/22, 10.254.1.0/24';
: '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 || '10.250.0.0/22, 10.254.1.0/24, fdcc:ad94:bacf:61a4::cafe:0/64';
// 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 = '/app/wg0-post-up.sh';
module.exports.WG_POST_UP = path.resolve(__dirname, './wgsh/wg0-post-up.sh');
// module.exports.WG_POST_UP = process.env.WG_POST_UP;
// if (!process.env.WG_POST_UP) {
// module.exports.WG_POST_UP = `iptables -t nat -I POSTROUTING 1 -s ${module.exports.WG_DEFAULT_ADDRESS.replace('x', '0')}/24 -o ${module.exports.WG_DEVICE} -j MASQUERADE;
// iptables -I INPUT 1 -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT;
// iptables -I FORWARD 1 -i wg0 -j ACCEPT;
// iptables -I FORWARD 1 -o wg0 -j ACCEPT;
// ip6tables -t nat -I POSTROUTING 1 -o ${module.exports.WG_DEVICE} -j MASQUERADE;
// ip6tables -I INPUT 1 -p udp -m udp --dport ${module.exports.WG_PORT} -j ACCEPT;
// ip6tables -I FORWARD 1 -i wg0 -j ACCEPT;
// ip6tables -I FORWARD 1 -o wg0 -j ACCEPT;`.split('\n').join(' ');
// }
module.exports.WG_PRE_DOWN = path.resolve(__dirname, './wgsh/wg0-pre-down.sh');
module.exports.WG_POST_DOWN = process.env.WG_PRE_DOWN || '';
// 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;
// ip6tables -t nat -D POSTROUTING -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;`.split('\n').join(' ');
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;

104
src/lib/Server.js

@ -4,7 +4,7 @@ const bcrypt = require('bcryptjs');
const crypto = require('node:crypto');
const { createServer } = require('node:http');
const { stat, readFile } = require('node:fs/promises');
const { join } = require('node:path');
const { resolve, sep } = require('node:path');
const expressSession = require('express-session');
const debug = require('debug')('Server');
@ -29,11 +29,40 @@ const {
WEBUI_HOST,
RELEASE,
PASSWORD,
PASSWORD_HASH,
LANG,
UI_TRAFFIC_STATS,
UI_CHART_TYPE,
} = require('../config');
const requiresPassword = !!PASSWORD || !!PASSWORD_HASH;
/**
* Checks if `password` matches the PASSWORD_HASH.
*
* For backward compatibility it also allows `password` to match the clear text PASSWORD,
* but only if no PASSWORD_HASH is provided.
*
* If both enviornment variables are not set, the password is always invalid.
*
* @param {string} password String to test
* @returns {boolean} true if matching environment, otherwise false
*/
const isPasswordValid = (password) => {
if (typeof password !== 'string') {
return false;
}
if (PASSWORD_HASH) {
return bcrypt.compareSync(password, PASSWORD_HASH);
}
if (PASSWORD) {
return password === PASSWORD;
}
return false;
};
module.exports = class Server {
constructor() {
@ -72,7 +101,6 @@ module.exports = class Server {
// Authentication
.get('/api/session', defineEventHandler((event) => {
const requiresPassword = !!process.env.PASSWORD;
const authenticated = requiresPassword
? !!(event.node.req.session && event.node.req.session.authenticated)
: true;
@ -85,14 +113,16 @@ module.exports = class Server {
.post('/api/session', defineEventHandler(async (event) => {
const { password } = await readBody(event);
if (typeof password !== 'string') {
if (!requiresPassword) {
// if no password is required, the API should never be called.
// Do not automatically authenticate the user.
throw createError({
status: 401,
message: 'Missing: Password',
message: 'Invalid state',
});
}
if (password !== PASSWORD) {
if (!isPasswordValid(password)) {
throw createError({
status: 401,
message: 'Incorrect Password',
@ -104,13 +134,13 @@ module.exports = class Server {
debug(`New Session: ${event.node.req.session.id}`);
return { succcess: true };
return { success: true };
}));
// WireGuard
app.use(
fromNodeMiddleware((req, res, next) => {
if (!PASSWORD || !req.url.startsWith('/api/')) {
if (!requiresPassword || !req.url.startsWith('/api/')) {
return next();
}
@ -119,7 +149,7 @@ module.exports = class Server {
}
if (req.url.startsWith('/api/') && req.headers['authorization']) {
if (bcrypt.compareSync(req.headers['authorization'], bcrypt.hashSync(PASSWORD, 10))) {
if (isPasswordValid(req.headers['authorization'])) {
return next();
}
return res.status(401).json({
@ -210,17 +240,69 @@ 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) => {
// Manage web root (edge case)
if (target === '/') {
return `${base}${sep}`;
}
// Prepend './' to prevent absolute paths
const targetPath = `.${sep}${target}`;
// Resolve the absolute path
const resolvedPath = resolve(base, targetPath);
// Check if resolvedPath is a subpath of base
if (resolvedPath.startsWith(`${base}${sep}`)) {
return resolvedPath;
}
throw createError({
status: 400,
message: 'Bad Request',
});
};
// backup_restore
const router3 = createRouter();
app.use(router3);
router3
.get('/api/wireguard/backup', defineEventHandler(async (event) => {
const config = await WireGuard.backupConfiguration();
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"');
setHeader(event, 'Content-Type', 'text/json');
return config;
}))
.put('/api/wireguard/restore', defineEventHandler(async (event) => {
const { file } = await readBody(event);
await WireGuard.restoreConfiguration(file);
return { success: true };
}));
// Static assets
const publicDir = '/home/ubuntu/wg-easy/src/www';
const publicDir = '/app/www';
app.use(
defineEventHandler((event) => {
return serveStatic(event, {
getContents: (id) => readFile(join(publicDir, id)),
getContents: (id) => {
return readFile(safePathJoin(publicDir, id));
},
getMeta: async (id) => {
const stats = await stat(join(publicDir, id)).catch(() => {});
const filePath = safePathJoin(publicDir, id);
const stats = await stat(filePath).catch(() => {});
if (!stats || !stats.isFile()) {
return;
}

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

156
src/lib/WireGuard.js

@ -1,10 +1,9 @@
'use strict';
const fs = require('fs').promises;
const fs = require('node:fs/promises');
const path = require('path');
const debug = require('debug')('WireGuard');
const uuid = require('uuid');
const crypto = require('node:crypto');
const QRCode = require('qrcode');
const Util = require('./Util');
@ -14,9 +13,12 @@ const {
WG_PATH,
WG_HOST,
WG_PORT,
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,
@ -27,54 +29,60 @@ const {
module.exports = class WireGuard {
async __buildConfig() {
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.');
}
return config;
});
return this.__configPromise;
}
async getConfig() {
if (!this.__configPromise) {
this.__configPromise = Promise.resolve().then(async () => {
if (!WG_HOST) {
throw new Error('WG_HOST Environment Variable Not Set!');
}
const config = await this.__buildConfig();
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!');
}
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;
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 this.__configPromise;
@ -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}/64
ListenPort = ${WG_PORT}
${WG_DEFAULT_DNS ? `DNS = ${WG_DEFAULT_DNS}\n` : ''}\
${WG_MTU ? `MTU = ${WG_MTU}\n` : ''}\
@ -116,7 +124,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...');
@ -200,12 +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
${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]
@ -213,7 +223,7 @@ PublicKey = ${config.server.publicKey}
${client.preSharedKey ? `PresharedKey = ${client.preSharedKey}\n` : ''
}AllowedIPs = ${WG_ALLOWED_IPS}
PersistentKeepalive = ${WG_PERSISTENT_KEEPALIVE}
Endpoint = ${WG_HOST}:${WG_PORT}`;
Endpoint = ${WG_HOST}:${WG_CONFIG_PORT}`;
}
async getClientQRCodeSVG({ clientId }) {
@ -232,7 +242,9 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
const config = await this.getConfig();
const privateKey = await Util.exec('wg genkey');
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`);
const publicKey = await Util.exec(`echo ${privateKey} | wg pubkey`, {
log: 'echo ***hidden*** | wg pubkey',
});
// const preSharedKey = await Util.exec('wg genpsk');
// Calculate next IP
@ -252,12 +264,29 @@ 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 id = uuid.v4();
const id = crypto.randomUUID();
const client = {
id,
name,
address,
address6,
privateKey,
publicKey,
// preSharedKey,
@ -324,9 +353,30 @@ Endpoint = ${WG_HOST}:${WG_PORT}`;
await this.saveConfig();
}
async __reloadConfig() {
await this.__buildConfig();
await this.__syncConfig();
}
async restoreConfiguration(config) {
debug('Starting configuration restore process.');
const _config = JSON.parse(config);
await this.__saveConfig(_config);
await this.__reloadConfig();
debug('Configuration restore process completed.');
}
async backupConfiguration() {
debug('Starting configuration backup.');
const config = await this.getConfig();
const backup = JSON.stringify(config, null, 2);
debug('Configuration backup completed.');
return backup;
}
// Shutdown wireguard
async Shutdown() {
await Util.exec('wg-quick down wg0').catch(() => { });
await Util.exec('wg-quick down wg0').catch(() => {});
}
};

18
src/wgsh/wg0-post-up.sh

@ -7,7 +7,7 @@ IPT6="/sbin/ip6tables"
IN_FACE="enp0s3" # NIC connected to the internet
WG_FACE="wg0" # WG NIC
SUB_NET="10.250.1.0/24" # WG IPv4 sub/net aka CIDR
# SUB_NET_6="" # Change this according
SUB_NET_6="fdcc:ad94:bacf:61a4::cafe:0/64" # Change this according
WG_PORT="51820" # WG udp port
# WG_TABLE='10'
@ -18,15 +18,21 @@ $IPT -t nat -I POSTROUTING 1 -s $SUB_NET -o $IN_FACE -j MASQUERADE
$IPT -I FORWARD 1 -i $WG_FACE -j ACCEPT #for internet
$IPT -I FORWARD 1 -o $WG_FACE -j ACCEPT #for internet
# IPv6 (Uncomment) ##
$IPT6 -I INPUT 1 -p udp --dport $WG_PORT -j ACCEPT # for incoming connection
$IPT6 -I INPUT 1 -p tcp --dport 51821 -j ACCEPT # for webui
$IPT6 -t nat -I POSTROUTING 1 -s $SUB_NET_6 -o $IN_FACE -j MASQUERADE
$IPT6 -I FORWARD 1 -i $WG_FACE -j ACCEPT #for internet
$IPT6 -I FORWARD 1 -o $WG_FACE -j ACCEPT #for internet
# $IPT6 -t nat -I POSTROUTING 1 -s $SUB_NET_6 -o $IN_FACE -j MASQUERADE
# $IPT6 -I INPUT 1 -i $WG_FACE -j ACCEPT
# $IPT6 -I FORWARD 1 -i $IN_FACE -o $WG_FACE -j ACCEPT
# $IPT6 -I FORWARD 1 -i $WG_FACE -o $IN_FACE -j ACCEPT
wg-quick up wg1
systemctl start mount-mypi.service
# wg set wg0 fwmark 51820
## IPv6 (Uncomment) ##
# $IPT6 -t nat -I POSTROUTING 1 -s $SUB_NET_6 -o $IN_FACE -j MASQUERADE
# $IPT6 -I INPUT 1 -i $WG_FACE -j ACCEPT
# $IPT6 -I FORWARD 1 -i $IN_FACE -o $WG_FACE -j ACCEPT
# $IPT6 -I FORWARD 1 -i $WG_FACE -o $IN_FACE -j ACCEPT
set -e

13
src/wgsh/wg0-pre-down.sh

@ -7,9 +7,9 @@ IPT6="/sbin/ip6tables"
IN_FACE="enp0s3" # NIC connected to the internet
WG_FACE="wg0" # WG NIC
SUB_NET="10.250.1.0/24" # WG IPv4 sub/net aka CIDR
#SUB_NET_6="fd42:42:42::/64" # WG IPv6 sub/net
WG_PORT="61820" # WG udp port
WG_TABLE='10'
SUB_NET_6="fdcc:ad94:bacf:61a4::cafe:0/64" # WG IPv6 sub/net
WG_PORT="51820" # WG udp port
# WG_TABLE='10'
# IPv4 rules #
$IPT -D INPUT -p udp --dport $WG_PORT -j ACCEPT # for incoming connection
@ -18,6 +18,13 @@ $IPT -t nat -D POSTROUTING -s $SUB_NET -o $IN_FACE -j MASQUERADE
$IPT -D FORWARD -i $WG_FACE -j ACCEPT #for internet
$IPT -D FORWARD -o $WG_FACE -j ACCEPT #for internet
# IPv4 rules #
$IPT6 -D INPUT -p udp --dport $WG_PORT -j ACCEPT # for incoming connection
$IPT6 -D INPUT -p tcp --dport 51821 -j ACCEPT # for webui
$IPT6 -t nat -D POSTROUTING -s $SUB_NET -o $IN_FACE -j MASQUERADE
$IPT6 -D FORWARD -i $WG_FACE -j ACCEPT #for internet
$IPT6 -D FORWARD -o $WG_FACE -j ACCEPT #for internet
# $IPT -t nat -D POSTROUTING -s $SUB_NET -o $IN_FACE -j MASQUERADE
# $IPT -D FORWARD -i $IN_FACE -o $WG_FACE -j ACCEPT
# $IPT -D FORWARD -i $WG_FACE -o $IN_FACE -j ACCEPT

15
src/www/css/app.css

@ -1,5 +1,5 @@
/*
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
! tailwindcss v3.4.4 | MIT License | https://tailwindcss.com
*/
/*
@ -802,6 +802,11 @@ video {
display: none;
}
.size-6 {
width: 1.5rem;
height: 1.5rem;
}
.h-1 {
height: 0.25rem;
}
@ -846,6 +851,10 @@ video {
min-height: 100vh;
}
.w-1 {
width: 0.25rem;
}
.w-10 {
width: 2.5rem;
}
@ -1454,6 +1463,10 @@ video {
border-bottom-width: 0px;
}
.hover\:cursor-pointer:hover {
cursor: pointer;
}
.hover\:border-red-800:hover {
--tw-border-opacity: 1;
border-color: rgb(153 27 27 / var(--tw-border-opacity));

43
src/www/index.html

@ -91,6 +91,24 @@
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p>
</div>
<div class="flex-shrink-0">
<!-- Restore configuration -->
<label for="inputRC" :title="$t('titleRestoreConfig')"
class="hover:cursor-pointer hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded inline-flex items-center transition">
<svg inline class="w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"></path>
</svg>
<span class="text-sm">{{$t("restore")}}</span>
<input id="inputRC" type="file" name="configurationfile" accept="text/*,.json" @change="restoreConfig" class="hidden"/>
</label>
<!-- Backup configuration -->
<a href="./api/wireguard/backup" :title="$t('titleBackupConfig')"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded inline-flex items-center transition">
<svg inline class="w-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 14.25h13.5m-13.5 0a3 3 0 0 1-3-3m3 3a3 3 0 1 0 0 6h13.5a3 3 0 1 0 0-6m-16.5-3a3 3 0 0 1 3-3h13.5a3 3 0 0 1 3 3m-19.5 0a4.5 4.5 0 0 1 .9-2.7L5.737 5.1a3.375 3.375 0 0 1 2.7-1.35h7.126c1.062 0 2.062.5 2.7 1.35l2.587 3.45a4.5 4.5 0 0 1 .9 2.7m0 0a3 3 0 0 1-3 3m0 3h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Zm-3 6h.008v.008h-.008v-.008Zm0-6h.008v.008h-.008v-.008Z"></path>
</svg>
<span class="text-sm">{{$t("backup")}}</span>
</a>
<!-- New client -->
<button @click="clientCreate = true; clientCreateName = '';"
class="hover:bg-red-800 hover:border-red-800 hover:text-white text-gray-700 dark:text-neutral-200 border-2 border-gray-100 dark:border-neutral-600 py-2 px-4 rounded inline-flex items-center transition">
<svg class="w-4 mr-2" inline xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
@ -226,6 +244,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 restoreConfiguration(file) {
return this.call({
method: 'put',
path: '/wireguard/restore',
body: { file },
});
}
}

21
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,22 @@ new Vue({
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
},
restoreConfig(e) {
e.preventDefault();
const file = e.currentTarget.files.item(0);
if (file) {
file.text()
.then((content) => {
this.api.restoreConfiguration(content)
.then((_result) => alert('The configuration was updated.'))
.catch((err) => alert(err.message || err.toString()))
.finally(() => this.refresh().catch(console.error));
})
.catch((err) => alert(err.message || err.toString()));
} else {
alert('Failed to load your file!');
}
},
toggleTheme() {
const themes = ['light', 'dark', 'auto'];
const currentIndex = themes.indexOf(this.uiTheme);
@ -390,9 +408,6 @@ new Vue({
return releasesArray[0];
});
console.log(`Current Release: ${currentRelease}`);
console.log(`Latest Release: ${latestRelease.version}`);
if (currentRelease >= latestRelease.version) return;
this.currentRelease = currentRelease;

46
src/www/js/i18n.js

@ -30,6 +30,10 @@ const messages = { // eslint-disable-line no-unused-vars
donate: 'Donate',
toggleCharts: 'Show/hide Charts',
theme: { dark: 'Dark theme', light: 'Light theme', auto: 'Auto theme' },
restore: 'Restore',
backup: 'Backup',
titleRestoreConfig: 'Restore your configuration',
titleBackupConfig: 'Backup your configuration',
},
ua: {
name: 'Ім`я',
@ -193,6 +197,10 @@ const messages = { // eslint-disable-line no-unused-vars
downloadConfig: 'Télécharger la configuration',
madeBy: 'Développé par',
donate: 'Soutenir',
restore: 'Restaurer',
backup: 'Sauvegarder',
titleRestoreConfig: 'Restaurer votre configuration',
titleBackupConfig: 'Sauvegarder votre configuration',
},
de: { // github.com/florian-asche
name: 'Name',
@ -221,6 +229,10 @@ const messages = { // eslint-disable-line no-unused-vars
downloadConfig: 'Konfiguration herunterladen',
madeBy: 'Erstellt von',
donate: 'Spenden',
restore: 'Wiederherstellen',
backup: 'Sichern',
titleRestoreConfig: 'Stelle deine Konfiguration wieder her',
titleBackupConfig: 'Sichere deine Konfiguration',
},
ca: { // github.com/guillembonet
name: 'Nom',
@ -277,6 +289,10 @@ const messages = { // eslint-disable-line no-unused-vars
donate: 'Donar',
toggleCharts: 'Mostrar/Ocultar gráficos',
theme: { dark: 'Modo oscuro', light: 'Modo claro', auto: 'Modo automático' },
restore: 'Restaurar',
backup: 'Realizar copia de seguridad',
titleRestoreConfig: 'Restaurar su configuración',
titleBackupConfig: 'Realizar copia de seguridad de su configuración',
},
ko: {
name: '이름',
@ -445,27 +461,27 @@ const messages = { // eslint-disable-line no-unused-vars
password: '密碼',
signIn: '登入',
logout: '登出',
updateAvailable: '有新版本可用!',
updateAvailable: '有新版本可以使用!',
update: '更新',
clients: '客戶',
new: '建',
deleteClient: '刪除客戶',
clients: '使用者',
new: '建',
deleteClient: '刪除使用者',
deleteDialog1: '您確定要刪除',
deleteDialog2: '此操作無法撤銷。',
deleteDialog2: '此作業無法復原。',
cancel: '取消',
create: '建立',
createdOn: '建立於 ',
lastSeen: '最後訪問於 ',
lastSeen: '最後存取於 ',
totalDownload: '總下載: ',
totalUpload: '總上傳: ',
newClient: '新戶',
disableClient: '禁用客戶',
enableClient: '啟用客戶',
noClients: '目前沒有客戶。',
showQR: '顯示二維碼',
downloadConfig: '下載配置',
newClient: '新戶',
disableClient: '停用使用者',
enableClient: '啟用使用者',
noClients: '目前沒有使用者。',
showQR: '顯示 QR Code',
downloadConfig: '下載 Config 檔',
madeBy: '由',
donate: '捐贈',
donate: '抖內',
},
it: {
name: 'Nome',
@ -493,6 +509,10 @@ const messages = { // eslint-disable-line no-unused-vars
downloadConfig: 'Scarica configurazione',
madeBy: 'Realizzato da',
donate: 'Donazione',
restore: 'Ripristina',
backup: 'Backup',
titleRestoreConfig: 'Ripristina la tua configurazione',
titleBackupConfig: 'Esegui il backup della tua configurazione',
},
th: {
name: 'ชื่อ',

8
src/www/js/vendor/apexcharts.min.js

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save