summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorlvoegl <lvoegl@tdt.de>2021-08-31 13:48:31 +0200
committerLukas Voegl <lvoegl@tdt.de>2021-09-17 12:28:36 +0200
commit8950c9f66c5d2e6123aeb5359fc3861d2f09ca72 (patch)
treebb4f90c77acb3033ffe5754bba40387a53b5e403
parent584301a9022b79482af55cbcf6b9f54136400bf1 (diff)
luci-app-wireguard: replace luci-app-wireguard
Signed-off-by: lvoegl <lvoegl@tdt.de>
-rw-r--r--applications/luci-app-wireguard/htdocs/luci-static/resources/view/wireguard/status.js214
-rw-r--r--applications/luci-app-wireguard/luasrc/view/wireguard.htm285
-rw-r--r--applications/luci-app-wireguard/root/usr/libexec/rpcd/luci.wireguard_status138
-rw-r--r--applications/luci-app-wireguard/root/usr/share/luci/menu.d/luci-app-wireguard.json7
-rw-r--r--applications/luci-app-wireguard/root/usr/share/rpcd/acl.d/luci-app-wireguard.json12
5 files changed, 368 insertions, 288 deletions
diff --git a/applications/luci-app-wireguard/htdocs/luci-static/resources/view/wireguard/status.js b/applications/luci-app-wireguard/htdocs/luci-static/resources/view/wireguard/status.js
new file mode 100644
index 0000000000..ca4ca9fd17
--- /dev/null
+++ b/applications/luci-app-wireguard/htdocs/luci-static/resources/view/wireguard/status.js
@@ -0,0 +1,214 @@
+'use strict';
+'require view';
+'require rpc';
+'require form';
+'require poll';
+
+
+var callGetWgInstances = rpc.declare({
+ object: 'luci.wireguard_status',
+ method: 'getWgInstances'
+});
+
+function timestampToStr(timestamp) {
+ if (timestamp < 1) {
+ return _('Never');
+ }
+ var now = new Date();
+ var seconds = (now.getTime() / 1000) - timestamp;
+ var ago = '';
+ if (seconds < 60) {
+ ago = parseInt(seconds) + _('s ago');
+ } else if (seconds < 3600) {
+ ago = parseInt(seconds / 60) + _('m ago');
+ } else if (seconds < 86401) {
+ ago = parseInt(seconds / 3600) + _('h ago');
+ } else {
+ ago = _('over a day ago');
+ }
+ var t = new Date(timestamp * 1000);
+ return t.toUTCString() + ' (' + ago + ')';
+}
+
+function generatePeerOption(key, title, value) {
+ return E('div', { 'class': 'cbi-value', 'style': 'padding: 0;' }, [
+ E('label', {
+ 'class': 'cbi-value-title', 'style': 'font-weight: bold;'
+ }, title),
+ E('input', {
+ 'class': 'cbi-input-text',
+ 'data-name': key,
+ 'style': 'border: none; float: left; width: 50%;',
+ 'disabled': '',
+ 'value': value
+ })
+ ]);
+}
+
+function generatePeerTable(options, iconSrc) {
+ return E('div', { 'class': 'table cbi-section-table' }, [
+ E('div', { 'class': 'td' },
+ E('img', { 'src': iconSrc, 'class': 'tunnel-icon' })
+ ),
+ E('div', { 'class': 'td peer-options' },
+ options.filter(function (option) {
+ return option[2] != null;
+ }).map(function (option) {
+ return generatePeerOption.apply(null, option);
+ })
+ )
+ ]);
+}
+
+function getTunnelIcon(latestHandshake) {
+ var img = (new Date().getTime() / 1000 - latestHandshake) < 140 ?
+ 'tunnel' : 'tunnel_disabled';
+
+ return L.resource('icons', img + '.png');
+}
+
+function generatePeerRows(peers) {
+ var peerRows = [];
+
+ peers.forEach(function (peer) {
+ var peerData = parsePeerData(peer);
+ var iconSrc = getTunnelIcon(peer.latest_handshake);
+
+ peerRows.push(E('div', {
+ 'class': 'tr cbi-section-table-row'
+ }, [
+ E('div', {
+ 'class': 'td peer-name',
+ 'style': 'width: 25%; font-size: 0.9rem;'
+ }, peer.name),
+ E('div', { 'class': 'td', 'data-section-id': peer.name },
+ generatePeerTable(peerData, iconSrc)
+ )
+ ]));
+ });
+
+ return peerRows;
+}
+
+function parseIfaceData(iface) {
+ return [
+ ['public_key', _('Public Key'),
+ iface.public_key != '(none)' ? iface.public_key : null],
+ ['listen_port', _('Listen Port'),
+ iface.listen_port > 0 ? iface.listen_port : null],
+ ['fwmark', _('Firewall Mark'),
+ iface.fwmark != 'off' ? iface.fwmark : null]
+ ];
+}
+
+function parsePeerData(peer) {
+ return [
+ ['public_key', _('Public Key'),
+ peer.public_key],
+ ['endpoint', _('Endpoint'),
+ peer.endpoint == '(none)' ? null : peer.endpoint],
+ ['allowed_ips', _('Allowed IPs'),
+ peer.allowed_ips.length == 0 ? null : peer.allowed_ips.join('\n')],
+ ['persistent_keepalive', _('Persistent Keepalive'),
+ peer.persistent_keepalive == 'off' ? null : peer.persistent_keepalive + 's'],
+ ['latest_handshake', _('Latest Handshake'),
+ timestampToStr(peer.latest_handshake)],
+ ['transfer_rx', _('Data Received'),
+ '%1024mB'.format(peer.transfer_rx)],
+ ['transfer_tx', _('Data Transmitted'),
+ '%1024mB'.format(peer.transfer_tx)]
+ ];
+}
+
+return view.extend({
+ load: function () {
+ return callGetWgInstances();
+ },
+ poll_status: function (nodes, ifaces) {
+ Object.keys(ifaces).forEach(function (ifaceName) {
+ var iface = ifaces[ifaceName];
+
+ var section = nodes.querySelector(
+ '[data-section-id="%q"]'.format(ifaceName)
+ );
+
+ parseIfaceData(iface).forEach(function (option) {
+ if (option[2] != null) {
+ var optionEl = section.querySelector(
+ '[data-name="%q"]'.format(option[0])
+ );
+ var inputEl = optionEl.querySelector('input');
+
+ inputEl.value = option[2];
+ }
+ });
+
+ iface.peers.forEach(function (peer) {
+ var peerData = parsePeerData(peer);
+ var iconSrc = getTunnelIcon(peer.latest_handshake);
+
+ var peerSection = section.querySelector(
+ '[data-section-id="%q"]'.format(peer.name)
+ );
+ var iconEl = peerSection.querySelector('.tunnel-icon');
+ iconEl.src = iconSrc;
+
+ peerData.forEach(function (option) {
+ if (option[2]) {
+ var inputEl = peerSection.querySelector(
+ '[data-name="%q"]'.format(option[0])
+ );
+ inputEl.value = option[2];
+ }
+ })
+ });
+ });
+ },
+ render: function (ifaces) {
+ var m, s, o, ss;
+
+ m = new form.JSONMap(ifaces, _('WireGuard Status'));
+ m.tabbed = true;
+
+ var ifaceNames = Object.keys(ifaces);
+ for (var i = ifaceNames.length - 1; i >= 0; i--) {
+ var ifaceName = ifaceNames[i];
+ var iface = ifaces[ifaceName];
+
+ s = m.section(form.TypedSection, ifaceName);
+ s.tabbed = true;
+ s.anonymous = true;
+
+ var ifaceData = parseIfaceData(iface);
+ ifaceData.forEach(function (option) {
+ if (option[2] != null) {
+ o = s.option(form.Value, option[0], option[1]);
+ o.readonly = true;
+ }
+ });
+
+ o = s.option(form.SectionValue, 'peers', form.TypedSection, 'peers');
+ ss = o.subsection;
+
+ ss.render = L.bind(function (view, section_id) {
+ return E('div', { 'class': 'cbi-section' }, [
+ E('h3', _('Peers')),
+ E('div', { 'class': 'table cbi-section-table' },
+ generatePeerRows(this.peers))
+ ]);
+ }, iface, this);
+ }
+
+ return m.render().then(L.bind(function (m, nodes) {
+ poll.add(L.bind(function () {
+ return callGetWgInstances().then(
+ L.bind(this.poll_status, this, nodes)
+ );
+ }, this), 5);
+ return nodes;
+ }, this, m));
+ },
+ handleReset: null,
+ handleSaveApply: null,
+ handleSave: null
+});
diff --git a/applications/luci-app-wireguard/luasrc/view/wireguard.htm b/applications/luci-app-wireguard/luasrc/view/wireguard.htm
deleted file mode 100644
index 9282e65d30..0000000000
--- a/applications/luci-app-wireguard/luasrc/view/wireguard.htm
+++ /dev/null
@@ -1,285 +0,0 @@
-<%#
- Copyright 2016-2017 Dan Luedtke <mail@danrl.com>
- Licensed to the public under the Apache License 2.0.
--%>
-
-<%
- local data = { }
- local last_device = ""
- local qr_pubkey = { }
-
- local function qr_clean(qr_type, value)
- if not value or value == "" or value == "(none)" then
- return ""
- end
- if qr_type == "privkey" then
- return "PrivateKey = " ..value
- elseif qr_type == "pubkey" then
- return "PublicKey = " ..value
- end
- end
-
- local wg_dump = io.popen("wg show all dump 2>/dev/null")
- if wg_dump then
- local line
- for line in wg_dump:lines() do
- local line = string.split(line, "\t")
- if not (last_device == line[1]) then
- last_device = line[1]
- data[line[1]] = {
- name = line[1],
- public_key = line[3],
- listen_port = line[4],
- fwmark = line[5],
- peers = { }
- }
- qr_pubkey[line[1]] = qr_clean("pubkey", line[3])
- else
- local peer = {
- public_key = line[2],
- endpoint = line[4],
- allowed_ips = { },
- latest_handshake = line[6],
- transfer_rx = line[7],
- transfer_tx = line[8],
- persistent_keepalive = line[9]
- }
- if not (line[4] == '(none)') then
- local ipkey, ipvalue
- for ipkey, ipvalue in pairs(string.split(line[5], ",")) do
- if #ipvalue > 0 then
- table.insert(peer['allowed_ips'], ipvalue)
- end
- end
- end
- table.insert(data[line[1]].peers, peer)
- end
- end
- end
-
- if luci.http.formvalue("status") == "1" then
- luci.http.prepare_content("application/json")
- luci.http.write_json(data)
- return
- end
--%>
-
-<%+header%>
-
-<script type="text/javascript">//<![CDATA[
-
- function bytes_to_str(bytes) {
- bytes = parseFloat(bytes);
- if (bytes < 1) { return "0 B"; }
- var sizes = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'];
- var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
- return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i];
- };
-
- function timestamp_to_str(timestamp) {
- if (timestamp < 1) {
- return '<%:Never%>';
- }
- var now = new Date();
- var seconds = (now.getTime() / 1000) - timestamp;
- var ago = "";
- if (seconds < 60) {
- ago = parseInt(seconds) + '<%:s ago%>';
- } else if (seconds < 3600) {
- ago = parseInt(seconds / 60) + '<%:m ago%>';
- } else if (seconds < 86401) {
- ago = parseInt(seconds / 3600) + '<%:h ago%>';
- } else {
- ago = '<%:over a day ago%>';
- }
- var t = new Date(timestamp * 1000);
- return t.toUTCString() + ' (' + ago + ')';
- }
-
- function toggle_qrcode(iface) {
- var view = document.getElementById(iface.name);
- if (view.style.display === "none") {
- view.style.display = "block";
- } else {
- view.style.display = "none";
- }
- }
-
- XHR.poll(-1, '<%=REQUEST_URI%>', { status: 1 },
- function(x, data) {
- for (var key in data) {
- if (!data.hasOwnProperty(key)) { continue; }
- var ifname = key;
- var iface = data[key];
- var s = "";
- if (iface.public_key == '(none)') {
- s += '<em><%:Interface does not have a public key!%></em>';
- } else {
- s += String.format(
- '<strong><%:Public Key%>: </strong>%s',
- iface.public_key
- );
- }
- if (iface.listen_port > 0) {
- s += String.format(
- '<br /><strong><%:Listen Port%>: </strong>%s',
- iface.listen_port
- );
- }
- if (iface.fwmark != 'off') {
- s += String.format(
- '<br /><strong><%:Firewall Mark%>: </strong>%s',
- iface.fwmark
- );
- }
- document.getElementById(ifname + "_info").innerHTML = s;
- for (var i = 0, ilen = iface.peers.length; i < ilen; i++) {
- var peer = iface.peers[i];
- var s = String.format(
- '<strong><%:Public Key%>: </strong>%s',
- peer.public_key
- );
- if (peer.endpoint != '(none)') {
- s += String.format(
- '<br /><strong><%:Endpoint%>: </strong>%s',
- peer.endpoint
- );
- }
- if (peer.allowed_ips.length > 0) {
- s += '<br /><strong><%:Allowed IPs%>:</strong>';
- for (var k = 0, klen = peer.allowed_ips.length; k < klen; k++) {
- s += '<br />&#160;&#160;&#8226;&#160;' + peer.allowed_ips[k];
- }
- }
- if (peer.persistent_keepalive != 'off') {
- s += String.format(
- '<br /><strong><%:Persistent Keepalive%>: </strong>%ss',
- peer.persistent_keepalive
- );
- }
- var icon = '<img src="<%=resource%>/icons/tunnel_disabled.png" />';
- var now = new Date();
- if (((now.getTime() / 1000) - peer.latest_handshake) < 140) {
- icon = '<img src="<%=resource%>/icons/tunnel.png" />';
- }
- s += String.format(
- '<br /><strong><%:Latest Handshake%>: </strong>%s',
- timestamp_to_str(peer.latest_handshake)
- );
- s += String.format(
- '<br /><strong><%:Data Received%>: </strong>%s' +
- '<br /><strong><%:Data Transmitted%>: </strong>%s',
- bytes_to_str(peer.transfer_rx),
- bytes_to_str(peer.transfer_tx),
- );
- document.getElementById(ifname + "_" + peer.public_key + "_icon").innerHTML = icon;
- document.getElementById(ifname + "_" + peer.public_key + "_info").innerHTML = s;
- }
- }
- });
-//]]></script>
-
-<h2><%:WireGuard Status%></h2>
-
-<div class="cbi-section">
-
-<% if next(data) == nil then %>
- <div class="table cbi-section-table">
- <div class="tr cbi-section-table-row">
- <p>
- <em><%:This section contains no values yet%></em>
- </p>
- </div>
- </div>
-<% end %>
-
-<%-
-local ikey, iface
-for ikey, iface in pairs(data) do
--%>
- <h3><%:Interface%> <%=ikey%></h3>
- <div class="cbi-value" id="button" style="padding: 5px">
- <input class="btn cbi-button cbi-button-apply" type="button" name="qrcode_<%=ikey%>" value="<%:Show/Hide QR-Code%>" onclick="toggle_qrcode(this)" />
- </div>
-<%-
- local qr_enc
- local qr_code
- local qr_privkey
- if fs.access("/usr/bin/qrencode") then
- qr_privkey = qr_clean("privkey", luci.sys.exec("wg genkey 2>/dev/null"))
- if qr_pubkey[ikey] and qr_privkey then
- qr_enc = "[Interface]\n" ..qr_privkey.. "\n[Peer]\n" ..qr_pubkey[ikey].. "\nAllowedIPs = 0.0.0.0/0, ::/0"
- qr_code = luci.sys.exec("/usr/bin/qrencode --inline --8bit --type=SVG --output=- '" ..qr_enc.. "' 2>/dev/null")
- else
- qr_code = "<em>The QR-Code could not be generated, the wg interface setup is incomplete!</em>"
- end
- else
- qr_code = "<em>For QR-Code support please install the package 'qrencode'!</em>"
- end
--%>
- <div class="cbi-section-node">
- <span class="cbi-value" style="display: none" id="qrcode_<%=ikey%>">
- <%:The QR-Code works per wg interface, it will be refreshed with every manual page reload and transfers the following information:%><br />
- &#8226;&#160;<%:[Interface] A random, on the fly generated 'PrivateKey', the key will not be saved on the router%><br />
- &#8226;&#160;<%:[Peer] The 'PublicKey' of that wg interface and the 'AllowedIPs' with the default of '0.0.0.0/0, ::/0' to allow sending traffic to any IPv4 and IPv6 address%><br />
- <hr /><%=qr_code%><br />
- </span>
- </div>
- <div class="cbi-section-node">
- <div class="table cbi-section-table">
- <div class="tr cbi-section-table-row" style="text-align: left;">
- <div class="td" style="text-align: left; vertical-align:top"><%:Configuration%></div>
- <div class="td" style="flex: 0 1 90%; text-align: left;">
- <div class="table cbi-section-table" style="border: 0px;">
- <div class="tr cbi-section-table-row" style="text-align: left; border: 0px;">
- <div class="td" id="<%=ikey%>_icon" style="width: 22px; text-align: left; border-top: 0px; padding: 3px;">&#160;</div>
- <div class="td" id="<%=ikey%>_info" style="flex: 0 1 90%; text-align: left; vertical-align:middle; padding: 3px; border-top: 0px;"><em><%:Collecting data...%></em></div>
- </div>
- </div>
- </div>
- </div>
- <%-
- local cur = uci.cursor()
- local pkey, peer
- for pkey, peer in pairs(iface.peers) do
- local desc
- cur:foreach("network", "wireguard_" .. ikey, function(s)
- local key, value, tmp_desc, pub_key
- for key, value in pairs(s) do
- if key == "description" then
- tmp_desc = value
- end
- if value == peer.public_key then
- pub_key = value
- end
- if pub_key and tmp_desc then
- desc = ': ' ..tmp_desc
- end
- end
- end)
- -%>
- <div class="tr cbi-section-table-row" style="text-align: left;">
- <div class="td" style="text-align: left; vertical-align:top"><%:Peer%><%=desc%></div>
- <div class="td" style="flex: 0 1 90%; text-align: left;">
- <div class="table cbi-section-table" style="border: 0px">
- <div class="tr cbi-section-table-row" style="border: 0px;">
- <div class="td" id="<%=ikey%>_<%=peer.public_key%>_icon" style="width:16px; text-align: left; padding: 3px;border-top: 0px;">
- <img src="<%=resource%>/icons/tunnel_disabled.png" />
- <small>?</small>
- </div>
- <div class="td" id="<%=ikey%>_<%=peer.public_key%>_info" style="flex: 0 1 90%; text-align: left; vertical-align:middle; padding: 3px;border-top: 0px;"><em><%:Collecting data...%></em></div>
- </div>
- </div>
- </div>
- </div>
- <%-
- end
- -%>
- </div>
- </div>
- <%-
-end
--%>
-</div>
-
-<%+footer%>
diff --git a/applications/luci-app-wireguard/root/usr/libexec/rpcd/luci.wireguard_status b/applications/luci-app-wireguard/root/usr/libexec/rpcd/luci.wireguard_status
new file mode 100644
index 0000000000..892e74dbf1
--- /dev/null
+++ b/applications/luci-app-wireguard/root/usr/libexec/rpcd/luci.wireguard_status
@@ -0,0 +1,138 @@
+#!/usr/bin/env lua
+
+local json = require "luci.jsonc"
+local sys = require "luci.sys"
+local io = require "io"
+local uci = require "uci"
+
+local methods = {
+ getWgInstances = {
+ call = function()
+ local data = {}
+ local last_device = ""
+ local qr_pubkey = {}
+
+ local wg_dump = io.popen("wg show all dump 2>/dev/null")
+ if wg_dump then
+ local line
+ for line in wg_dump:lines() do
+ local line = string.split(line, "\t")
+ if not (last_device == line[1]) then
+ last_device = line[1]
+ data[line[1]] = {
+ name = line[1],
+ public_key = line[3],
+ listen_port = line[4],
+ fwmark = line[5],
+ peers = {}
+ }
+ if not line[3] or line[3] == "" or line[3] == "(none)" then
+ qr_pubkey[line[1]] = ""
+ else
+ qr_pubkey[line[1]] = "PublicKey = " .. line[3]
+ end
+ else
+ local peer_name
+ local cur = uci.cursor()
+
+ cur:foreach(
+ "network",
+ "wireguard_" .. line[1],
+ function(s)
+ if s.public_key == line[2] then
+ peer_name = s.description
+ end
+ end
+ )
+
+ table.insert(
+ data[line[1]].peers,
+ {
+ name = peer_name,
+ public_key = line[2],
+ endpoint = line[4],
+ allowed_ips = {},
+ latest_handshake = line[6],
+ transfer_rx = line[7],
+ transfer_tx = line[8],
+ persistent_keepalive = line[9]
+ }
+ )
+
+ if not (line[4] == "(none)") then
+ local ipkey, ipvalue
+ for ipkey, ipvalue in pairs(string.split(line[5], ",")) do
+ if #ipvalue > 0 then
+ table.insert(data[line[1]].peers[peer_name]["allowed_ips"], ipvalue)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ return data
+ end
+ }
+}
+
+local function parseInput()
+ local parse = json.new()
+ local done, err
+
+ while true do
+ local chunk = io.read(4096)
+ if not chunk then
+ break
+ elseif not done and not err then
+ done, err = parse:parse(chunk)
+ end
+ end
+
+ if not done then
+ print(json.stringify({error = err or "Incomplete input"}))
+ os.exit(1)
+ end
+
+ return parse:get()
+end
+
+local function validateArgs(func, uargs)
+ local method = methods[func]
+ if not method then
+ print(json.stringify({error = "Method not found"}))
+ os.exit(1)
+ end
+
+ if type(uargs) ~= "table" then
+ print(json.stringify({error = "Invalid arguments"}))
+ os.exit(1)
+ end
+
+ uargs.ubus_rpc_session = nil
+
+ local k, v
+ local margs = method.args or {}
+ for k, v in pairs(uargs) do
+ if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then
+ print(json.stringify({error = "Invalid arguments"}))
+ os.exit(1)
+ end
+ end
+
+ return method
+end
+
+if arg[1] == "list" then
+ local _, method, rv = nil, nil, {}
+ for _, method in pairs(methods) do
+ rv[_] = method.args or {}
+ end
+ print((json.stringify(rv):gsub(":%[%]", ":{}")))
+elseif arg[1] == "call" then
+ local args = parseInput()
+ local method = validateArgs(arg[2], args)
+ local result, code = method.call(args)
+ print((json.stringify(result):gsub("^%[%]$", "{}")))
+ os.exit(code or 0)
+end
diff --git a/applications/luci-app-wireguard/root/usr/share/luci/menu.d/luci-app-wireguard.json b/applications/luci-app-wireguard/root/usr/share/luci/menu.d/luci-app-wireguard.json
index 3652bdabb4..02cdb5e871 100644
--- a/applications/luci-app-wireguard/root/usr/share/luci/menu.d/luci-app-wireguard.json
+++ b/applications/luci-app-wireguard/root/usr/share/luci/menu.d/luci-app-wireguard.json
@@ -3,11 +3,12 @@
"title": "WireGuard",
"order": 92,
"action": {
- "type": "template",
- "path": "wireguard"
+ "type": "view",
+ "path": "wireguard/status"
},
"depends": {
- "acl": [ "luci-mod-status-index" ]
+ "acl": [ "luci-app-wireguard" ],
+ "uci": { "network": true }
}
}
}
diff --git a/applications/luci-app-wireguard/root/usr/share/rpcd/acl.d/luci-app-wireguard.json b/applications/luci-app-wireguard/root/usr/share/rpcd/acl.d/luci-app-wireguard.json
new file mode 100644
index 0000000000..f0938e5b00
--- /dev/null
+++ b/applications/luci-app-wireguard/root/usr/share/rpcd/acl.d/luci-app-wireguard.json
@@ -0,0 +1,12 @@
+{
+ "luci-app-wireguard": {
+ "description": "Grant access to LuCI app wireguard",
+ "read": {
+ "ubus": {
+ "luci.wireguard_status": [
+ "getWgInstances"
+ ]
+ }
+ }
+ }
+}