luci-app-libreswan: Add LuCI for Libreswan
authorJaymin Patel <jem.patel@gmail.com>
Sun, 22 Oct 2023 06:39:57 +0000 (12:09 +0530)
committerJo-Philipp Wich <jo@mein.io>
Fri, 10 Nov 2023 12:16:34 +0000 (13:16 +0100)
LuCI Support for IPSec VPN (Libreswan)

Signed-off-by: Jaymin Patel <jem.patel@gmail.com>
[fix '15h' label typo]
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
applications/luci-app-libreswan/Makefile [new file with mode: 0644]
applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/globals.js [new file with mode: 0644]
applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/overview.js [new file with mode: 0644]
applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/proposals.js [new file with mode: 0644]
applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/tunnels.js [new file with mode: 0644]
applications/luci-app-libreswan/root/etc/uci-defaults/802-luci-app-libreswan [new file with mode: 0644]
applications/luci-app-libreswan/root/usr/share/luci/menu.d/luci-app-libreswan.json [new file with mode: 0644]
applications/luci-app-libreswan/root/usr/share/rpcd/acl.d/luci-app-libreswan.json [new file with mode: 0644]

diff --git a/applications/luci-app-libreswan/Makefile b/applications/luci-app-libreswan/Makefile
new file mode 100644 (file)
index 0000000..4631c33
--- /dev/null
@@ -0,0 +1,19 @@
+#
+# Copyright (C) 2022 Jaymin Patel <jem.patel@gmail.com>
+#
+# This is free software, licensed under the GNU General Public License v2.
+#
+
+include $(TOPDIR)/rules.mk
+
+PKG_LICENSE:=GPL-2.0-or-later
+PKG_MAINTAINER:=Jaymin Patel <jem.patel@gmail.com>
+
+LUCI_TITLE=Luci Application for IPSec VPN (Libreswan)
+LUCI_DEPENDS:=+luci-base +libreswan
+LUCI_PKGARCH:=all
+
+include ../../luci.mk
+
+# call BuildPackage - OpenWrt buildroot signature
+
diff --git a/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/globals.js b/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/globals.js
new file mode 100644 (file)
index 0000000..3050e53
--- /dev/null
@@ -0,0 +1,72 @@
+'use strict';
+'require view';
+'require form';
+'require network';
+'require tools.widgets as widgets';
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       network.getDevices(),
+               ]);
+       },
+
+       render: function(data) {
+               var netDevs = data[0];
+               var m, s, o;
+
+               m = new form.Map('libreswan', _('IPSec Global Settings'));
+
+               s = m.section(form.NamedSection, 'globals', 'libreswan');
+               s.anonymous = false;
+               s.addremove = false;
+
+               o = s.option(form.ListValue, 'debug', _('Debug Logs'));
+               o.default = false;
+               o.rmempty = false;
+               o.value('none', _('none - No Logging'));
+               o.value('base', _('base - Moderate Logging'));
+               o.value('cpu-usage', _('cpu-usage - Timing/Load Logging'));
+               o.value('crypto', _('crypto - All crypto related Logging'));
+               o.value('tmi', _('tmi - Too Much/Excessive Logging'));
+               o.value('private', _('private - Sensitive private-key/password Logging'));
+               o.default = 'none'
+
+               o = s.option(form.Flag, 'uniqueids', _('Uniquely Identify Remotes'),
+                       _('Whether IDs should be considered identifying remote parties uniquely'));
+               o.default = false;
+               o.rmempty = false;
+
+               o = s.option(widgets.NetworkSelect, 'listen_interface', _('Listen Interface'),
+                       _('Interface for IPsec to use'));
+               o.datatype = 'string';
+               o.multiple = false;
+               o.optional = true;
+
+               o = s.option(form.Value, 'listen', _('Listen Address'),
+                       _('IP address to listen on, default depends on Listen Interface'));
+               o.datatype = 'ip4addr';
+               for (var i = 0; i < netDevs.length; i++) {
+                       var addrs = netDevs[i].getIPAddrs();
+                       for (var j = 0; j < addrs.length; j++) {
+                               o.value(addrs[j].split('/')[0]);
+                       }
+               }
+               o.depends({ 'listen_interface' : '' });
+
+               o = s.option(form.Value, 'nflog_all', _('Enable nflog on nfgroup'),
+                       _('NFLOG group number to log all pre-crypt and post-decrypt traffic to'));
+               o.datatype = 'uinteger';
+               o.default = 0;
+               o.rmempty = true;
+               o.optional = true;
+
+               o = s.option(form.DynamicList, 'virtual_private', _('Allowed Virtual Private'),
+                       _('The address ranges that may live behind a NAT router through which a client connects'));
+               o.datatype = 'neg(ip4addr)';
+               o.multiple = true;
+               o.optional = true;
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/overview.js b/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/overview.js
new file mode 100644 (file)
index 0000000..61a2e4c
--- /dev/null
@@ -0,0 +1,77 @@
+'use strict';
+'require view';
+'require rpc';
+'require form';
+'require poll';
+
+var callLibreswanStatus = rpc.declare({
+       object: 'libreswan',
+       method: 'status',
+       expect: {  },
+});
+
+function secondsToString(seconds) {
+       var str = '';
+       var numdays = Math.floor(seconds / 86400);
+       var numhours = Math.floor((seconds % 86400) / 3600);
+       var numminutes = Math.floor(((seconds % 86400) % 3600) / 60);
+       var numseconds = ((seconds % 86400) % 3600) % 60;
+
+       str = (numdays ? numdays + 'd ' : '') + (numhours ? numhours + 'h ' : '') + (numminutes ? numminutes + 'm ' : '') +  numseconds + 's';
+       return str;
+}
+
+return view.extend({
+       render: function() {
+               var table =
+                       E('table', { 'class': 'table lases' }, [
+                               E('tr', { 'class': 'tr table-titles' }, [
+                                       E('th', { 'class': 'th' }, _('Name')),
+                                       E('th', { 'class': 'th' }, _('Remote')),
+                                       E('th', { 'class': 'th' }, _('Local Subnet')),
+                                       E('th', { 'class': 'th' }, _('Remote Subnet')),
+                                       E('th', { 'class': 'th' }, _('Tx')),
+                                       E('th', { 'class': 'th' }, _('Rx')),
+                                       E('th', { 'class': 'th' }, _('Phase1')),
+                                       E('th', { 'class': 'th' }, _('Phase2')),
+                                       E('th', { 'class': 'th' }, _('Status')),
+                                       E('th', { 'class': 'th' }, _('Uptime')),
+                                       E([])
+                               ])
+                       ]);
+
+               poll.add(function() {
+                       return callLibreswanStatus().then(function(tunnelsInfo) {
+                               var tunnels = Array.isArray(tunnelsInfo.tunnels) ? tunnelsInfo.tunnels : [];
+
+                               cbi_update_table(table,
+                                       tunnels.map(function(tunnel) {
+                                               return  [
+                                                       tunnel.name,
+                                                       tunnel.right,
+                                                       tunnel.leftsubnet,
+                                                       tunnel.rightsubnet,
+                                                       tunnel.tx,
+                                                       tunnel.rx,
+                                                       tunnel.phase1 ? _('Up') : _('Down'),
+                                                       tunnel.phase2 ? _('Up') : _('Down'),
+                                                       tunnel.connected ? _('Up') : _('Down'),
+                                                       secondsToString(tunnel.uptime),
+                                               ];
+                                       }),
+                                       E('em', _('There are no active Tunnels'))
+                               );
+                       });
+               });
+
+               return E([
+                       E('h3', _('IPSec Tunnels Summary')),
+                       E('br'),
+                       table
+               ]);
+       },
+
+       handleSave: null,
+       handleSaveApply:null,
+       handleReset: null
+});
diff --git a/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/proposals.js b/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/proposals.js
new file mode 100644 (file)
index 0000000..8569e05
--- /dev/null
@@ -0,0 +1,56 @@
+'use strict';
+'require view';
+'require ui';
+'require form';
+
+return view.extend({
+       render: function() {
+               var m, s, o;
+
+               m = new form.Map('libreswan', _('IPSec Proposals'));
+
+               s = m.section(form.GridSection, 'crypto_proposal');
+               s.anonymous = false;
+               s.addremove = true;
+               s.nodescriptions = true;
+               s.addbtntitle = _('Add Proposal');
+
+               o = s.tab('general', _('General'));
+
+               o = s.taboption('general', form.MultiValue, 'hash_algorithm', _('Hash Algorithm'), ('* = %s').format(_('Unsafe')));
+               o.default = 'md5';
+               o.value('md5', _('MD5*'));
+               o.value('sha1', _('SHA1*'));
+               o.value('sha256', _('SHA256'));
+               o.value('sha384', _('SHA384'));
+               o.value('sha512', _('SHA512'));
+
+               o = s.taboption('general', form.MultiValue, 'encryption_algorithm', _('Encryption Method'), ('* = %s').format(_('Unsafe')));
+               o.default = 'aes';
+               o.value('3des', _('3DES*'))
+               o.value('aes', _('AES'))
+               o.value('aes_ctr', _('AES_CTR'));
+               o.value('aes_cbc', _('AES_CBC'));
+               o.value('aes128', _('AES128'));
+               o.value('aes192', _('AES192'));
+               o.value('aes256', _('AES256'));
+               o.value('camellia_cbc', _('CAMELLIA_CBC'));
+
+               o = s.taboption('general', form.MultiValue, 'dh_group', _('DH Group'),
+                       ('* = %s <a href="%s">RFC8247</a>.').format(_('Unsafe, See'), 'https://www.rfc-editor.org/rfc/rfc8247#section-2.4'));
+               o.default = 'modp1536';
+               o.value('modp1536', _('DH Group 5*'));
+               o.value('modp2048', _('DH Group 14'));
+               o.value('modp3072', _('DH Group 15'));
+               o.value('modp4096', _('DH Group 16'));
+               o.value('modp6144', _('DH Group 17'));
+               o.value('modp8192', _('DH Group 18'));
+               o.value('dh19', _('DH Group 19'));
+               o.value('dh20', _('DH Group 20'));
+               o.value('dh21', _('DH Group 21'));
+               o.value('dh22', _('DH Group 22*'));
+               o.value('dh31', _('DH Group 31'));
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/tunnels.js b/applications/luci-app-libreswan/htdocs/luci-static/resources/view/libreswan/tunnels.js
new file mode 100644 (file)
index 0000000..f7cb439
--- /dev/null
@@ -0,0 +1,267 @@
+'use strict';
+'require view';
+'require form';
+'require ui';
+'require uci';
+'require network';
+'require validation';
+'require tools.widgets as widgets';
+
+function calculateNetwork(addr, mask) {
+       addr = validation.parseIPv4(String(addr));
+
+       if (!isNaN(mask))
+               mask = validation.parseIPv4(network.prefixToMask(+mask));
+       else
+               mask = validation.parseIPv4(String(mask));
+
+       if (addr == null || mask == null)
+               return null;
+
+       return  [
+                       addr[0] & (mask[0] >>> 0 & 255),
+                       addr[1] & (mask[1] >>> 0 & 255),
+                       addr[2] & (mask[2] >>> 0 & 255),
+                       addr[3] & (mask[3] >>> 0 & 255)
+               ].join('.') + '/' +
+               network.maskToPrefix(mask.join('.'));
+}
+
+return view.extend({
+       load: function() {
+               return Promise.all([
+                       network.getDevices(),
+                       uci.load('libreswan'),
+               ]);
+       },
+
+       render: function(data) {
+               var netDevs = data[0];
+               var m, s, o;
+               var proposals;
+
+               proposals = uci.sections('libreswan', 'crypto_proposal');
+               if (proposals == '') {
+                       ui.addNotification(null, E('p', _('Proposals must be configured for Tunnels')));
+                       return;
+               }
+
+               m = new form.Map('libreswan', 'IPSec Tunnels');
+
+               s = m.section(form.GridSection, 'tunnel');
+               s.anonymous = false;
+               s.addremove = true;
+               s.nodedescription = false;
+               s.addbtntitle = _('Add Tunnel');
+
+               o = s.tab('general', _('General'));
+               o = s.tab('authentication', _('Authentication'));
+               o = s.tab('interface', _('Interface'));
+               o = s.tab('advanced', _('Advanced'));
+
+               o = s.taboption('general', form.ListValue, 'auto', _('Mode'));
+               o.default = 'start';
+               o.value('add', _('Listen'));
+               o.value('start', _('Initiate'));
+
+               o = s.taboption('general', widgets.NetworkSelect, 'left_interface', _('Left Interface'));
+               o.datatype = 'string';
+               o.multiple = false;
+               o.optional = true;
+
+               o = s.taboption('general', form.Value, 'left', _('Left IP/Device'));
+               o.datatype = 'or(string, ipaddr)';
+               for (var i = 0; i < netDevs.length; i++) {
+                       var addrs = netDevs[i].getIPAddrs();
+                       for (var j = 0; j < addrs.length; j++) {
+                               o.value(addrs[j].split('/')[0]);
+                       }
+               }
+               for (var i = 0; i < netDevs.length; i++) {
+                       o.value('%' + netDevs[i].device);
+               }
+               o.value('%defaultroute');
+               o.optional = false;
+               o.depends({ 'left_interface' : '' });
+
+               o = s.taboption('general', form.Value, 'leftid', _('Left ID'));
+               o.datatype = 'string';
+               o.value('%any');
+               o.modalonly = true;
+
+               o = s.taboption('general', form.Value, 'right', _('Remote IP'));
+               o.datatype = 'or(string, ipaddr)';
+               o.value('0.0.0.0');
+               o.value('%any');
+               o.optional = false;
+
+               o = s.taboption('general', form.Value, 'rightid', _('Right ID'));
+               o.datatype = 'string';
+               o.value('%any');
+               o.modalonly = true;
+
+               o = s.taboption('general', form.Value, 'leftsourceip', _('Local Source IP'));
+               o.datatype = 'ipaddr';
+               for (var i = 0; i < netDevs.length; i++) {
+                       var addrs = netDevs[i].getIPAddrs();
+                       for (var j = 0; j < addrs.length; j++) {
+                               o.value(addrs[j].split('/')[0]);
+                       }
+               }
+               o.optional = false;
+               o.modalonly = true;
+
+               o = s.taboption('general', form.Value, 'rightsourceip', _('Remote Source IP'));
+               o.datatype = 'ipaddr';
+               o.optional = false;
+               o.modalonly = true;
+
+               o = s.taboption('general', form.DynamicList, 'leftsubnets', _('Local Subnets'));
+               o.datatype = 'ipaddr';
+               for (var i = 0; i < netDevs.length; i++) {
+                       var addrs = netDevs[i].getIPAddrs();
+                       for (var j = 0; j < addrs.length; j++) {
+                               var subnet = calculateNetwork(addrs[j].split('/')[0], addrs[j].split('/')[1]);
+                               if (subnet) {
+                                       o.value(subnet);
+                               }
+                       }
+               }
+               o.value('0.0.0.0/0');
+
+               o = s.taboption('general', form.DynamicList, 'rightsubnets', _('Remote Subnets'));
+               o.datatype = 'ipaddr';
+               o.value('0.0.0.0/0');
+
+               o = s.taboption('authentication', form.ListValue, 'authby', _('Auth Method'));
+               o.default = 'secret'
+               o.value('secret', _('Shared Secret'));
+               o.value('never', 'Never');
+               o.value('null', 'Null');
+               o.modalonly = true;
+               o.optional = false;
+
+               o = s.taboption('authentication', form.Value, 'psk', _('Preshared Key'));
+               o.datatype = 'and(string, minlength(8))'
+               o.depends({ 'authby' : 'secret' });
+               o.password = true;
+               o.modalonly = true;
+               o.optional = false;
+
+               o = s.taboption('advanced', form.ListValue, 'ikev2', _('IKE V2'));
+               o.default = 'yes';
+               o.value('yes', _('IKE Version 2'));
+               o.value('no', _('IKE Version 1'));
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.MultiValue, 'ike', _('Phase1 Proposals'));
+               for (var i = 0; i < proposals.length; i++) {
+                       o.value(proposals[i]['.name']);
+               }
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'ikelifetime', _('IKE Life Time'), _('Acceptable values are an integer followed by m, h, d'));
+               o.default = '8h';
+               o.value('1h', '1h');
+               o.value('2h', '2h');
+               o.value('4h', '4h');
+               o.value('8h', '8h');
+               o.value('12h', '12h');
+               o.value('16h', '16h');
+               o.value('24h', '24h');
+               o.modalonly = false;
+               o.modalonly = true;
+               o.validate = function(section_id, value) {
+                       if (!/^[0-9]{1,3}[smhd]$/.test(value)) {
+                               return _('Acceptable values are an integer followed by m, h, d');
+                       }
+                       return true;
+               }
+
+               o = s.taboption('advanced', form.Flag, 'rekey', _('Rekey'));
+               o.default = false;
+               o.modalonly = false;
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'rekeymargin', _('Rekey Margin Time'), _('Acceptable values are an integer followed by m, h, d'));
+               o.default = '9m';
+               o.value('5m', '5m');
+               o.value('9m', '9m');
+               o.value('15m', '15m');
+               o.value('20m', '20m');
+               o.value('30m', '30m');
+               o.value('60m', '60m');
+               o.modalonly = false;
+               o.modalonly = true;
+               o.validate = function(section_id, value) {
+                       if (!/^[0-9]{1,3}[smhd]$/.test(value)) {
+                               return _('Acceptable values are an integer followed by m, h, d');
+                       }
+                       return true;
+               }
+
+               o = s.taboption('advanced', form.ListValue, 'dpdaction', _('DPD Action'));
+               o.default = 'restart';
+               o.value('none', _('None'));
+               o.value('clear', _('Clear'));
+               o.value('hold', _('Hold'));
+               o.value('restart', _('Restart'));
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'dpddelay', _('DPD Delay'));
+               o.datatype = 'uinteger';
+               o.default = 30;
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'dpdtimeout', _('DPD Timeout'));
+               o.datatype = 'uinteger';
+               o.default = 150;
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.ListValue, 'phase2', _('Phase2 Protocol'));
+               o.default = 'esp';
+               o.value('esp', 'ESP');
+               o.modalonly = true;
+               o.optional = false;
+
+               o = s.taboption('advanced', form.MultiValue, 'phase2alg', _('Phase2 Proposals'));
+               for (var i = 0; i < proposals.length; i++) {
+                       o.value(proposals[i]['.name']);
+               }
+               o.modalonly = true;
+
+               o = s.taboption('advanced', form.Value, 'nflog', _('Enable nflog on nfgroup'));
+               o.default = 0;
+               o.datatype = 'uinteger';
+               o.rmempty = true;
+               o.optional = true;
+               o.modalonly = true;
+
+               var interfaces = uci.sections('network', 'interface');
+               o = s.taboption('advanced', form.ListValue, 'interface', _('Tunnel Interface'),
+                       _('Lists XFRM interfaces in format "ipsecN", N denotes ifid of xfrm interface') + '<br>' +
+                       _('Lists VTI interfaces configured with ikey and okey'));
+               o.datatype = 'string';
+               o.rmempty = true;
+               o.modalonly = true;
+               o.value('');
+               for (var i = 0; i < interfaces.length; i++) {
+                       if ((interfaces[i]['proto'] == "vti") && interfaces[i]['ikey'] && interfaces[i]['okey']) {
+                               o.value(interfaces[i]['.name'], 'VTI - ' + interfaces[i]['.name']);
+                       }
+
+                       if ((interfaces[i]['proto'] == "xfrm")
+                               && interfaces[i]['ifid']
+                               && interfaces[i]['.name'].match('ipsec' + interfaces[i]['ifid'])) {
+                               o.value(interfaces[i]['.name'], 'XFRM - ' + interfaces[i]['.name']);
+                       }
+               }
+
+               o = s.taboption('advanced', form.Flag, 'update_peeraddr', _('Update Peer Address'),
+                       _('Auto Update Peer Address of VTI interface'));
+               o.rmempty = true;
+               o.modalonly = true;
+
+               return m.render();
+       }
+});
diff --git a/applications/luci-app-libreswan/root/etc/uci-defaults/802-luci-app-libreswan b/applications/luci-app-libreswan/root/etc/uci-defaults/802-luci-app-libreswan
new file mode 100644 (file)
index 0000000..6383335
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/sh
+
+uci -q batch <<-EOF >/dev/null
+       delete ucitrack.@libreswan[-1]
+       add ucitrack libreswan
+       set ucitrack.@libreswan[-1].init=ipsec
+       commit ucitrack
+EOF
diff --git a/applications/luci-app-libreswan/root/usr/share/luci/menu.d/luci-app-libreswan.json b/applications/luci-app-libreswan/root/usr/share/luci/menu.d/luci-app-libreswan.json
new file mode 100644 (file)
index 0000000..a2974b5
--- /dev/null
@@ -0,0 +1,45 @@
+{
+       "admin/vpn/libreswan": {
+               "title": "Libreswan IPSec",
+               "order": 90,
+               "action": {
+                       "type": "firstchild"
+               }
+       },
+
+       "admin/vpn/libreswan/overview": {
+               "title": "Overview",
+               "order": 10,
+               "action": {
+                       "type": "view",
+                       "path": "libreswan/overview"
+               }
+       },
+
+       "admin/vpn/libreswan/globals": {
+               "title": "IPSec Globals",
+               "order": 20,
+               "action": {
+                       "type": "view",
+                       "path": "libreswan/globals"
+               }
+       },
+
+       "admin/vpn/libreswan/proposals": {
+               "title": "IPSec Proposals",
+               "order": 30,
+               "action": {
+                       "type": "view",
+                       "path": "libreswan/proposals"
+               }
+       },
+
+       "admin/vpn/libreswan/tunnels": {
+               "title": "IPSec Tunnels",
+               "order": 40,
+               "action": {
+                       "type": "view",
+                       "path": "libreswan/tunnels"
+               }
+       }
+}
diff --git a/applications/luci-app-libreswan/root/usr/share/rpcd/acl.d/luci-app-libreswan.json b/applications/luci-app-libreswan/root/usr/share/rpcd/acl.d/luci-app-libreswan.json
new file mode 100644 (file)
index 0000000..a846496
--- /dev/null
@@ -0,0 +1,14 @@
+{
+       "luci-app-libreswan" : {
+               "description" : "Grant access to LuCI app Libreswan IPSec",
+               "read" : {
+                       "ubus" : {
+                               "libreswan" : [ "*" ]
+                       },
+                       "uci": [ "libreswan" ]
+               },
+               "write" : {
+                       "uci": [ "libreswan" ]
+               }
+       }
+}