Browse Source

feat: firewall

- add rule on chain WGEASY
- delete rule on chain WGEASY
- restore if rules config exists otherwise create new config
pull/1210/head
tetuaoro 2 years ago
parent
commit
db087a251e
  1. 22
      src/lib/Firewall.js
  2. 16
      src/lib/Server.js
  3. 32
      src/lib/Util.js
  4. 13
      src/server.js
  5. 2
      src/tailwind.config.js
  6. 5
      src/www/css/app.css
  7. 27
      src/www/js/api.js
  8. 19
      src/www/js/i18n.js
  9. 2
      src/www/js/vendor/vue-firewall.umd.min.js
  10. 2
      src/www/js/vendor/vue-firewall.umd.min.js.map
  11. 111
      src/www/templates/Firewall.vue

22
src/lib/Firewall.js

@ -46,6 +46,7 @@ module.exports = class Firewall {
async getIptablesRules() {
// iptables list the rules WG_IPT_CHAIN_NAME chain
<<<<<<< HEAD
//
// $ iptables -L WGEASY -nv --line-numbers
// Chain WGEASY (0 references)
@ -56,6 +57,18 @@ module.exports = class Firewall {
// Chain (0 references)
// num target prot source destination
// 1 ACCEPT 6 172.16.7.2 10.8.2.5
=======
// $ iptables -L ${WG_IPT_CHAIN_NAME} -n -v
// Chain WGEASY (0 references)
// num pkts bytes target prot opt in out source destination
// 1 0 0 DROP 6 -- * * 172.16.8.2 172.16.8.3
// 2 0 0 ACCEPT 6 -- * * 172.16.9.2 172.16.8.3
// $ iptables -L ${WG_IPT_CHAIN_NAME} -n -v | awk '{print $3,$4,$8,$9}'
// (0 references)
// num target prot source destination
// 1 DROP 6 172.16.8.2 172.16.8.3
// 2 ACCEPT 6 172.16.9.2 172.16.8.3
>>>>>>> 9ec7359 (feat: firewall)
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/);
@ -74,6 +87,7 @@ module.exports = class Firewall {
async addIptablesRule(source, destination, protocol, target) {
debug('Rule adding...');
<<<<<<< HEAD
// Validate target & protocol
if (!Util.isTarget(target) || !Util.isProtocol(protocol)) {
throw new Error('Invalid target or protocol.');
@ -117,6 +131,14 @@ module.exports = class Firewall {
iptablesCommand += ` -p ${protocol} -j ${target}`;
await Util.exec(iptablesCommand);
=======
// Validate target
if (target !== 'ACCEPT' && target !== 'DROP') {
throw new Error('Invalid action. Must be "ACCEPT" or "DROP".');
}
await Util.exec(`iptables -A ${WG_IPT_CHAIN_NAME} -s ${source} -d ${destination} -p ${protocol} -j ${target}`);
>>>>>>> 9ec7359 (feat: firewall)
await this.__saveIptablesRules();
debug('Rule added');
}

16
src/lib/Server.js

@ -284,6 +284,22 @@ module.exports = class Server {
.get('/api/fw/interfaces', defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'text/json');
return Firewall.getInterfaces();
}))
.get('/api/fw/rules', defineEventHandler(async (event) => {
setHeader(event, 'Content-Type', 'text/json');
return Firewall.getIptablesRules();
}))
.post('/api/fw/rule', defineEventHandler(async (event) => {
const {
source, destination, protocol, target,
} = await readBody(event);
await Firewall.addIptablesRule(source, destination, protocol, target);
return { success: true };
}))
.delete('/api/fw/rule', defineEventHandler(async (event) => {
const { num } = await readBody(event);
await Firewall.deleteIptablesRule(num);
return { success: true };
}));
// Static assets

32
src/lib/Util.js

@ -2,6 +2,30 @@
const childProcess = require('child_process');
const Protocol = Object.freeze({
ALL: 0,
TCP: 6,
UDP: 17,
});
const ProtocolName = Object.freeze({
[Protocol.ALL]: '*',
[Protocol.TCP]: 'TCP',
[Protocol.UDP]: 'UDP',
});
const Target = Object.freeze({
RETURN: 'RETURN',
ALLOW: 'ACCEPT',
BLOCK: 'DROP',
});
const TargetName = Object.freeze({
[Target.RETURN]: 'RETURN',
[Target.ALLOW]: 'ALLOW',
[Target.BLOCK]: 'BLOCK',
});
module.exports = class Util {
static isValidIPv4(str) {
@ -77,4 +101,12 @@ module.exports = class Util {
});
}
static getProtocolName(protocolNumber) {
return ProtocolName[protocolNumber] || 'Unknown Protocol';
}
static getTargetName(target) {
return TargetName[target] || 'Unknown Target';
}
};

13
src/server.js

@ -1,13 +1,22 @@
'use strict';
require('./services/Server');
require('./services/Firewall');
const WireGuard = require('./services/WireGuard');
const Firewall = require('./services/Firewall');
WireGuard.getConfig()
.then(() => {
Firewall.init().catch((err) => {
// eslint-disable-next-line no-console
console.error(err);
// eslint-disable-next-line no-process-exit
process.exit(1);
});
})
.catch((err) => {
// eslint-disable-next-line no-console
// eslint-disable-next-line no-console
console.error(err);
// eslint-disable-next-line no-process-exit

2
src/tailwind.config.js

@ -4,7 +4,7 @@
module.exports = {
darkMode: 'selector',
content: ['./www/**/*.{html,js}'],
content: ['./www/**/*.{html,js,vue}'],
theme: {
screens: {
xxs: '450px',

5
src/www/css/app.css

@ -1515,6 +1515,11 @@ video {
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.hover\:bg-green-600:hover {
--tw-bg-opacity: 1;
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
}
.hover\:bg-red-700:hover {
--tw-bg-opacity: 1;
background-color: rgb(185 28 28 / var(--tw-bg-opacity));

27
src/www/js/api.js

@ -153,4 +153,31 @@ class API {
});
}
async getRules() {
return this.call({
method: 'get',
path: '/fw/rules',
});
}
async addRule({
source, destination, protocol, target,
}) {
return this.call({
method: 'post',
path: '/fw/rule',
body: {
source, destination, protocol, target,
},
});
}
async deleteRule({ num }) {
return this.call({
method: 'delete',
path: '/fw/rule',
body: { num },
});
}
}

19
src/www/js/i18n.js

@ -34,7 +34,15 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Backup',
titleRestoreConfig: 'Restore your configuration',
titleBackupConfig: 'Backup your configuration',
firewall: 'Firewall',
fwFirewall: 'Firewall',
fwInterface: 'Interface',
fwIpv4: 'IPv4 address',
fwIpv6: 'IPv6 address',
fwAction: 'Action',
fwSource: 'Source',
fwDestination: 'Destination',
fwProtocol: 'Protocol',
fwTarget: 'Target',
},
ua: {
name: 'Ім`я',
@ -209,6 +217,15 @@ const messages = { // eslint-disable-line no-unused-vars
backup: 'Sauvegarder',
titleRestoreConfig: 'Restaurer votre configuration',
titleBackupConfig: 'Sauvegarder votre configuration',
fwFirewall: 'Parefeu',
fwInterface: 'Interface',
fwIpv4: 'Address IPv4',
fwIpv6: 'Address IPv4',
fwAction: 'Action',
fwSource: 'Source',
fwDestination: 'Destination',
fwProtocol: 'Protocole',
fwTarget: 'Règle',
},
de: { // github.com/florian-asche
name: 'Name',

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

File diff suppressed because one or more lines are too long

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

File diff suppressed because one or more lines are too long

111
src/www/templates/Firewall.vue

@ -3,7 +3,14 @@ export default {
name: 'Firewall',
data() {
let interfaces = [];
return { interfaces };
let rules = [];
let newRule = {
source: '',
destination: '',
protocol: '',
target: '',
};
return { interfaces, rules, newRule };
},
methods: {
getInterfaces() {
@ -13,11 +20,44 @@ export default {
.catch((err) => {
alert(err.message || err.toString());
})
}
},
getRules() {
this.api.getRules().then((rules) => {
this.rules = rules;
})
.catch((err) => {
alert(err.message || err.toString());
})
},
addRule(e) {
e.preventDefault();
const { source, destination, protocol, target } = this.newRule;
this.api.addRule({ source, destination, protocol, target }).then(() => {
this.getRules();
}).then(() => {
this.newRule = {
source: '',
destination: '',
protocol: '',
target: '',
};
}).catch((err) => {
alert(err.message || err.toString());
});
},
deleteRule(num) {
this.api.deleteRule({ num }).then(() => {
this.getRules();
}).catch((err) => {
alert(err.message || err.toString());
});
},
},
mounted() {
this.api = new API();
this.getInterfaces();
this.getRules();
}
}
</script>
@ -26,16 +66,16 @@ export default {
<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>
<p class="text-2xl font-medium dark:text-neutral-200">{{ $t("fwFirewall") }}</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>
<th>{{ $t('fwInterface') }}</th>
<th>{{ $t('fwIv4') }}</th>
<th>{{ $t('fwIv6') }}</th>
</tr>
</thead>
<tbody class="text-center">
@ -50,43 +90,58 @@ export default {
<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>
<th>{{ $t('fwAction') }}</th>
<th class="border-x-2 dark:border-neutral-200">{{ $t('fwSource') }}</th>
<th class="border-x-2 dark:border-neutral-200">{{ $t('fwDestination') }}</th>
<th class="border-x-2 dark:border-neutral-200">{{ $t('fwProtocol') }}</th>
<th>{{ $t('fwTarget') }}</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 v-for="(rule, index) in rules" :key="index">
<td class="border border-gray-600 p-1 hover:bg-red-800 transition">
<button @click="deleteRule(rule.num)">
<svg 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="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</td>
<td class="border border-gray-600 p-1">{{ rule.source }}</td>
<td class="border border-gray-600 p-1">{{ rule.destination }}</td>
<td class="border border-gray-600 p-1">{{ rule.protocol }}</td>
<td class="border border-gray-600 p-1">{{ rule.target }}</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" />
<td class="border border-gray-600 p-1 hover:bg-green-600 transition">
<form id="fw" @submit="addRule">
<button type="submit">
<svg 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="M12 9v6m3-3H9m12 0a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</button>
</form>
</td>
<td class="border border-gray-600">
<input form="fw" type="text" name="source" class="w-full outline-none bg-transparent text-center" />
<input form="fw" type="text" name="source" v-model="newRule.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" />
<input form="fw" type="text" name="destination" v-model="newRule.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 form="fw" name="protocol" v-model="newRule.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 form="fw" name="target" v-model="newRule.target" class="bg-transparent">
<option value="ACCEPT">ALLOW</option>
<option value="DROP">BLOCK</option>
</select>
</td>
</tr>

Loading…
Cancel
Save