luci-app-wireguard: replace luci-app-wireguard
authorlvoegl <lvoegl@tdt.de>
Tue, 31 Aug 2021 11:48:31 +0000 (13:48 +0200)
committerLukas Voegl <lvoegl@tdt.de>
Fri, 17 Sep 2021 10:28:36 +0000 (12:28 +0200)
Signed-off-by: lvoegl <lvoegl@tdt.de>
applications/luci-app-wireguard/htdocs/luci-static/resources/view/wireguard/status.js [new file with mode: 0644]
applications/luci-app-wireguard/luasrc/view/wireguard.htm [deleted file]
applications/luci-app-wireguard/root/usr/libexec/rpcd/luci.wireguard_status [new file with mode: 0644]
applications/luci-app-wireguard/root/usr/share/luci/menu.d/luci-app-wireguard.json
applications/luci-app-wireguard/root/usr/share/rpcd/acl.d/luci-app-wireguard.json [new file with mode: 0644]

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 (file)
index 0000000..ca4ca9f
--- /dev/null
@@ -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 (file)
index 9282e65..0000000
+++ /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 (file)
index 0000000..892e74d
--- /dev/null
@@ -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
index 3652bdabb46de3eb1524ddab33a5f0583044c9c0..02cdb5e8718346f11b280238555f0c9135164e55 100644 (file)
@@ -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 (file)
index 0000000..f0938e5
--- /dev/null
@@ -0,0 +1,12 @@
+{
+       "luci-app-wireguard": {
+               "description": "Grant access to LuCI app wireguard",
+               "read": {
+                       "ubus": {
+                               "luci.wireguard_status": [
+                                       "getWgInstances"
+                               ]
+                       }
+               }
+       }
+}