summaryrefslogtreecommitdiffhomepage
path: root/protocols/luci-proto-wireguard
diff options
context:
space:
mode:
Diffstat (limited to 'protocols/luci-proto-wireguard')
-rw-r--r--protocols/luci-proto-wireguard/Makefile4
-rw-r--r--protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js763
-rw-r--r--protocols/luci-proto-wireguard/htdocs/luci-static/resources/view/wireguard/status.js175
-rw-r--r--protocols/luci-proto-wireguard/root/usr/share/luci/menu.d/luci-proto-wireguard.json14
-rw-r--r--protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json25
-rw-r--r--protocols/luci-proto-wireguard/root/usr/share/rpcd/ucode/luci.wireguard107
6 files changed, 1063 insertions, 25 deletions
diff --git a/protocols/luci-proto-wireguard/Makefile b/protocols/luci-proto-wireguard/Makefile
index 3540a74acd..12137fb19b 100644
--- a/protocols/luci-proto-wireguard/Makefile
+++ b/protocols/luci-proto-wireguard/Makefile
@@ -7,9 +7,11 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=Support for WireGuard VPN
-LUCI_DEPENDS:=+kmod-wireguard +wireguard-tools
+LUCI_DEPENDS:=+wireguard-tools +ucode
LUCI_PKGARCH:=all
+PKG_PROVIDES:=luci-app-wireguard
+
include ../../luci.mk
# call BuildPackage - OpenWrt buildroot signature
diff --git a/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js b/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js
index e88c07c339..58537f38f8 100644
--- a/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js
+++ b/protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js
@@ -1,7 +1,33 @@
'use strict';
+'require fs';
+'require ui';
+'require dom';
'require uci';
+'require rpc';
'require form';
'require network';
+'require validation';
+
+var generateKey = rpc.declare({
+ object: 'luci.wireguard',
+ method: 'generateKeyPair',
+ expect: { keys: {} }
+});
+
+var getPublicAndPrivateKeyFromPrivate = rpc.declare({
+ object: 'luci.wireguard',
+ method: 'getPublicAndPrivateKeyFromPrivate',
+ params: ['privkey'],
+ expect: { keys: {} }
+});
+
+var generatePsk = rpc.declare({
+ object: 'luci.wireguard',
+ method: 'generatePsk',
+ expect: { psk: '' }
+});
+
+var qrIcon = '<svg viewBox="0 0 29 29" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M0 0h29v29H0z"/><path d="M4 4h1v1H4zM5 4h1v1H5zM6 4h1v1H6zM7 4h1v1H7zM8 4h1v1H8zM9 4h1v1H9zM10 4h1v1h-1zM12 4h1v1h-1zM13 4h1v1h-1zM14 4h1v1h-1zM15 4h1v1h-1zM16 4h1v1h-1zM18 4h1v1h-1zM19 4h1v1h-1zM20 4h1v1h-1zM21 4h1v1h-1zM22 4h1v1h-1zM23 4h1v1h-1zM24 4h1v1h-1zM4 5h1v1H4zM10 5h1v1h-1zM12 5h1v1h-1zM14 5h1v1h-1zM16 5h1v1h-1zM18 5h1v1h-1zM24 5h1v1h-1zM4 6h1v1H4zM6 6h1v1H6zM7 6h1v1H7zM8 6h1v1H8zM10 6h1v1h-1zM12 6h1v1h-1zM18 6h1v1h-1zM20 6h1v1h-1zM21 6h1v1h-1zM22 6h1v1h-1zM24 6h1v1h-1zM4 7h1v1H4zM6 7h1v1H6zM7 7h1v1H7zM8 7h1v1H8zM10 7h1v1h-1zM12 7h1v1h-1zM13 7h1v1h-1zM14 7h1v1h-1zM15 7h1v1h-1zM18 7h1v1h-1zM20 7h1v1h-1zM21 7h1v1h-1zM22 7h1v1h-1zM24 7h1v1h-1zM4 8h1v1H4zM6 8h1v1H6zM7 8h1v1H7zM8 8h1v1H8zM10 8h1v1h-1zM16 8h1v1h-1zM18 8h1v1h-1zM20 8h1v1h-1zM21 8h1v1h-1zM22 8h1v1h-1zM24 8h1v1h-1zM4 9h1v1H4zM10 9h1v1h-1zM12 9h1v1h-1zM13 9h1v1h-1zM15 9h1v1h-1zM18 9h1v1h-1zM24 9h1v1h-1zM4 10h1v1H4zM5 10h1v1H5zM6 10h1v1H6zM7 10h1v1H7zM8 10h1v1H8zM9 10h1v1H9zM10 10h1v1h-1zM12 10h1v1h-1zM14 10h1v1h-1zM16 10h1v1h-1zM18 10h1v1h-1zM19 10h1v1h-1zM20 10h1v1h-1zM21 10h1v1h-1zM22 10h1v1h-1zM23 10h1v1h-1zM24 10h1v1h-1zM13 11h1v1h-1zM14 11h1v1h-1zM15 11h1v1h-1zM16 11h1v1h-1zM4 12h1v1H4zM5 12h1v1H5zM8 12h1v1H8zM9 12h1v1H9zM10 12h1v1h-1zM13 12h1v1h-1zM15 12h1v1h-1zM19 12h1v1h-1zM21 12h1v1h-1zM22 12h1v1h-1zM23 12h1v1h-1zM24 12h1v1h-1zM5 13h1v1H5zM6 13h1v1H6zM8 13h1v1H8zM11 13h1v1h-1zM13 13h1v1h-1zM14 13h1v1h-1zM15 13h1v1h-1zM16 13h1v1h-1zM19 13h1v1h-1zM22 13h1v1h-1zM4 14h1v1H4zM5 14h1v1H5zM9 14h1v1H9zM10 14h1v1h-1zM11 14h1v1h-1zM15 14h1v1h-1zM18 14h1v1h-1zM19 14h1v1h-1zM20 14h1v1h-1zM21 14h1v1h-1zM22 14h1v1h-1zM23 14h1v1h-1zM7 15h1v1H7zM8 15h1v1H8zM9 15h1v1H9zM11 15h1v1h-1zM12 15h1v1h-1zM13 15h1v1h-1zM17 15h1v1h-1zM18 15h1v1h-1zM20 15h1v1h-1zM21 15h1v1h-1zM23 15h1v1h-1zM4 16h1v1H4zM6 16h1v1H6zM10 16h1v1h-1zM11 16h1v1h-1zM13 16h1v1h-1zM14 16h1v1h-1zM16 16h1v1h-1zM17 16h1v1h-1zM18 16h1v1h-1zM22 16h1v1h-1zM23 16h1v1h-1zM24 16h1v1h-1zM12 17h1v1h-1zM16 17h1v1h-1zM17 17h1v1h-1zM18 17h1v1h-1zM4 18h1v1H4zM5 18h1v1H5zM6 18h1v1H6zM7 18h1v1H7zM8 18h1v1H8zM9 18h1v1H9zM10 18h1v1h-1zM14 18h1v1h-1zM16 18h1v1h-1zM17 18h1v1h-1zM21 18h1v1h-1zM22 18h1v1h-1zM23 18h1v1h-1zM4 19h1v1H4zM10 19h1v1h-1zM12 19h1v1h-1zM13 19h1v1h-1zM15 19h1v1h-1zM16 19h1v1h-1zM19 19h1v1h-1zM21 19h1v1h-1zM23 19h1v1h-1zM24 19h1v1h-1zM4 20h1v1H4zM6 20h1v1H6zM7 20h1v1H7zM8 20h1v1H8zM10 20h1v1h-1zM12 20h1v1h-1zM13 20h1v1h-1zM15 20h1v1h-1zM18 20h1v1h-1zM19 20h1v1h-1zM20 20h1v1h-1zM22 20h1v1h-1zM23 20h1v1h-1zM24 20h1v1h-1zM4 21h1v1H4zM6 21h1v1H6zM7 21h1v1H7zM8 21h1v1H8zM10 21h1v1h-1zM13 21h1v1h-1zM15 21h1v1h-1zM16 21h1v1h-1zM19 21h1v1h-1zM21 21h1v1h-1zM23 21h1v1h-1zM24 21h1v1h-1zM4 22h1v1H4zM6 22h1v1H6zM7 22h1v1H7zM8 22h1v1H8zM10 22h1v1h-1zM13 22h1v1h-1zM15 22h1v1h-1zM18 22h1v1h-1zM19 22h1v1h-1zM20 22h1v1h-1zM21 22h1v1h-1zM22 22h1v1h-1zM4 23h1v1H4zM10 23h1v1h-1zM12 23h1v1h-1zM13 23h1v1h-1zM14 23h1v1h-1zM17 23h1v1h-1zM18 23h1v1h-1zM20 23h1v1h-1zM22 23h1v1h-1zM4 24h1v1H4zM5 24h1v1H5zM6 24h1v1H6zM7 24h1v1H7zM8 24h1v1H8zM9 24h1v1H9zM10 24h1v1h-1zM12 24h1v1h-1zM13 24h1v1h-1zM14 24h1v1h-1zM16 24h1v1h-1zM17 24h1v1h-1zM18 24h1v1h-1zM22 24h1v1h-1zM24 24h1v1h-1z"/></svg>';
function validateBase64(section_id, value) {
if (value.length == 0)
@@ -16,6 +42,78 @@ function validateBase64(section_id, value) {
return true;
}
+var stubValidator = {
+ factory: validation,
+ apply: function(type, value, args) {
+ if (value != null)
+ this.value = value;
+
+ return validation.types[type].apply(this, args);
+ },
+ assert: function(condition) {
+ return !!condition;
+ }
+};
+
+function generateDescription(name, texts) {
+ return E('li', { 'style': 'color: inherit;' }, [
+ E('span', name),
+ E('ul', texts.map(function (text) {
+ return E('li', { 'style': 'color: inherit;' }, text);
+ }))
+ ]);
+}
+
+function invokeQREncode(data, code) {
+ return fs.exec_direct('/usr/bin/qrencode', [
+ '--inline', '--8bit', '--type=SVG',
+ '--output=-', '--', data
+ ]).then(function(svg) {
+ code.style.opacity = '';
+ dom.content(code, Object.assign(E(svg), { style: 'width:100%;height:auto' }));
+ }).catch(function(error) {
+ code.style.opacity = '';
+
+ if (L.isObject(error) && error.name == 'NotFoundError') {
+ dom.content(code, [
+ Object.assign(E(qrIcon), { style: 'width:32px;height:32px;opacity:.2' }),
+ E('p', _('The <em>qrencode</em> package is required for generating an QR code image of the configuration.'))
+ ]);
+ }
+ else {
+ dom.content(code, [
+ _('Unable to generate QR code: %s').format(L.isObject(error) ? error.message : error)
+ ]);
+ }
+ });
+}
+
+var cbiKeyPairGenerate = form.DummyValue.extend({
+ cfgvalue: function(section_id, value) {
+ return E('button', {
+ 'class': 'btn',
+ 'click': ui.createHandlerFn(this, function(section_id, ev) {
+ var prv = this.section.getUIElement(section_id, 'private_key'),
+ pub = this.section.getUIElement(section_id, 'public_key'),
+ map = this.map;
+
+ if ((prv.getValue() || pub.getValue()) && !confirm(_('Do you want to replace the current keys?')))
+ return;
+
+ return generateKey().then(function(keypair) {
+ prv.setValue(keypair.priv);
+ pub.setValue(keypair.pub);
+ map.save(null, true);
+ });
+ }, section_id)
+ }, [ _('Generate new key pair') ]);
+ }
+});
+
+function handleWindowDragDropIgnore(ev) {
+ ev.preventDefault()
+}
+
return network.registerProtocol('wireguard', {
getI18n: function() {
return _('WireGuard VPN');
@@ -46,7 +144,7 @@ return network.registerProtocol('wireguard', {
},
renderFormOptions: function(s) {
- var o, ss;
+ var o, ss, ss2;
// -- general ---------------------------------------------------------------------
@@ -55,6 +153,26 @@ return network.registerProtocol('wireguard', {
o.validate = validateBase64;
o.rmempty = false;
+ var serverName = this.getIfname();
+
+ o = s.taboption('general', form.Value, 'public_key', _('Public Key'), _('Base64-encoded public key of this interface for sharing.'));
+ o.rmempty = false;
+ o.write = function() {/* write nothing */};
+
+ o.load = function(section_id) {
+ var privKey = s.formvalue(section_id, 'private_key') || uci.get('network', section_id, 'private_key');
+
+ return getPublicAndPrivateKeyFromPrivate(privKey).then(
+ function(keypair) {
+ return keypair.pub || '';
+ },
+ function(error) {
+ return _('Error getting PublicKey');
+ }, this)
+ };
+
+ s.taboption('general', cbiKeyPairGenerate, '_gen_server_keypair', ' ');
+
o = s.taboption('general', form.Value, 'listen_port', _('Listen Port'), _('Optional. UDP port used for outgoing and incoming packets.'));
o.datatype = 'port';
o.placeholder = _('random');
@@ -67,22 +185,23 @@ return network.registerProtocol('wireguard', {
o = s.taboption('general', form.Flag, 'nohostroute', _('No Host Routes'), _('Optional. Do not create host routes to peers.'));
o.optional = true;
- // -- advanced --------------------------------------------------------------------
+ o = s.taboption('general', form.Button, '_import', _('Import configuration'), _('Imports settings from an existing WireGuard configuration file'));
+ o.inputtitle = _('Load configuration…');
+ o.onclick = function() {
+ return ss.handleConfigImport('full');
+ };
- o = s.taboption('advanced', form.Value, 'metric', _('Metric'), _('Optional'));
- o.datatype = 'uinteger';
- o.placeholder = '0';
- o.optional = true;
+ // -- advanced --------------------------------------------------------------------
o = s.taboption('advanced', form.Value, 'mtu', _('MTU'), _('Optional. Maximum Transmission Unit of tunnel interface.'));
- o.datatype = 'range(1280,1420)';
+ o.datatype = 'range(0,8940)';
o.placeholder = '1420';
o.optional = true;
o = s.taboption('advanced', form.Value, 'fwmark', _('Firewall Mark'), _('Optional. 32-bit mark for outgoing encrypted packets. Enter value in hex, starting with <code>0x</code>.'));
o.optional = true;
o.validate = function(section_id, value) {
- if (value.length > 0 && !value.match(/^0x[a-fA-F0-9]{1,4}$/))
+ if (value.length > 0 && !value.match(/^0x[a-fA-F0-9]{1,8}$/))
return _('Invalid hexadecimal value');
return true;
@@ -96,59 +215,655 @@ return network.registerProtocol('wireguard', {
}
catch(e) {}
- o = s.taboption('peers', form.SectionValue, '_peers', form.TypedSection, 'wireguard_%s'.format(s.section));
+ o = s.taboption('peers', form.SectionValue, '_peers', form.GridSection, 'wireguard_%s'.format(s.section));
o.depends('proto', 'wireguard');
ss = o.subsection;
ss.anonymous = true;
ss.addremove = true;
ss.addbtntitle = _('Add peer');
+ ss.nodescriptions = true;
+ ss.modaltitle = _('Edit peer');
- ss.renderSectionPlaceholder = function() {
- return E([], [
- E('br'),
- E('em', _('No peers defined yet'))
+ ss.handleDragConfig = function(ev) {
+ ev.stopPropagation();
+ ev.preventDefault();
+ ev.dataTransfer.dropEffect = 'copy';
+ };
+
+ ss.handleDropConfig = function(mode, ev) {
+ var file = ev.dataTransfer.files[0],
+ nodes = ev.currentTarget,
+ input = nodes.querySelector('textarea'),
+ reader = new FileReader();
+
+ if (file) {
+ reader.onload = function(rev) {
+ input.value = rev.target.result.trim();
+ ss.handleApplyConfig(mode, nodes, file.name, ev);
+ };
+
+ reader.readAsText(file);
+ }
+
+ ev.stopPropagation();
+ ev.preventDefault();
+ };
+
+ ss.parseConfig = function(data) {
+ var lines = String(data).split(/(\r?\n)+/),
+ section = null,
+ config = { peers: [] },
+ s;
+
+ for (var i = 0; i < lines.length; i++) {
+ var line = lines[i].replace(/#.*$/, '').trim();
+
+ if (line.match(/^\[(\w+)\]$/)) {
+ section = RegExp.$1.toLowerCase();
+
+ if (section == 'peer')
+ config.peers.push(s = {});
+ else
+ s = config;
+ }
+ else if (section && line.match(/^(\w+)\s*=\s*(.+)$/)) {
+ var key = RegExp.$1,
+ val = RegExp.$2.trim();
+
+ if (val.length)
+ s[section + '_' + key.toLowerCase()] = val;
+ }
+ }
+
+ if (config.interface_address) {
+ config.interface_address = config.interface_address.split(/[, ]+/);
+
+ for (var i = 0; i < config.interface_address.length; i++)
+ if (!stubValidator.apply('ipaddr', config.interface_address[i]))
+ return _('Address setting is invalid');
+ }
+
+ if (config.interface_dns) {
+ config.interface_dns = config.interface_dns.split(/[, ]+/);
+
+ for (var i = 0; i < config.interface_dns.length; i++)
+ if (!stubValidator.apply('ipaddr', config.interface_dns[i], ['nomask']))
+ return _('DNS setting is invalid');
+ }
+
+ if (!config.interface_privatekey || validateBase64(null, config.interface_privatekey) !== true)
+ return _('PrivateKey setting is missing or invalid');
+
+ if (!stubValidator.apply('port', config.interface_listenport || '0'))
+ return _('ListenPort setting is invalid');
+
+ for (var i = 0; i < config.peers.length; i++) {
+ var pconf = config.peers[i];
+
+ if (pconf.peer_publickey != null && validateBase64(null, pconf.peer_publickey) !== true)
+ return _('PublicKey setting is invalid');
+
+ if (pconf.peer_presharedkey != null && validateBase64(null, pconf.peer_presharedkey) !== true)
+ return _('PresharedKey setting is invalid');
+
+ if (pconf.peer_allowedips) {
+ pconf.peer_allowedips = pconf.peer_allowedips.split(/[, ]+/);
+
+ for (var j = 0; j < pconf.peer_allowedips.length; j++)
+ if (!stubValidator.apply('ipaddr', pconf.peer_allowedips[j]))
+ return _('AllowedIPs setting is invalid');
+ }
+ else {
+ pconf.peer_allowedips = [ '0.0.0.0/0', '::/0' ];
+ }
+
+ if (pconf.peer_endpoint) {
+ var host_port = pconf.peer_endpoint.match(/^\[([a-fA-F0-9:]+)\]:(\d+)$/) || pconf.peer_endpoint.match(/^(.+):(\d+)$/);
+
+ if (!host_port || !stubValidator.apply('host', host_port[1]) || !stubValidator.apply('port', host_port[2]))
+ return _('Endpoint setting is invalid');
+
+ pconf.peer_endpoint = [ host_port[1], host_port[2] ];
+ }
+
+ if (pconf.peer_persistentkeepalive == 'off' || pconf.peer_persistentkeepalive == '0')
+ delete pconf.peer_persistentkeepalive;
+
+ if (!stubValidator.apply('port', pconf.peer_persistentkeepalive || '0'))
+ return _('PersistentKeepAlive setting is invalid');
+ }
+
+ return config;
+ };
+
+ ss.handleApplyConfig = function(mode, nodes, comment, ev) {
+ var input = nodes.querySelector('textarea').value,
+ error = nodes.querySelector('.alert-message'),
+ cancel = nodes.nextElementSibling.querySelector('.btn'),
+ config = this.parseConfig(input);
+
+ if (typeof(config) == 'string') {
+ error.firstChild.data = _('Cannot parse configuration: %s').format(config);
+ error.style.display = 'block';
+ return;
+ }
+
+ if (mode == 'full') {
+ var prv = s.formvalue(s.section, 'private_key');
+
+ if (prv && prv != config.interface_privatekey && !confirm(_('Overwrite the current settings with the imported configuration?')))
+ return;
+
+ return getPublicAndPrivateKeyFromPrivate(config.interface_privatekey).then(function(keypair) {
+ s.getOption('private_key').getUIElement(s.section).setValue(keypair.priv);
+ s.getOption('public_key').getUIElement(s.section).setValue(keypair.pub);
+ s.getOption('listen_port').getUIElement(s.section).setValue(config.interface_listenport || '');
+ s.getOption('addresses').getUIElement(s.section).setValue(config.interface_address);
+
+ if (config.interface_dns)
+ s.getOption('dns').getUIElement(s.section).setValue(config.interface_dns);
+
+ for (var i = 0; i < config.peers.length; i++) {
+ var pconf = config.peers[i];
+ var sid = uci.add('network', 'wireguard_' + s.section);
+
+ uci.sections('network', 'wireguard_' + s.section, function(peer) {
+ if (peer.public_key == pconf.peer_publickey)
+ uci.remove('network', peer['.name']);
+ });
+
+ uci.set('network', sid, 'description', comment || _('Imported peer configuration'));
+ uci.set('network', sid, 'public_key', pconf.peer_publickey);
+ uci.set('network', sid, 'preshared_key', pconf.peer_presharedkey);
+ uci.set('network', sid, 'allowed_ips', pconf.peer_allowedips);
+ uci.set('network', sid, 'persistent_keepalive', pconf.peer_persistentkeepalive);
+
+ if (pconf.peer_endpoint) {
+ uci.set('network', sid, 'endpoint_host', pconf.peer_endpoint[0]);
+ uci.set('network', sid, 'endpoint_port', pconf.peer_endpoint[1]);
+ }
+ }
+
+ return s.map.save(null, true);
+ }).then(function() {
+ cancel.click();
+ });
+ }
+ else {
+ return getPublicAndPrivateKeyFromPrivate(config.interface_privatekey).then(function(keypair) {
+ var sid = uci.add('network', 'wireguard_' + s.section);
+ var pub = s.formvalue(s.section, 'public_key');
+
+ uci.sections('network', 'wireguard_' + s.section, function(peer) {
+ if (peer.public_key == keypair.pub)
+ uci.remove('network', peer['.name']);
+ });
+
+ uci.set('network', sid, 'description', comment || _('Imported peer configuration'));
+ uci.set('network', sid, 'public_key', keypair.pub);
+ uci.set('network', sid, 'private_key', keypair.priv);
+
+ for (var i = 0; i < config.peers.length; i++) {
+ var pconf = config.peers[i];
+
+ if (pconf.peer_publickey == pub) {
+ uci.set('network', sid, 'preshared_key', pconf.peer_presharedkey);
+ uci.set('network', sid, 'allowed_ips', pconf.peer_allowedips);
+ uci.set('network', sid, 'persistent_keepalive', pconf.peer_persistentkeepalive);
+ break;
+ }
+ }
+
+ return s.map.save(null, true);
+ }).then(function() {
+ cancel.click();
+ });
+ }
+ };
+
+ ss.handleConfigImport = function(mode) {
+ var mapNode = ss.getActiveModalMap(),
+ headNode = mapNode.parentNode.querySelector('h4'),
+ parent = this.map;
+
+ var nodes = E('div', {
+ 'dragover': this.handleDragConfig,
+ 'drop': this.handleDropConfig.bind(this, mode)
+ }, [
+ E([], (mode == 'full') ? [
+ E('p', _('Drag or paste a valid <em>*.conf</em> file below to configure the local WireGuard interface.'))
+ ] : [
+ E('p', _('Paste or drag a WireGuard configuration (commonly <em>wg0.conf</em>) from another system below to create a matching peer entry allowing that system to connect to the local WireGuard interface.')),
+ E('p', _('To fully configure the local WireGuard interface from an existing (e.g. provider supplied) configuration file, use the <strong><a class="full-import" href="#">configuration import</a></strong> instead.'))
+ ]),
+ E('p', [
+ E('textarea', {
+ 'placeholder': (mode == 'full')
+ ? _('Paste or drag supplied WireGuard configuration file…')
+ : _('Paste or drag WireGuard peer configuration (wg0.conf) file…'),
+ 'style': 'height:5em;width:100%; white-space:pre'
+ })
+ ]),
+ E('div', {
+ 'class': 'alert-message',
+ 'style': 'display:none'
+ }, [''])
]);
+
+ var cancelFn = function() {
+ nodes.parentNode.removeChild(nodes.nextSibling);
+ nodes.parentNode.removeChild(nodes);
+ mapNode.classList.remove('hidden');
+ mapNode.nextSibling.classList.remove('hidden');
+ headNode.removeChild(headNode.lastChild);
+ window.removeEventListener('dragover', handleWindowDragDropIgnore);
+ window.removeEventListener('drop', handleWindowDragDropIgnore);
+ };
+
+ var a = nodes.querySelector('a.full-import');
+
+ if (a) {
+ a.addEventListener('click', ui.createHandlerFn(this, function(mode) {
+ cancelFn();
+ this.handleConfigImport('full');
+ }));
+ }
+
+ mapNode.classList.add('hidden');
+ mapNode.nextElementSibling.classList.add('hidden');
+
+ headNode.appendChild(E('span', [ ' » ', (mode == 'full') ? _('Import configuration') : _('Import as peer') ]));
+ mapNode.parentNode.appendChild(E([], [
+ nodes,
+ E('div', {
+ 'class': 'right'
+ }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': cancelFn
+ }, [ _('Cancel') ]),
+ ' ',
+ E('button', {
+ 'class': 'btn primary',
+ 'click': ui.createHandlerFn(this, 'handleApplyConfig', mode, nodes, null)
+ }, [ _('Import settings') ])
+ ])
+ ]));
+
+ window.addEventListener('dragover', handleWindowDragDropIgnore);
+ window.addEventListener('drop', handleWindowDragDropIgnore);
};
+ ss.renderSectionAdd = function(/* ... */) {
+ var nodes = this.super('renderSectionAdd', arguments);
+
+ nodes.appendChild(E('button', {
+ 'class': 'btn',
+ 'click': ui.createHandlerFn(this, 'handleConfigImport', 'peer')
+ }, [ _('Import configuration as peer…') ]));
+
+ return nodes;
+ };
+
+ ss.renderSectionPlaceholder = function() {
+ return E('em', _('No peers defined yet.'));
+ };
+
+ o = ss.option(form.Flag, 'disabled', _('Peer disabled'), _('Enable / Disable peer. Restart wireguard interface to apply changes.'));
+ o.modalonly = true;
+ o.optional = true;
+
o = ss.option(form.Value, 'description', _('Description'), _('Optional. Description of peer.'));
o.placeholder = 'My Peer';
o.datatype = 'string';
o.optional = true;
+ o.width = '30%';
+ o.textvalue = function(section_id) {
+ var dis = ss.getOption('disabled'),
+ pub = ss.getOption('public_key'),
+ prv = ss.getOption('private_key'),
+ psk = ss.getOption('preshared_key'),
+ name = this.cfgvalue(section_id),
+ key = pub.cfgvalue(section_id);
+
+ var desc = [
+ E('p', [
+ name ? E('span', [ name ]) : E('em', [ _('Untitled peer') ])
+ ])
+ ];
+
+ if (dis.cfgvalue(section_id) == '1')
+ desc.push(E('span', {
+ 'class': 'ifacebadge',
+ 'data-tooltip': _('WireGuard peer is disabled')
+ }, [
+ E('em', [ _('Disabled', 'Label indicating that WireGuard peer is disabled') ])
+ ]), ' ');
+
+ if (!key || !pub.isValid(section_id)) {
+ desc.push(E('span', {
+ 'class': 'ifacebadge',
+ 'data-tooltip': _('Public key is missing')
+ }, [
+ E('em', [ _('Key missing', 'Label indicating that WireGuard peer lacks public key') ])
+ ]));
+ }
+ else {
+ desc.push(
+ E('span', {
+ 'class': 'ifacebadge',
+ 'data-tooltip': _('Public key: %h', 'Tooltip displaying full WireGuard peer public key').format(key)
+ }, [
+ E('code', [ key.replace(/^(.{5}).+(.{6})$/, '$1…$2') ])
+ ]),
+ ' ',
+ (prv.cfgvalue(section_id) && prv.isValid(section_id))
+ ? E('span', {
+ 'class': 'ifacebadge',
+ 'data-tooltip': _('Private key present')
+ }, [ _('Private', 'Label indicating that WireGuard peer private key is stored') ]) : '',
+ ' ',
+ (psk.cfgvalue(section_id) && psk.isValid(section_id))
+ ? E('span', {
+ 'class': 'ifacebadge',
+ 'data-tooltip': _('Preshared key in use')
+ }, [ _('PSK', 'Label indicating that WireGuard peer uses a PSK') ]) : ''
+ );
+ }
- o = ss.option(form.Value, 'public_key', _('Public Key'), _('Required. Base64-encoded public key of peer.'));
+ return E([], desc);
+ };
+
+ function handleKeyChange(ev, section_id, value) {
+ var prv = this.section.getUIElement(section_id, 'private_key'),
+ btn = this.map.findElement('.btn.qr-code');
+
+ btn.disabled = (!prv.isValid() || !prv.getValue());
+ }
+
+ o = ss.option(form.Value, 'public_key', _('Public Key'), _('Required. Public key of the WireGuard peer.'));
+ o.modalonly = true;
o.validate = validateBase64;
- o.rmempty = false;
+ o.onchange = handleKeyChange;
- o = ss.option(form.Value, 'preshared_key', _('Preshared Key'), _('Optional. Base64-encoded preshared key. Adds in an additional layer of symmetric-key cryptography for post-quantum resistance.'));
+ o = ss.option(form.Value, 'private_key', _('Private Key'), _('Optional. Private key of the WireGuard peer. The key is not required for establishing a connection but allows generating a peer configuration or QR code if available. It can be removed after the configuration has been exported.'));
+ o.modalonly = true;
+ o.validate = validateBase64;
+ o.onchange = handleKeyChange;
o.password = true;
+
+ o = ss.option(cbiKeyPairGenerate, '_gen_peer_keypair', ' ');
+ o.modalonly = true;
+
+ o = ss.option(form.Value, 'preshared_key', _('Preshared Key'), _('Optional. Base64-encoded preshared key. Adds in an additional layer of symmetric-key cryptography for post-quantum resistance.'));
+ o.modalonly = true;
o.validate = validateBase64;
- o.optional = true;
+ o.password = true;
+
+ o = ss.option(form.DummyValue, '_gen_psk', ' ');
+ o.modalonly = true;
+ o.cfgvalue = function(section_id, value) {
+ return E('button', {
+ 'class': 'btn',
+ 'click': ui.createHandlerFn(this, function(section_id, ev) {
+ var psk = this.section.getUIElement(section_id, 'preshared_key'),
+ map = this.map;
+
+ if (psk.getValue() && !confirm(_('Do you want to replace the current PSK?')))
+ return;
+
+ return generatePsk().then(function(key) {
+ psk.setValue(key);
+ map.save(null, true);
+ });
+ }, section_id)
+ }, [ _('Generate preshared key') ]);
+ };
- o = ss.option(form.DynamicList, 'allowed_ips', _('Allowed IPs'), _("Required. IP addresses and prefixes that this peer is allowed to use inside the tunnel. Usually the peer's tunnel IP addresses and the networks the peer routes through the tunnel."));
+ o = ss.option(form.DynamicList, 'allowed_ips', _('Allowed IPs'), _("Optional. IP addresses and prefixes that this peer is allowed to use inside the tunnel. Usually the peer's tunnel IP addresses and the networks the peer routes through the tunnel."));
o.datatype = 'ipaddr';
- o.validate = function(section, value) {
- var opt = this.map.lookupOption('allowed_ips', section);
- var ips = opt[0].formvalue(section);
- if (ips.length == 0) {
- return _('Value must not be empty');
+ o.textvalue = function(section_id) {
+ var ips = L.toArray(this.cfgvalue(section_id)),
+ list = [];
+
+ for (var i = 0; i < ips.length; i++) {
+ if (i > 7) {
+ list.push(E('em', {
+ 'class': 'ifacebadge cbi-tooltip-container'
+ }, [
+ _('+ %d more', 'Label indicating further amount of allowed ips').format(ips.length - i),
+ E('span', {
+ 'class': 'cbi-tooltip'
+ }, [
+ E('ul', ips.map(function(ip) {
+ return E('li', [
+ E('span', { 'class': 'ifacebadge' }, [ ip ])
+ ]);
+ }))
+ ])
+ ]));
+
+ break;
+ }
+
+ list.push(E('span', { 'class': 'ifacebadge' }, [ ips[i] ]));
}
- return true;
+
+ if (!list.length)
+ list.push('*');
+
+ return E('span', { 'style': 'display:inline-flex;flex-wrap:wrap;gap:.125em' }, list);
};
o = ss.option(form.Flag, 'route_allowed_ips', _('Route Allowed IPs'), _('Optional. Create routes for Allowed IPs for this peer.'));
+ o.modalonly = true;
o = ss.option(form.Value, 'endpoint_host', _('Endpoint Host'), _('Optional. Host of peer. Names are resolved prior to bringing up the interface.'));
o.placeholder = 'vpn.example.com';
o.datatype = 'host';
+ o.textvalue = function(section_id) {
+ var host = this.cfgvalue(section_id),
+ port = this.section.cfgvalue(section_id, 'endpoint_port');
+
+ return (host && port)
+ ? '%h:%d'.format(host, port)
+ : (host
+ ? '%h:*'.format(host)
+ : (port
+ ? '*:%d'.format(port)
+ : '*'));
+ };
o = ss.option(form.Value, 'endpoint_port', _('Endpoint Port'), _('Optional. Port of peer.'));
+ o.modalonly = true;
o.placeholder = '51820';
o.datatype = 'port';
o = ss.option(form.Value, 'persistent_keepalive', _('Persistent Keep Alive'), _('Optional. Seconds between keep alive messages. Default is 0 (disabled). Recommended value if this device is behind a NAT is 25.'));
+ o.modalonly = true;
o.datatype = 'range(0,65535)';
o.placeholder = '0';
+
+
+
+ o = ss.option(form.DummyValue, '_keyops', _('Configuration Export'),
+ _('Generates a configuration suitable for import on a WireGuard peer'));
+
+ o.modalonly = true;
+
+ o.createPeerConfig = function(section_id, endpoint, ips) {
+ var pub = s.formvalue(s.section, 'public_key'),
+ port = s.formvalue(s.section, 'listen_port') || '51820',
+ prv = this.section.formvalue(section_id, 'private_key'),
+ psk = this.section.formvalue(section_id, 'preshared_key'),
+ eport = this.section.formvalue(section_id, 'endpoint_port'),
+ keep = this.section.formvalue(section_id, 'persistent_keepalive');
+
+ // If endpoint is IPv6 we must escape it with []
+ if (endpoint.indexOf(':') > 0) {
+ endpoint = '['+endpoint+']';
+ }
+
+ return [
+ '[Interface]',
+ 'PrivateKey = ' + prv,
+ eport ? 'ListenPort = ' + eport : '# ListenPort not defined',
+ '',
+ '[Peer]',
+ 'PublicKey = ' + pub,
+ psk ? 'PresharedKey = ' + psk : '# PresharedKey not used',
+ ips && ips.length ? 'AllowedIPs = ' + ips.join(', ') : '# AllowedIPs not defined',
+ endpoint ? 'Endpoint = ' + endpoint + ':' + port : '# Endpoint not defined',
+ keep ? 'PersistentKeepAlive = ' + keep : '# PersistentKeepAlive not defined'
+ ].join('\n');
+ };
+
+ o.handleGenerateQR = function(section_id, ev) {
+ var mapNode = ss.getActiveModalMap(),
+ headNode = mapNode.parentNode.querySelector('h4'),
+ configGenerator = this.createPeerConfig.bind(this, section_id),
+ parent = this.map;
+
+ return Promise.all([
+ network.getWANNetworks(),
+ network.getWAN6Networks(),
+ L.resolveDefault(uci.load('ddns')),
+ L.resolveDefault(uci.load('system')),
+ parent.save(null, true)
+ ]).then(function(data) {
+ var hostnames = [];
+
+ uci.sections('ddns', 'service', function(s) {
+ if (typeof(s.lookup_host) == 'string' && s.enabled == '1')
+ hostnames.push(s.lookup_host);
+ });
+
+ uci.sections('system', 'system', function(s) {
+ if (typeof(s.hostname) == 'string' && s.hostname.indexOf('.') > 0)
+ hostnames.push(s.hostname);
+ });
+
+ for (var i = 0; i < data[0].length; i++)
+ hostnames.push.apply(hostnames, data[0][i].getIPAddrs().map(function(ip) { return ip.split('/')[0] }));
+
+ for (var i = 0; i < data[1].length; i++)
+ hostnames.push.apply(hostnames, data[1][i].getIP6Addrs().map(function(ip) { return ip.split('/')[0] }));
+
+ var ips = [ '0.0.0.0/0', '::/0' ];
+
+ var qrm, qrs, qro;
+
+ qrm = new form.JSONMap({ config: { endpoint: hostnames[0], allowed_ips: ips } }, null, _('The generated configuration can be imported into a WireGuard client application to set up a connection towards this device.'));
+ qrm.parent = parent;
+
+ qrs = qrm.section(form.NamedSection, 'config');
+
+ function handleConfigChange(ev, section_id, value) {
+ var code = this.map.findElement('.qr-code'),
+ conf = this.map.findElement('.client-config'),
+ endpoint = this.section.getUIElement(section_id, 'endpoint'),
+ ips = this.section.getUIElement(section_id, 'allowed_ips');
+
+ if (this.isValid(section_id)) {
+ conf.firstChild.data = configGenerator(endpoint.getValue(), ips.getValue());
+ code.style.opacity = '.5';
+
+ invokeQREncode(conf.firstChild.data, code);
+ }
+ };
+
+ qro = qrs.option(form.Value, 'endpoint', _('Connection endpoint'), _('The public hostname or IP address of this system the peer should connect to. This usually is a static public IP address, a static hostname or a DDNS domain.'));
+ qro.datatype = 'or(ipaddr,hostname)';
+ hostnames.forEach(function(hostname) { qro.value(hostname) });
+ qro.onchange = handleConfigChange;
+
+ qro = qrs.option(form.DynamicList, 'allowed_ips', _('Allowed IPs'), _('IP addresses that are allowed inside the tunnel. The peer will accept tunnelled packets with source IP addresses matching this list and route back packets with matching destination IP.'));
+ qro.datatype = 'ipaddr';
+ qro.default = ips;
+ ips.forEach(function(ip) { qro.value(ip) });
+ qro.onchange = handleConfigChange;
+
+ qro = qrs.option(form.DummyValue, 'output');
+ qro.renderWidget = function() {
+ var peer_config = configGenerator(hostnames[0], ips);
+
+ var node = E('div', {
+ 'style': 'display:flex;flex-wrap:wrap;align-items:center;gap:.5em;width:100%'
+ }, [
+ E('div', {
+ 'class': 'qr-code',
+ 'style': 'width:320px;flex:0 1 320px;text-align:center'
+ }, [
+ E('em', { 'class': 'spinning' }, [ _('Generating QR code…') ])
+ ]),
+ E('pre', {
+ 'class': 'client-config',
+ 'style': 'flex:1;white-space:pre;overflow:auto',
+ 'click': function(ev) {
+ var sel = window.getSelection(),
+ range = document.createRange();
+
+ range.selectNodeContents(ev.currentTarget);
+
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ }, [ peer_config ])
+ ]);
+
+ invokeQREncode(peer_config, node.firstChild);
+
+ return node;
+ };
+
+ return qrm.render().then(function(nodes) {
+ mapNode.classList.add('hidden');
+ mapNode.nextElementSibling.classList.add('hidden');
+
+ headNode.appendChild(E('span', [ ' » ', _('Generate configuration') ]));
+ mapNode.parentNode.appendChild(E([], [
+ nodes,
+ E('div', {
+ 'class': 'right'
+ }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': function() {
+ nodes.parentNode.removeChild(nodes.nextSibling);
+ nodes.parentNode.removeChild(nodes);
+ mapNode.classList.remove('hidden');
+ mapNode.nextSibling.classList.remove('hidden');
+ headNode.removeChild(headNode.lastChild);
+ }
+ }, [ _('Back to peer configuration') ])
+ ])
+ ]));
+
+ if (!s.formvalue(s.section, 'listen_port')) {
+ nodes.appendChild(E('div', { 'class': 'alert-message' }, [
+ E('p', [
+ _('No fixed interface listening port defined, peers might not be able to initiate connections to this WireGuard instance!')
+ ])
+ ]));
+ }
+ });
+ });
+ };
+
+ o.cfgvalue = function(section_id, value) {
+ var privkey = this.section.cfgvalue(section_id, 'private_key');
+
+ return E('button', {
+ 'class': 'btn qr-code',
+ 'style': 'display:inline-flex;align-items:center;gap:.5em',
+ 'click': ui.createHandlerFn(this, 'handleGenerateQR', section_id),
+ 'disabled': privkey ? null : ''
+ }, [
+ Object.assign(E(qrIcon), { style: 'width:22px;height:22px' }),
+ _('Generate configuration…')
+ ]);
+ };
},
deleteConfiguration: function() {
diff --git a/protocols/luci-proto-wireguard/htdocs/luci-static/resources/view/wireguard/status.js b/protocols/luci-proto-wireguard/htdocs/luci-static/resources/view/wireguard/status.js
new file mode 100644
index 0000000000..4344c36739
--- /dev/null
+++ b/protocols/luci-proto-wireguard/htdocs/luci-static/resources/view/wireguard/status.js
@@ -0,0 +1,175 @@
+'use strict';
+'require view';
+'require rpc';
+'require poll';
+'require dom';
+'require ui';
+
+
+var callGetWgInstances = rpc.declare({
+ object: 'luci.wireguard',
+ method: 'getWgInstances'
+});
+
+function timestampToStr(timestamp) {
+ if (timestamp < 1)
+ return _('Never', 'No WireGuard peer handshake yet');
+
+ var seconds = (Date.now() / 1000) - timestamp;
+ var ago;
+
+ if (seconds < 60)
+ ago = _('%ds ago').format(seconds);
+ else if (seconds < 3600)
+ ago = _('%dm ago').format(seconds / 60);
+ else if (seconds < 86401)
+ ago = _('%dh ago').format(seconds / 3600);
+ else
+ ago = _('over a day ago');
+
+ return (new Date(timestamp * 1000)).toUTCString() + ' (' + ago + ')';
+}
+
+function handleInterfaceDetails(iface) {
+ ui.showModal(_('Instance Details'), [
+ ui.itemlist(E([]), [
+ _('Name'), iface.name,
+ _('Public Key'), E('code', [ iface.public_key ]),
+ _('Listen Port'), iface.listen_port,
+ _('Firewall Mark'), iface.fwmark != 'off' ? iface.fwmark : E('em', _('none'))
+ ]),
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'btn cbi-button',
+ 'click': ui.hideModal
+ }, [ _('Dismiss') ])
+ ])
+ ]);
+}
+
+function handlePeerDetails(peer) {
+ ui.showModal(_('Peer Details'), [
+ ui.itemlist(E([]), [
+ _('Description'), peer.name,
+ _('Public Key'), E('code', [ peer.public_key ]),
+ _('Endpoint'), peer.endpoint,
+ _('Allowed IPs'), (Array.isArray(peer.allowed_ips) && peer.allowed_ips.length) ? peer.allowed_ips.join(', ') : E('em', _('none')),
+ _('Received Data'), '%1024mB'.format(peer.transfer_rx),
+ _('Transmitted Data'), '%1024mB'.format(peer.transfer_tx),
+ _('Latest Handshake'), timestampToStr(+peer.latest_handshake),
+ _('Keep-Alive'), (peer.persistent_keepalive != 'off') ? _('every %ds', 'WireGuard keep alive interval').format(+peer.persistent_keepalive) : E('em', _('none')),
+ ]),
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'btn cbi-button',
+ 'click': ui.hideModal
+ }, [ _('Dismiss') ])
+ ])
+ ]);
+}
+
+function renderPeerTable(instanceName, peers) {
+ var t = new L.ui.Table(
+ [
+ _('Peer'),
+ _('Endpoint'),
+ _('Data Received'),
+ _('Data Transmitted'),
+ _('Latest Handshake')
+ ],
+ {
+ id: 'peers-' + instanceName
+ },
+ E('em', [
+ _('No peers connected')
+ ])
+ );
+
+ t.update(peers.map(function(peer) {
+ return [
+ [
+ peer.name || '',
+ E('div', {
+ 'style': 'cursor:pointer',
+ 'click': ui.createHandlerFn(this, handlePeerDetails, peer)
+ }, [
+ E('p', [
+ peer.name ? E('span', [ peer.name ]) : E('em', [ _('Untitled peer') ])
+ ]),
+ E('span', {
+ 'class': 'ifacebadge hide-sm',
+ 'data-tooltip': _('Public key: %h', 'Tooltip displaying full WireGuard peer public key').format(peer.public_key)
+ }, [
+ E('code', [ peer.public_key.replace(/^(.{5}).+(.{6})$/, '$1…$2') ])
+ ])
+ ])
+ ],
+ peer.endpoint,
+ [ +peer.transfer_rx, '%1024mB'.format(+peer.transfer_rx) ],
+ [ +peer.transfer_tx, '%1024mB'.format(+peer.transfer_tx) ],
+ [ +peer.latest_handshake, timestampToStr(+peer.latest_handshake) ]
+ ];
+ }));
+
+ return t.render();
+}
+
+return view.extend({
+ renderIfaces: function(ifaces) {
+ var res = [
+ E('h2', [ _('WireGuard Status') ])
+ ];
+
+ for (var instanceName in ifaces) {
+ res.push(
+ E('h3', [ _('Instance "%h"', 'WireGuard instance heading').format(instanceName) ]),
+ E('p', {
+ 'style': 'cursor:pointer',
+ 'click': ui.createHandlerFn(this, handleInterfaceDetails, ifaces[instanceName])
+ }, [
+ E('span', { 'class': 'ifacebadge' }, [
+ E('img', { 'src': L.resource('icons', 'tunnel.png') }),
+ '\xa0',
+ instanceName
+ ]),
+ E('span', { 'style': 'opacity:.8' }, [
+ ' · ',
+ _('Port %d', 'WireGuard listen port').format(ifaces[instanceName].listen_port),
+ ' · ',
+ E('code', { 'click': '' }, [ ifaces[instanceName].public_key ])
+ ])
+ ]),
+ renderPeerTable(instanceName, ifaces[instanceName].peers)
+ );
+ }
+
+ if (res.length == 1)
+ res.push(E('p', { 'class': 'center', 'style': 'margin-top:5em' }, [
+ E('em', [ _('No WireGuard interfaces configured.') ])
+ ]));
+
+ return E([], res);
+ },
+
+ render: function() {
+ poll.add(L.bind(function () {
+ return callGetWgInstances().then(L.bind(function(ifaces) {
+ dom.content(
+ document.querySelector('#view'),
+ this.renderIfaces(ifaces)
+ );
+ }, this));
+ }, this), 5);
+
+ return E([], [
+ E('h2', [ _('WireGuard Status') ]),
+ E('p', { 'class': 'center', 'style': 'margin-top:5em' }, [
+ E('em', [ _('Loading data…') ])
+ ])
+ ]);
+ },
+
+ handleReset: null,
+ handleSaveApply: null,
+ handleSave: null
+});
diff --git a/protocols/luci-proto-wireguard/root/usr/share/luci/menu.d/luci-proto-wireguard.json b/protocols/luci-proto-wireguard/root/usr/share/luci/menu.d/luci-proto-wireguard.json
new file mode 100644
index 0000000000..06940ee7ef
--- /dev/null
+++ b/protocols/luci-proto-wireguard/root/usr/share/luci/menu.d/luci-proto-wireguard.json
@@ -0,0 +1,14 @@
+{
+ "admin/status/wireguard": {
+ "title": "WireGuard",
+ "order": 92,
+ "action": {
+ "type": "view",
+ "path": "wireguard/status"
+ },
+ "depends": {
+ "acl": [ "luci-proto-wireguard" ],
+ "uci": { "network": true }
+ }
+ }
+}
diff --git a/protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json b/protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json
new file mode 100644
index 0000000000..e7187c0e4f
--- /dev/null
+++ b/protocols/luci-proto-wireguard/root/usr/share/rpcd/acl.d/luci-wireguard.json
@@ -0,0 +1,25 @@
+{
+ "luci-proto-wireguard": {
+ "description": "Grant access to LuCI Wireguard procedures",
+ "read": {
+ "file": {
+ "/usr/bin/qrencode --inline --8bit --type=SVG --output=- -- *": [ "exec" ]
+ },
+ "ubus": {
+ "luci.wireguard": [
+ "getWgInstances"
+ ]
+ },
+ "uci": [ "ddns", "system" ]
+ },
+ "write": {
+ "ubus": {
+ "luci.wireguard": [
+ "generateKeyPair",
+ "getPublicAndPrivateKeyFromPrivate",
+ "generatePsk"
+ ]
+ }
+ }
+ }
+}
diff --git a/protocols/luci-proto-wireguard/root/usr/share/rpcd/ucode/luci.wireguard b/protocols/luci-proto-wireguard/root/usr/share/rpcd/ucode/luci.wireguard
new file mode 100644
index 0000000000..add810c8ae
--- /dev/null
+++ b/protocols/luci-proto-wireguard/root/usr/share/rpcd/ucode/luci.wireguard
@@ -0,0 +1,107 @@
+// Copyright 2022 Jo-Philipp Wich <jo@mein.io>
+// Licensed to the public under the Apache License 2.0.
+
+'use strict';
+
+import { cursor } from 'uci';
+import { popen } from 'fs';
+
+
+function shellquote(s) {
+ return `'${replace(s ?? '', "'", "'\\''")}'`;
+}
+
+function command(cmd) {
+ return trim(popen(cmd)?.read?.('all'));
+}
+
+
+const methods = {
+ generatePsk: {
+ call: function() {
+ return { psk: command('wg genpsk 2>/dev/null') };
+ }
+ },
+
+ generateKeyPair: {
+ call: function() {
+ const priv = command('wg genkey 2>/dev/null');
+ const pub = command(`echo ${shellquote(priv)} | wg pubkey 2>/dev/null`);
+
+ return { keys: { priv, pub } };
+ }
+ },
+
+ getPublicAndPrivateKeyFromPrivate: {
+ args: { privkey: "privkey" },
+ call: function(req) {
+ const priv = req.args?.privkey;
+ const pub = command(`echo ${shellquote(priv)} | wg pubkey 2>/dev/null`);
+
+ return { keys: { priv, pub } };
+ }
+ },
+
+ getWgInstances: {
+ call: function() {
+ const data = {};
+ let last_device;
+ let qr_pubkey = {};
+
+ const uci = cursor();
+ const wg_dump = popen("wg show all dump 2>/dev/null");
+
+ if (wg_dump) {
+ uci.load("network");
+
+ for (let line = wg_dump.read('line'); length(line); line = wg_dump.read('line')) {
+ const record = split(rtrim(line, '\n'), '\t');
+
+ if (last_device != record[0]) {
+ last_device = record[0];
+ data[last_device] = {
+ name: last_device,
+ public_key: record[2],
+ listen_port: record[3],
+ fwmark: record[4],
+ peers: []
+ };
+
+ if (!length(record[2]) || record[2] == '(none)')
+ qr_pubkey[last_device] = '';
+ else
+ qr_pubkey[last_device] = `PublicKey = ${record[2]}`;
+ }
+ else {
+ let peer_name;
+
+ uci.foreach('network', `wireguard_${last_device}`, (s) => {
+ if (s.public_key == record[1])
+ peer_name = s.description;
+ });
+
+ const peer = {
+ name: peer_name,
+ public_key: record[1],
+ endpoint: record[3],
+ allowed_ips: [],
+ latest_handshake: record[5],
+ transfer_rx: record[6],
+ transfer_tx: record[7],
+ persistent_keepalive: record[8]
+ };
+
+ if (record[3] != '(none)' && length(record[4]))
+ push(peer.allowed_ips, ...split(record[4], ','));
+
+ push(data[last_device].peers, peer);
+ }
+ }
+ }
+
+ return data;
+ }
+ }
+};
+
+return { 'luci.wireguard': methods };