luci-mod-status: reimplement route status page as client side view
authorJo-Philipp Wich <jo@mein.io>
Sun, 29 Dec 2019 22:03:58 +0000 (23:03 +0100)
committerJo-Philipp Wich <jo@mein.io>
Wed, 4 Mar 2020 07:50:21 +0000 (08:50 +0100)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
(backported from commit 16d049f7cd76f0e309814a2b09cde32ff680d962)

modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json
modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js [new file with mode: 0644]
modules/luci-mod-status/luasrc/controller/admin/status.lua
modules/luci-mod-status/luasrc/view/admin_status/routes.htm [deleted file]

index ba0c213c9e3c37b2f356916dfb1f22b7942a57db..1f5b26f8d659438c746ef69a76b662aa2ab7de71 100644 (file)
                                "/bin/ping6 *": [ "exec" ],
                                "/bin/traceroute *": [ "exec" ],
                                "/bin/traceroute6 *": [ "exec" ],
+                               "/sbin/ip -4 neigh show": [ "exec" ],
+                               "/sbin/ip -4 route show table all": [ "exec" ],
+                               "/sbin/ip -6 neigh show": [ "exec" ],
+                               "/sbin/ip -6 route show table all": [ "exec" ],
                                "/sbin/logread -e ^": [ "exec" ],
                                "/usr/bin/ping *": [ "exec" ],
                                "/usr/bin/ping6 *": [ "exec" ],
diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js
new file mode 100644 (file)
index 0000000..a296b62
--- /dev/null
@@ -0,0 +1,216 @@
+'use strict';
+'require fs';
+'require rpc';
+'require validation';
+
+var callNetworkInterfaceDump = rpc.declare({
+       object: 'network.interface',
+       method: 'dump',
+       expect: { interface: [] }
+});
+
+function applyMask(addr, mask, v6) {
+       var words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr);
+
+       if (!words || mask < 0 || mask > (v6 ? 128 : 32))
+               return null;
+
+       for (var i = 0; i < words.length; i++) {
+               var b = Math.min(mask, v6 ? 16 : 8);
+               words[i] &= ((1 << b) - 1);
+               mask -= b;
+       }
+
+       return String.prototype.format.apply(
+               v6 ? '%x:%x:%x:%x:%x:%x:%x:%x' : '%d.%d.%d.%d', words);
+}
+
+return L.view.extend({
+       load: function() {
+               return Promise.all([
+                       callNetworkInterfaceDump(),
+                       L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'neigh', 'show' ]), {}),
+                       L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'route', 'show', 'table', 'all' ]), {}),
+                       L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'neigh', 'show' ]), {}),
+                       L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'route', 'show', 'table', 'all' ]), {})
+               ]);
+       },
+
+       getNetworkByDevice(networks, dev, addr, mask, v6) {
+               var addr_arrays = [ 'ipv4-address', 'ipv6-address', 'ipv6-prefix', 'ipv6-prefix-assignment', 'route' ],
+                   matching_iface = null,
+                   matching_prefix = -1;
+
+               for (var i = 0; i < networks.length; i++) {
+                       if (!L.isObject(networks[i]))
+                               continue;
+
+                       if (networks[i].l3_device != dev && networks[i].device != dev)
+                               continue;
+
+                       for (var j = 0; j < addr_arrays.length; j++) {
+                               var addr_list = networks[i][addr_arrays[j]];
+
+                               if (!Array.isArray(addr_list) || addr_list.length == 0)
+                                       continue;
+
+                               for (var k = 0; k < addr_list.length; k++) {
+                                       var cmp_addr = addr_list[k].address || addr_list[k].target,
+                                           cmp_mask = addr_list[k].mask;
+
+                                       if (cmp_addr == null)
+                                               continue;
+
+                                       var addr1 = applyMask(cmp_addr, cmp_mask, v6),
+                                           addr2 = applyMask(addr, cmp_mask, v6);
+
+                                       if (addr1 != addr2 || mask < cmp_mask)
+                                               continue;
+
+                                       if (cmp_mask > matching_prefix) {
+                                               matching_iface = networks[i].interface;
+                                               matching_prefix = cmp_mask;
+                                       }
+                               }
+                       }
+               }
+
+               return matching_iface;
+       },
+
+       parseNeigh: function(s, networks, v6) {
+               var lines = s.trim().split(/\n/),
+                   res = [];
+
+               for (var i = 0; i < lines.length; i++) {
+                       var m = lines[i].match(/^([0-9a-f:.]+) (.+) (\S+)$/),
+                           addr = m ? m[1] : null,
+                           flags = m ? m[2].trim().split(/\s+/) : [],
+                           state = (m ? m[3] : null) || 'FAILED';
+
+                       if (!addr || state == 'FAILED' || addr.match(/^fe[89a-f][0-9a-f]:/))
+                               continue;
+
+                       for (var j = 0; j < flags.length; j += 2)
+                               flags[flags[j]] = flags[j + 1];
+
+                       if (!flags.lladdr)
+                               continue;
+
+                       var net = this.getNetworkByDevice(networks, flags.dev, addr, v6 ? 128 : 32, v6);
+
+                       res.push([
+                               addr,
+                               flags.lladdr.toUpperCase(),
+                               E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ])
+                       ]);
+               }
+
+               return res;
+       },
+
+       parseRoute: function(s, networks, v6) {
+               var lines = s.trim().split(/\n/),
+                   res = [];
+
+               for (var i = 0; i < lines.length; i++) {
+                       var m = lines[i].match(/^(?:([a-z_]+|\d+) )?(default|[0-9a-f:.\/]+) (.+)$/),
+                           type = (m ? m[1] : null) || 'unicast',
+                           dest = m ? (m[2] == 'default' ? (v6 ? '::/0' : '0.0.0.0/0') : m[2]) : null,
+                           flags = m ? m[3].trim().split(/\s+/) : [];
+
+                       console.debug(lines[i], m);
+
+                       if (!dest || type != 'unicast' || dest == 'fe80::/64' || dest == 'ff00::/8')
+                               continue;
+
+                       for (var j = 0; j < flags.length; j += 2)
+                               flags[flags[j]] = flags[j + 1];
+
+                       var addr = dest.split('/'),
+                           bits = (addr[1] != null) ? +addr[1] : (v6 ? 128 : 32),
+                           net = this.getNetworkByDevice(networks, flags.dev, addr[0], bits, v6);
+
+                       res.push([
+                               E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ]),
+                               dest,
+                               (v6 ? flags.from : flags.via) || '-',
+                               String(flags.metric || 0),
+                               flags.table || 'main'
+                       ]);
+               }
+
+               return res;
+       },
+
+       render: function(data) {
+               var networks = data[0],
+                   ip4neigh = data[1].stdout || '',
+                   ip4route = data[2].stdout || '',
+                   ip6neigh = data[3].stdout || '',
+                   ip6route = data[4].stdout || '';
+
+               var neigh4tbl = E('div', { 'class': 'table' }, [
+                       E('div', { 'class': 'tr table-titles' }, [
+                               E('div', { 'class': 'th' }, [ _('IPv4-Address') ]),
+                               E('div', { 'class': 'th' }, [ _('MAC-Address') ]),
+                               E('div', { 'class': 'th' }, [ _('Interface') ])
+                       ])
+               ]);
+
+               var route4tbl = E('div', { 'class': 'table' }, [
+                       E('div', { 'class': 'tr table-titles' }, [
+                               E('div', { 'class': 'th' }, [ _('Network') ]),
+                               E('div', { 'class': 'th' }, [ _('Target') ]),
+                               E('div', { 'class': 'th' }, [ _('IPv4-Gateway') ]),
+                               E('div', { 'class': 'th' }, [ _('Metric') ]),
+                               E('div', { 'class': 'th' }, [ _('Table') ])
+                       ])
+               ]);
+
+               var neigh6tbl = E('div', { 'class': 'table' }, [
+                       E('div', { 'class': 'tr table-titles' }, [
+                               E('div', { 'class': 'th' }, [ _('IPv6-Address') ]),
+                               E('div', { 'class': 'th' }, [ _('MAC-Address') ]),
+                               E('div', { 'class': 'th' }, [ _('Interface') ])
+                       ])
+               ]);
+
+               var route6tbl = E('div', { 'class': 'table' }, [
+                       E('div', { 'class': 'tr table-titles' }, [
+                               E('div', { 'class': 'th' }, [ _('Network') ]),
+                               E('div', { 'class': 'th' }, [ _('Target') ]),
+                               E('div', { 'class': 'th' }, [ _('Source') ]),
+                               E('div', { 'class': 'th' }, [ _('Metric') ]),
+                               E('div', { 'class': 'th' }, [ _('Table') ])
+                       ])
+               ]);
+
+               cbi_update_table(neigh4tbl, this.parseNeigh(ip4neigh, networks, false));
+               cbi_update_table(route4tbl, this.parseRoute(ip4route, networks, false));
+               cbi_update_table(neigh6tbl, this.parseNeigh(ip6neigh, networks, true));
+               cbi_update_table(route6tbl, this.parseRoute(ip6route, networks, true));
+
+               return E([], [
+                       E('h2', {}, [ _('Routes') ]),
+                       E('p', {}, [ _('The following rules are currently active on this system.') ]),
+
+                       E('h3', {}, [ _('ARP') ]),
+                       neigh4tbl,
+
+                       E('h3', {}, _('Active <abbr title="Internet Protocol Version 4">IPv4</abbr>-Routes')),
+                       route4tbl,
+
+                       E('h3', {}, [ _('IPv6 Neighbours') ]),
+                       neigh6tbl,
+
+                       E('h3', {}, _('Active <abbr title="Internet Protocol Version 6">IPv6</abbr>-Routes')),
+                       route6tbl
+               ]);
+       },
+
+       handleSaveApply: null,
+       handleSave: null,
+       handleReset: null
+});
+
index d94a6e27fded3fc8fe8247ec9813c0e77212e8c4..c78a83b7876fbb8e012f9d711519d0f1bea05e8a 100644 (file)
@@ -11,7 +11,7 @@ function index()
 
        entry({"admin", "status", "iptables"}, view("status/iptables"), _("Firewall"), 2).leaf = true
 
-       entry({"admin", "status", "routes"}, template("admin_status/routes"), _("Routes"), 3)
+       entry({"admin", "status", "routes"}, view("status/routes"), _("Routes"), 3)
        entry({"admin", "status", "syslog"}, view("status/syslog"), _("System Log"), 4)
        entry({"admin", "status", "dmesg"}, view("status/dmesg"), _("Kernel Log"), 5)
        entry({"admin", "status", "processes"}, view("status/processes"), _("Processes"), 6)
diff --git a/modules/luci-mod-status/luasrc/view/admin_status/routes.htm b/modules/luci-mod-status/luasrc/view/admin_status/routes.htm
deleted file mode 100644 (file)
index 74779f6..0000000
+++ /dev/null
@@ -1,156 +0,0 @@
-<%#
- Copyright 2008-2009 Steven Barth <steven@midlink.org>
- Copyright 2008-2015 Jo-Philipp Wich <jow@openwrt.org>
- Licensed to the public under the Apache License 2.0.
--%>
-
-<%-
-       require "luci.tools.webadmin"
-       require "nixio.fs"
-
-       local ip = require "luci.ip"
-       local style = true
-       local _, v
-
-       local rtn = {
-               [255] = "local",
-               [254] = "main",
-               [253] = "default",
-               [0]   = "unspec"
-       }
-
-       if nixio.fs.access("/etc/iproute2/rt_tables") then
-               local ln
-               for ln in io.lines("/etc/iproute2/rt_tables") do
-                       local i, n = ln:match("^(%d+)%s+(%S+)")
-                       if i and n then
-                               rtn[tonumber(i)] = n
-                       end
-               end
-       end
--%>
-
-<%+header%>
-
-
-<div class="cbi-map" id="cbi-network">
-       <h2 name="content"><%:Routes%></h2>
-       <div class="cbi-map-descr"><%:The following rules are currently active on this system.%></div>
-
-       <div class="cbi-section">
-               <legend>ARP</legend>
-               <div class="cbi-section-node">
-                       <div class="table">
-                               <div class="tr table-titles">
-                                       <div class="th"><%_<abbr title="Internet Protocol Version 4">IPv4</abbr>-Address%></div>
-                                       <div class="th"><%_<abbr title="Media Access Control">MAC</abbr>-Address%></div>
-                                       <div class="th"><%:Interface%></div>
-                               </div>
-
-                               <%
-                                       for _, v in ipairs(ip.neighbors({ family = 4 })) do
-                                               if v.mac then
-                               %>
-                               <div class="tr cbi-rowstyle-<%=(style and 1 or 2)%>">
-                                       <div class="td"><%=v.dest%></div>
-                                       <div class="td"><%=v.mac%></div>
-                                       <div class="td"><%=luci.tools.webadmin.iface_get_network(v.dev) or '(' .. v.dev .. ')'%></div>
-                               </div>
-                               <%
-                                                       style = not style
-                                               end
-                                       end
-                               %>
-                       </div>
-               </div>
-       </div>
-
-       <div class="cbi-section">
-               <legend><%_Active <abbr title="Internet Protocol Version 4">IPv4</abbr>-Routes%></legend>
-               <div class="cbi-section-node">
-                       <div class="table">
-                               <div class="tr table-titles">
-                                       <div class="th"><%:Network%></div>
-                                       <div class="th"><%:Target%></div>
-                                       <div class="th"><%_<abbr title="Internet Protocol Version 4">IPv4</abbr>-Gateway%></div>
-                                       <div class="th"><%:Metric%></div>
-                                       <div class="th"><%:Table%></div>
-                               </div>
-                               <% for _, v in ipairs(ip.routes({ family = 4, type = 1 })) do %>
-                               <div class="tr cbi-rowstyle-<%=(style and 1 or 2)%>">
-                                       <div class="td"><%=luci.tools.webadmin.iface_get_network(v.dev) or v.dev%></div>
-                                       <div class="td"><%=v.dest%></div>
-                                       <div class="td"><%=v.gw or "-"%></div>
-                                       <div class="td"><%=v.metric or 0%></div>
-                                       <div class="td"><%=rtn[v.table] or v.table%></div>
-                               </div>
-                               <% style = not style end %>
-                       </div>
-               </div>
-       </div>
-
-       <%
-               if nixio.fs.access("/proc/net/ipv6_route") then
-                       style = true
-       %>
-       <div class="cbi-section">
-               <legend><%_Active <abbr title="Internet Protocol Version 6">IPv6</abbr>-Routes%></legend>
-               <div class="cbi-section-node">
-                       <div class="table">
-                               <div class="tr table-titles">
-                                       <div class="th"><%:Network%></div>
-                                       <div class="th"><%:Target%></div>
-                                       <div class="th"><%:Source%></div>
-                                       <div class="th"><%:Metric%></div>
-                                       <div class="th"><%:Table%></div>
-                               </div>
-                               <%
-                                       for _, v in ipairs(ip.routes({ family = 6, type = 1 })) do
-                                               if v.dest and not v.dest:is6linklocal() then
-                               %>
-                               <div class="tr cbi-rowstyle-<%=(style and 1 or 2)%>">
-                                       <div class="td"><%=luci.tools.webadmin.iface_get_network(v.dev) or '(' .. v.dev .. ')'%></div>
-                                       <div class="td"><%=v.dest%></div>
-                                       <div class="td"><%=v.from%></div>
-                                       <div class="td"><%=v.metric or 0%></div>
-                                       <div class="td"><%=rtn[v.table] or v.table%></div>
-                               </div>
-                               <%
-                                                       style = not style
-                                               end
-                                       end
-                               %>
-                       </div>
-               </div>
-       </div>
-
-       <div class="cbi-section">
-               <legend><%:IPv6 Neighbours%></legend>
-               <div class="cbi-section-node">
-                       <div class="table">
-                               <div class="tr table-titles">
-                                       <div class="th"><%:IPv6-Address%></div>
-                                       <div class="th"><%:MAC-Address%></div>
-                                       <div class="th"><%:Interface%></div>
-                               </div>
-                               <%
-                                       for _, v in ipairs(ip.neighbors({ family = 6 })) do
-                                               if v.dest and not v.dest:is6linklocal() and v.mac then
-                               %>
-                               <div class="tr cbi-rowstyle-<%=(style and 1 or 2)%>">
-                                       <div class="td"><%=v.dest%></div>
-                                       <div class="td"><%=v.mac%></div>
-                                       <div class="td"><%=luci.tools.webadmin.iface_get_network(v.dev) or '(' .. v.dev .. ')'%></div>
-                               </div>
-                               <%
-                                                       style = not style
-                                               end
-                                       end
-                               %>
-                       </div>
-               </div>
-       </div>
-       <% end %>
-</div>
-
-<%+footer%>