Browse Source

get network interfaces container

pull/1210/head
tetuaoro 2 years ago
parent
commit
eaff7c5af8
  1. 2
      Dockerfile
  2. 153
      src/lib/Firewall.js
  3. 17
      src/lib/Server.js
  4. 28
      src/package-lock.json
  5. 6
      src/package.json
  6. 1
      src/server.js
  7. 5
      src/services/Firewall.js
  8. 8
      src/www/css/app.css
  9. 82
      src/www/index.html
  10. 7
      src/www/js/api.js
  11. 1
      src/www/js/app.js
  12. 1
      src/www/js/i18n.js
  13. 2
      src/www/js/vendor/vue-firewall.umd.min.js
  14. 1
      src/www/js/vendor/vue-firewall.umd.min.js.map
  15. 96
      src/www/templates/Firewall.vue

2
Dockerfile

@ -42,7 +42,7 @@ RUN apk add --no-cache \
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
# Set Environment
ENV DEBUG=Server,WireGuard
ENV DEBUG=Server,WireGuard,Firewall
# Run Web UI
WORKDIR /app

153
src/lib/Firewall.js

@ -0,0 +1,153 @@
'use strict';
const { existsSync } = require('fs');
const path = require('path');
const debug = require('debug')('Firewall');
const Util = require('./Util');
const WG_IPT_CHAIN_NAME = 'WGEASY';
const WG_IPT_FILE_RULE_NAME = 'fw.rules';
const { WG_PATH } = require('../config');
module.exports = class Firewall {
async init() {
try {
this.rulesFile = path.join(WG_PATH, WG_IPT_FILE_RULE_NAME);
const configFile = this.rulesFile;
if (existsSync(configFile)) {
await this.__restoreIptablesRules();
}
// check otherwise create
await Util.exec(`iptables -L ${WG_IPT_CHAIN_NAME} -n`);
} catch (error) {
debug(`Create rules on chain "${WG_IPT_CHAIN_NAME}"...`);
await Util.exec(`iptables -N ${WG_IPT_CHAIN_NAME}`);
await this.__saveIptablesRules();
debug('Rules created.');
}
}
async __saveIptablesRules() {
debug('Rules saving...');
const configFile = this.rulesFile;
await Util.exec(`iptables-save > ${configFile}`);
debug('Rules saved.');
}
async __restoreIptablesRules() {
debug('Rules restoring...');
const configFile = this.rulesFile;
await Util.exec(`iptables-restore < ${configFile}`);
debug('Rules restored.');
}
async getIptablesRules() {
// iptables list the rules WG_IPT_CHAIN_NAME chain
//
// $ iptables -L WGEASY -nv --line-numbers
// Chain WGEASY (0 references)
// num pkts bytes target prot opt in out source destination
// 1 0 0 ACCEPT 6 -- * * 172.16.7.2 10.8.2.5
//
// $ iptables -L ${WG_IPT_CHAIN_NAME} -nv --line-numbers | awk '{print $1,$4,$5,$9,$10}'
// Chain (0 references)
// num target prot source destination
// 1 ACCEPT 6 172.16.7.2 10.8.2.5
const stdout = await Util.exec(`iptables -L ${WG_IPT_CHAIN_NAME} -nv --line-numbers | awk '{print $1,$4,$5,$9,$10}'`);
const lines = stdout.split(/\r?\n/);
const rules = lines.slice(2).map((line) => {
const [num, target, protocol, source, destination] = line.trim().split(' ');
const targetToName = Util.getTargetName(target);
const protocolToName = Util.getProtocolName(protocol);
return {
num, target: targetToName, source, destination, protocol: protocolToName,
};
}).filter((rule) => rule !== null);
return JSON.stringify(rules);
}
async addIptablesRule(source, destination, protocol, target) {
debug('Rule adding...');
// Validate target & protocol
if (!Util.isTarget(target) || !Util.isProtocol(protocol)) {
throw new Error('Invalid target or protocol.');
}
/*
Support command :
iptables -A CHAIN -s [IPv4 | IPv4/CIDR] [--sport PORT] -d [IPv4 | IPv4/CIDR] [--dport PORT] -p PROTOCOL -j TARGET
*/
let iptablesCommand = `iptables -A ${WG_IPT_CHAIN_NAME}`;
if (Util.isSupportedAddress(source)) {
const [address, port] = source.split(':');
if (Util.isIPAddress(address) || Util.isCIDR(address)) {
iptablesCommand += ` -s ${address}`;
if (port) {
iptablesCommand += ` --sport ${port}`;
}
} else {
throw new Error('Invalid source address.');
}
} else {
throw new Error('Invalid source format.');
}
if (Util.isSupportedAddress(destination)) {
const [address, port] = destination.split(':');
if (Util.isIPAddress(address) || Util.isCIDR(address)) {
iptablesCommand += ` -d ${address}`;
if (port) {
iptablesCommand += ` --dport ${port}`;
}
} else {
throw new Error('Invalid destination address.');
}
} else {
throw new Error('Invalid destination format.');
}
iptablesCommand += ` -p ${protocol} -j ${target}`;
await Util.exec(iptablesCommand);
await this.__saveIptablesRules();
debug('Rule added');
}
async deleteIptablesRule(num) {
debug('Rule deleting...');
await Util.exec(`iptables -D ${WG_IPT_CHAIN_NAME} ${num}`);
await this.__saveIptablesRules();
debug('Rule deleted.');
}
async getInterfaces() {
// $ ip -brief address show
// lo UNKNOWN 127.0.0.1/8 ::1/128
// eth0@if150 UP 172.17.0.3/16
// $ ip -brief address show | awk '{print $1,$3,$4}'
// lo 127.0.0.1/8 ::1/128
// eth0@if150 172.17.0.3/16
const interfaces = await Util.exec("ip -brief a s | awk '{print $1,$3,$4}'");
const result = [];
if (typeof interfaces === 'string') {
for (const line of interfaces.split(/\r?\n/)) {
const [name, ipv4, ipv6] = line.split(' ');
result.push({
name, ipv4: ipv4 || '', ipv6: ipv6 || '',
});
}
}
return JSON.stringify(result);
}
};

17
src/lib/Server.js

@ -23,6 +23,7 @@ const {
} = require('h3');
const WireGuard = require('../services/WireGuard');
const Firewall = require('../services/Firewall');
const {
PORT,
@ -259,10 +260,10 @@ module.exports = class Server {
};
// backup_restore
const router3 = createRouter();
app.use(router3);
const routerBackupRestore = createRouter();
app.use(routerBackupRestore);
router3
routerBackupRestore
.get('/api/wireguard/backup', defineEventHandler(async (event) => {
const config = await WireGuard.backupConfiguration();
setHeader(event, 'Content-Disposition', 'attachment; filename="wg0.json"');
@ -275,6 +276,16 @@ module.exports = class Server {
return { success: true };
}));
// firewall
const routerFirewall = createRouter();
app.use(routerFirewall);
routerFirewall
.get('/api/fw/interfaces', defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'text/json');
return Firewall.getInterfaces();
}));
// Static assets
const publicDir = '/app/www';
app.use(

28
src/package-lock.json

@ -18,7 +18,8 @@
"devDependencies": {
"eslint-config-athom": "^3.1.3",
"nodemon": "^3.1.4",
"tailwindcss": "^3.4.7"
"tailwindcss": "^3.4.7",
"vue-template-compiler": "^2.7.16"
},
"engines": {
"node": ">=18"
@ -1305,6 +1306,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz",
"integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==",
"dev": true
},
"node_modules/debug": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
@ -2663,6 +2670,15 @@
"node": ">= 0.4"
}
},
"node_modules/he": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
"dev": true,
"bin": {
"he": "bin/he"
}
},
"node_modules/ignore": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
@ -5023,6 +5039,16 @@
"license": "MIT",
"peer": true
},
"node_modules/vue-template-compiler": {
"version": "2.7.16",
"resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz",
"integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==",
"dev": true,
"dependencies": {
"de-indent": "^1.0.2",
"he": "^1.2.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

6
src/package.json

@ -10,7 +10,8 @@
"serve": "DEBUG=Server,WireGuard npx nodemon server.js",
"serve-with-password": "PASSWORD=wg npm run serve",
"lint": "eslint .",
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css"
"buildcss": "npx tailwindcss -i ./www/src/css/app.css -o ./www/css/app.css",
"buildfirewall": "vue-cli-service build --dest www/js/vendor --no-module --formats umd-min --target lib --name VueFirewall --filename vue-firewall --no-clean www/templates/Firewall.vue"
},
"author": "Emile Nijssen",
"license": "CC BY-NC-SA 4.0",
@ -24,7 +25,8 @@
"devDependencies": {
"eslint-config-athom": "^3.1.3",
"nodemon": "^3.1.4",
"tailwindcss": "^3.4.7"
"tailwindcss": "^3.4.7",
"vue-template-compiler": "^2.7.16"
},
"nodemonConfig": {
"ignore": [

1
src/server.js

@ -1,6 +1,7 @@
'use strict';
require('./services/Server');
require('./services/Firewall');
const WireGuard = require('./services/WireGuard');

5
src/services/Firewall.js

@ -0,0 +1,5 @@
'use strict';
const Firewall = require('../lib/Firewall');
module.exports = new Firewall();

8
src/www/css/app.css

@ -1567,10 +1567,6 @@ video {
}
@media not all and (min-width: 768px) {
.max-md\:flex {
display: flex;
}
.max-md\:hidden {
display: none;
}
@ -1720,6 +1716,10 @@ video {
margin-right: 0.5rem;
}
.md\:block {
display: block;
}
.md\:inline-block {
display: inline-block;
}

82
src/www/index.html

@ -88,7 +88,7 @@
</div>
</div>
<div class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden">
<div class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden mb-4">
<div class="flex flex-row flex-auto items-center p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600">
<div class="flex-grow">
<p class="text-2xl font-medium dark:text-neutral-200">{{$t("clients")}}</p>
@ -384,84 +384,7 @@
</div>
<!-- Firewall -->
<div class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden">
<div
class="flex flex-row flex-auto items-center mb-4 p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600">
<div class="flex-grow">
<p class="text-2xl font-medium dark:text-neutral-200">Firewall</p>
</div>
</div>
<table class="table-auto w-[70%] dark:text-neutral-200 mb-4">
<thead>
<tr>
<th>Interface</th>
<th>IPv4 address</th>
<th>IPv6 address</th>
</tr>
</thead>
<tbody class="text-center">
<tr>
<td>eth0</td>
<td>192.168.1.1</td>
<td></td>
</tr>
<tr>
<td>wg0</td>
<td>10.8.0.1</td>
<td></td>
</tr>
</tbody>
</table>
<table class="table-auto border-t-2 dark:border-neutral-200 w-full dark:text-neutral-200">
<thead class="bg-gray-200 dark:bg-gray-600">
<tr>
<th></th>
<th class="border-x-2 dark:border-neutral-200">Source</th>
<th class="border-x-2 dark:border-neutral-200">Destination</th>
<th class="border-x-2 dark:border-neutral-200">Protocol</th>
<th>Action</th>
</tr>
</thead>
<tbody class="text-center">
<tr>
<td class="border border-gray-600 p-1">delete</td>
<td class="border border-gray-600 p-1">192.168.11.3</td>
<td class="border border-gray-600 p-1">10.9.8.1</td>
<td class="border border-gray-600 p-1">udp</td>
<td class="border border-gray-600 p-1"></td>
</tr>
<tr>
<td class="border border-gray-600 p-1">
<form id="fw">
<input form="fw" type="submit" value="send"
class="w-full outline-none bg-transparent text-center"/>
</form>
</td>
<td class="border border-gray-600">
<input form="fw" type="text" name="source" class="w-full outline-none bg-transparent text-center"/>
</td>
<td class="border border-gray-600">
<input form="fw" type="text" name="destination"
class="w-full outline-none bg-transparent text-center"/>
</td>
<td class="border border-gray-600">
<select form="fw" name="protocol" class="bg-transparent">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</td>
<td class="border border-gray-600 p-1">
<select form="fw" name="action" class="bg-transparent">
<option value="allow">allow</option>
<option value="block">block</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
<firewall></firewall>
<!-- QR Code-->
<div v-if="qrcode">
@ -689,6 +612,7 @@
<script src="./js/vendor/vue-apexcharts.min.js"></script>
<script src="./js/vendor/sha256.min.js"></script>
<script src="./js/vendor/timeago.full.min.js"></script>
<script src="./js/vendor/vue-firewall.umd.min.js"></script>
<script src="./js/api.js"></script>
<script src="./js/i18n.js"></script>
<script src="./js/app.js"></script>

7
src/www/js/api.js

@ -146,4 +146,11 @@ class API {
});
}
async getInterfaces() {
return this.call({
method: 'get',
path: '/fw/interfaces',
});
}
}

1
src/www/js/app.js

@ -46,6 +46,7 @@ new Vue({
el: '#app',
components: {
apexchart: VueApexCharts,
firewall: VueFirewall,
},
i18n,
data: {

1
src/www/js/i18n.js

@ -34,6 +34,7 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Backup',
titleRestoreConfig: 'Restore your configuration',
titleBackupConfig: 'Backup your configuration',
firewall: 'Firewall',
},
ua: {
name: 'Ім`я',

2
src/www/js/vendor/vue-firewall.umd.min.js

@ -0,0 +1,2 @@
(function(t,e){"object"===typeof exports&&"object"===typeof module?module.exports=e():"function"===typeof define&&define.amd?define([],e):"object"===typeof exports?exports["VueFirewall"]=e():t["VueFirewall"]=e()})("undefined"!==typeof self?self:this,(()=>(()=>{"use strict";var t={};(()=>{t.d=(e,r)=>{for(var a in r)t.o(r,a)&&!t.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:r[a]})}})(),(()=>{t.o=(t,e)=>Object.prototype.hasOwnProperty.call(t,e)})(),(()=>{t.p=""})();var e={};if(t.d(e,{default:()=>u}),"undefined"!==typeof window){var r=window.document.currentScript,a=r&&r.src.match(/(.+\/)[^/]+\.js(\?.*)?$/);a&&(t.p=a[1])}var s=function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("div",{staticClass:"shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden"},[r("div",{staticClass:"flex flex-row flex-auto items-center mb-4 p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600"},[r("div",{staticClass:"flex-grow"},[r("p",{staticClass:"text-2xl font-medium dark:text-neutral-200"},[t._v(t._s(t.$t("firewall")))])])]),r("table",{staticClass:"table-auto w-[70%] dark:text-neutral-200 mb-4"},[t._m(0),r("tbody",{staticClass:"text-center"},t._l(t.interfaces,(function(e,a){return r("tr",{key:a},[r("td",[t._v(t._s(e.name))]),r("td",[t._v(t._s(e.ipv4))]),r("td",[t._v(t._s(e.ipv6))])])})),0)]),t._m(1)])},o=[function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("thead",[r("tr",[r("th",[t._v("Interface")]),r("th",[t._v("IPv4 address")]),r("th",[t._v("IPv6 address")])])])},function(){var t=this,e=t.$createElement,r=t._self._c||e;return r("table",{staticClass:"table-auto border-t-2 dark:border-neutral-200 w-full dark:text-neutral-200"},[r("thead",{staticClass:"bg-gray-200 dark:bg-gray-600"},[r("tr",[r("th",[t._v("Action")]),r("th",{staticClass:"border-x-2 dark:border-neutral-200"},[t._v("Source")]),r("th",{staticClass:"border-x-2 dark:border-neutral-200"},[t._v("Destination")]),r("th",{staticClass:"border-x-2 dark:border-neutral-200"},[t._v("Protocol")]),r("th")])]),r("tbody",{staticClass:"text-center"},[r("tr",[r("td",{staticClass:"border border-gray-600 p-1"},[t._v("delete")]),r("td",{staticClass:"border border-gray-600 p-1"},[t._v("192.168.11.3")]),r("td",{staticClass:"border border-gray-600 p-1"},[t._v("10.9.8.1")]),r("td",{staticClass:"border border-gray-600 p-1"},[t._v("udp")]),r("td",{staticClass:"border border-gray-600 p-1"})]),r("tr",[r("td",{staticClass:"border border-gray-600 p-1"},[r("form",{attrs:{id:"fw"}},[r("input",{staticClass:"w-full outline-none bg-transparent text-center",attrs:{form:"fw",type:"submit",value:"send"}})])]),r("td",{staticClass:"border border-gray-600"},[r("input",{staticClass:"w-full outline-none bg-transparent text-center",attrs:{form:"fw",type:"text",name:"source"}})]),r("td",{staticClass:"border border-gray-600"},[r("input",{staticClass:"w-full outline-none bg-transparent text-center",attrs:{form:"fw",type:"text",name:"destination"}})]),r("td",{staticClass:"border border-gray-600"},[r("select",{staticClass:"bg-transparent",attrs:{form:"fw",name:"protocol"}},[r("option",{attrs:{value:"tcp"}},[t._v("tcp")]),r("option",{attrs:{value:"udp"}},[t._v("udp")])])]),r("td",{staticClass:"border border-gray-600 p-1"},[r("select",{staticClass:"bg-transparent",attrs:{form:"fw",name:"action"}},[r("option",{attrs:{value:"allow"}},[t._v("allow")]),r("option",{attrs:{value:"block"}},[t._v("block")])])])])])])}];const n={name:"Firewall",data(){let t=[];return{interfaces:t}},methods:{getInterfaces(){this.api.getInterfaces().then((t=>{this.interfaces=t})).catch((t=>{alert(t.message||t.toString())}))}},mounted(){this.api=new API,this.getInterfaces()}},i=n;function d(t,e,r,a,s,o,n,i){var d,l="function"===typeof t?t.options:t;if(e&&(l.render=e,l.staticRenderFns=r,l._compiled=!0),a&&(l.functional=!0),o&&(l._scopeId="data-v-"+o),n?(d=function(t){t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,t||"undefined"===typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),s&&s.call(this,t),t&&t._registeredComponents&&t._registeredComponents.add(n)},l._ssrRegister=d):s&&(d=i?function(){s.call(this,(l.functional?this.parent:this).$root.$options.shadowRoot)}:s),d)if(l.functional){l._injectStyles=d;var c=l.render;l.render=function(t,e){return d.call(e),c(t,e)}}else{var u=l.beforeCreate;l.beforeCreate=u?[].concat(u,d):[d]}return{exports:t,options:l}}var l=d(i,s,o,!1,null,null,null);const c=l.exports,u=c;return e=e["default"],e})()));
//# sourceMappingURL=vue-firewall.umd.min.js.map

1
src/www/js/vendor/vue-firewall.umd.min.js.map

File diff suppressed because one or more lines are too long

96
src/www/templates/Firewall.vue

@ -0,0 +1,96 @@
<script>
export default {
name: 'Firewall',
data() {
let interfaces = [];
return { interfaces };
},
methods: {
getInterfaces() {
this.api.getInterfaces().then((interfaces) => {
this.interfaces = interfaces;
})
.catch((err) => {
alert(err.message || err.toString());
})
}
},
mounted() {
this.api = new API();
this.getInterfaces();
}
}
</script>
<template>
<div class="shadow-md rounded-lg bg-white dark:bg-neutral-700 overflow-hidden">
<div class="flex flex-row flex-auto items-center mb-4 p-3 px-5 border-b-2 border-gray-100 dark:border-neutral-600">
<div class="flex-grow">
<p class="text-2xl font-medium dark:text-neutral-200">{{ $t("firewall") }}</p>
</div>
</div>
<table class="table-auto w-[70%] dark:text-neutral-200 mb-4">
<thead>
<tr>
<th>Interface</th>
<th>IPv4 address</th>
<th>IPv6 address</th>
</tr>
</thead>
<tbody class="text-center">
<tr v-for="(iface, index) in interfaces" :key="index">
<td>{{ iface.name }}</td>
<td>{{ iface.ipv4 }}</td>
<td>{{ iface.ipv6 }}</td>
</tr>
</tbody>
</table>
<table class="table-auto border-t-2 dark:border-neutral-200 w-full dark:text-neutral-200">
<thead class="bg-gray-200 dark:bg-gray-600">
<tr>
<th>Action</th>
<th class="border-x-2 dark:border-neutral-200">Source</th>
<th class="border-x-2 dark:border-neutral-200">Destination</th>
<th class="border-x-2 dark:border-neutral-200">Protocol</th>
<th></th>
</tr>
</thead>
<tbody class="text-center">
<tr>
<td class="border border-gray-600 p-1">delete</td>
<td class="border border-gray-600 p-1">192.168.11.3</td>
<td class="border border-gray-600 p-1">10.9.8.1</td>
<td class="border border-gray-600 p-1">udp</td>
<td class="border border-gray-600 p-1"></td>
</tr>
<tr>
<td class="border border-gray-600 p-1">
<form id="fw">
<input form="fw" type="submit" value="send" class="w-full outline-none bg-transparent text-center" />
</form>
</td>
<td class="border border-gray-600">
<input form="fw" type="text" name="source" class="w-full outline-none bg-transparent text-center" />
</td>
<td class="border border-gray-600">
<input form="fw" type="text" name="destination" class="w-full outline-none bg-transparent text-center" />
</td>
<td class="border border-gray-600">
<select form="fw" name="protocol" class="bg-transparent">
<option value="tcp">tcp</option>
<option value="udp">udp</option>
</select>
</td>
<td class="border border-gray-600 p-1">
<select form="fw" name="action" class="bg-transparent">
<option value="allow">allow</option>
<option value="block">block</option>
</select>
</td>
</tr>
</tbody>
</table>
</div>
</template>
Loading…
Cancel
Save