luci-mod-status: routes; refactor and add routesj page for ip -j output
authorPaul Donald <[email protected]>
Sun, 12 Oct 2025 16:37:57 +0000 (18:37 +0200)
committerPaul Donald <[email protected]>
Sun, 12 Oct 2025 17:43:57 +0000 (19:43 +0200)
Refactor functions and variables to ES6 standard.

Detect whether one of the JSON enabled ip variants is installed, and
prefer it to display route information over the text handling. This is
handled by the acls and menu depends.

Signed-off-by: Paul Donald <[email protected]>
modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js
modules/luci-mod-status/htdocs/luci-static/resources/view/status/routesj.js [new file with mode: 0644]
modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json
modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json

index 59ce7b7cd930a40a998fba6a2a16f42c735e5cc3..e48c1d65343e3ddb14c08ae65628a844e5d1a5bc 100644 (file)
@@ -1,38 +1,38 @@
 'use strict';
-'require view';
 'require fs';
 'require rpc';
-'require validation';
 'require ui';
+'require validation';
+'require view';
 
-var callNetworkInterfaceDump = rpc.declare({
+const callNetworkInterfaceDump = rpc.declare({
        object: 'network.interface',
        method: 'dump',
        expect: { interface: [] }
 });
 
-var checkUfpInstalled = rpc.declare({
+const checkUfpInstalled = rpc.declare({
        object: 'file',
        method: 'stat',
        params: [ 'path' ]
 });
 
-var callUfpList = rpc.declare({
+const callUfpList = rpc.declare({
        object: 'fingerprint',
        method: 'fingerprint',
        expect: { '': {} }
 });
 
 function applyMask(addr, mask, v6) {
-       var words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr);
-       var bword = v6 ? 0xffff : 0xff;
-       var bwlen = v6 ? 16 : 8;
+       const words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr);
+       const bword = v6 ? 0xffff : 0xff;
+       const bwlen = v6 ? 16 : 8;
 
        if (!words || mask < 0 || mask > (v6 ? 128 : 32))
                return null;
 
-       for (var i = 0; i < words.length; i++) {
-               var b = Math.min(mask, bwlen);
+       for (let i = 0; i < words.length; i++) {
+               const b = Math.min(mask, bwlen);
                words[i] &= (bword << (bwlen - b)) & bword;
                mask -= b;
        }
@@ -42,11 +42,11 @@ function applyMask(addr, mask, v6) {
 }
 
 return view.extend({
-       load: function() {
+       load() {
                return Promise.all([
                        checkUfpInstalled('/usr/sbin/ufpd')
-               ]).then(data => {
-                       var promises = [
+               ]).then(([ufpcheck]) => {
+                       return Promise.all([
                                callNetworkInterfaceDump(),
                                L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'neigh', 'show' ]), {}),
                                L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'route', 'show', 'table', 'all' ]), {}),
@@ -54,46 +54,33 @@ return view.extend({
                                L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'neigh', 'show' ]), {}),
                                L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'route', 'show', 'table', 'all' ]), {}),
                                L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'rule', 'show' ]), {}),
-                               data[0].type === 'file' ? callUfpList() : null
-                       ];
-
-                       return Promise.all(promises);
+                               ufpcheck?.type === 'file' ? callUfpList() : null
+                       ]);
                });
        },
 
        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;
+               const addr_arrays = [ 'ipv4-address', 'ipv6-address', 'ipv6-prefix', 'ipv6-prefix-assignment', 'route' ];
+               let matching_iface = null;
+               let matching_prefix = -1;
 
-               for (var i = 0; i < networks.length; i++) {
-                       if (!L.isObject(networks[i]))
+               for (const net of networks) {
+                       if (!L.isObject(net) || (net.l3_device !== dev && net.device !== dev))
                                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;
+                       for (const key of addr_arrays) {
+                               const list = net[key];
+                               if (!Array.isArray(list)) continue;
 
-                                       var addr1 = applyMask(cmp_addr, cmp_mask, v6),
-                                           addr2 = applyMask(addr, cmp_mask, v6);
+                               for (const { address, target, mask: cmp_mask } of list) {
+                                       const cmp_addr = address || target;
+                                       if (!cmp_addr) continue;
 
-                                       if (addr1 != addr2 || mask < cmp_mask)
+                                       if (applyMask(cmp_addr, cmp_mask, v6) !== applyMask(addr, cmp_mask, v6) || mask < cmp_mask)
                                                continue;
 
                                        if (cmp_mask > matching_prefix) {
-                                               matching_iface = networks[i].interface;
+                                               matching_iface = net.interface;
                                                matching_prefix = cmp_mask;
                                        }
                                }
@@ -103,21 +90,18 @@ return view.extend({
                return matching_iface;
        },
 
-       parseNeigh: function(s, macs, networks, v6) {
-               var lines = s.trim().split(/\n/),
-                   res = [];
+       parseNeighbs(nbs, macs, networks, v6) {
+               const 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',
-                           vendor;
+               for (const line of nbs.trim().split(/\n/)) {
+                       const [, addr = null, f = [], state = null] = line.match(/^([0-9a-f:.]+) (.+) (\S+) *$/);
+                       const flags = f?.trim?.().split?.(/\s+/);
+                       let vendor;
 
-                       if (!addr || state == 'FAILED' || addr.match(/^fe[89a-f][0-9a-f]:/))
+                       if (!addr || !state || addr.match(/^fe[89a-f][0-9a-f]:/))
                                continue;
 
-                       for (var j = 0; j < flags.length; j += 2)
+                       for (let j = 0; j < flags.length; j += 2)
                                flags[flags[j]] = flags[j + 1];
 
                        if (!flags.lladdr)
@@ -128,37 +112,34 @@ return view.extend({
                                        vendor = macs[mac].vendor;
                        }
 
-                       var net = this.getNetworkByDevice(networks, flags.dev, addr, v6 ? 128 : 32, v6);
+                       const net = this.getNetworkByDevice(networks, flags.dev, addr, v6 ? 128 : 32, v6);
 
                        res.push([
                                addr,
                                vendor ? flags.lladdr.toUpperCase() + ` (${vendor})` : flags.lladdr.toUpperCase(),
-                               E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ])
+                               E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ]),
                        ]);
                }
 
                return res;
        },
 
-       parseRoute: function(s, macs, networks, v6) {
-               var lines = s.trim().split(/\n/),
-                   res = [];
+       parseRoutes(routes, macs, networks, v6) {
+               const 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+/) : [];
+               for (const line of routes.trim().split(/\n/)) {
+                       const [, type = 'unicast', d, f = [] ] = line.match(/^(?:([a-z_]+|\d+) )?(default|[0-9a-f:.\/]+) (.+)$/);
+                       const dest = d == 'default' ? (v6 ? '::/0' : '0.0.0.0/0') : d;
+                       const flags = f?.trim?.().split?.(/\s+/);
 
                        if (!dest || type != 'unicast' || dest == 'fe80::/64' || dest == 'ff00::/8')
                                continue;
 
-                       for (var j = 0; j < flags.length; j += 2)
+                       for (let 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);
+                       const [addr, bits = (v6 ? 128 : 32)] = dest.split('/');
+                       const net = this.getNetworkByDevice(networks, flags.dev, addr, bits, v6);
 
                        res.push([
                                E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ]),
@@ -174,51 +155,39 @@ return view.extend({
                return res;
        },
 
-       parseRule: function(s) {
-               var lines = s.trim().split(/\n/),
-                   res = [];
-
-               for (var i = 0; i < lines.length; i++) {
-                       var m = lines[i].match(/^(\d+):\s+(.+)$/),
-                           prio = m ? m[1] : null,
-                           rule = m ? m[2] : null;
-
-                       res.push([
-                               prio,
-                               rule
-                       ]);
-               }
-
-               return res;
-       },
-
-       render: function(data) {
-               var networks = data[0],
-                   ip4neigh = data[1].stdout || '',
-                   ip4route = data[2].stdout || '',
-                   ip4rule = data[3].stdout || '',
-                   ip6neigh = data[4].stdout || '',
-                   ip6route = data[5].stdout || '',
-                   ip6rule = data[6].stdout || '',
-                   macdata = data[7];
-
-               var device_title = _('Which is used to access this %s').format(_('Target'));
-               var target_title = _('Network and its mask that define the size of the destination');
-               var gateway_title = _('The address through which this %s is reachable').format(_('Target'));
-               var metric_title = _('Quantifies the cost or distance to a destination in a way that allows routers to make informed decisions about the optimal path to forward data packets');
-               var table_title = _('Common name or numeric ID of the %s in which this route is found').format(_('Table'));
-               var proto_title = _('The routing protocol identifier of this route');
-               var source_title = _('Network and its mask that define which source addresses use this route');
-
-               var neigh4tbl = E('table', { 'class': 'table' }, [
+       parseRules: rules => rules.trim().split('\n').map(l => {
+               const [, prio=null, rule=null] = l.match(/^(\d+):\s+(.+)$/) || [];
+               return [prio, rule];
+       }),
+
+       render([
+               networks,
+               { stdout: ip4neigh = '' } = {},
+               { stdout: ip4route = '' } = {},
+               { stdout: ip4rule = '' } = {},
+               { stdout: ip6neigh = '' } = {},
+               { stdout: ip6route = '' } = {},
+               { stdout: ip6rule = '' } = {},
+               macdata,
+       ]) {
+
+               const device_title = _('Which is used to access this %s').format(_('Target'));
+               const target_title = _('Network and its mask that define the size of the destination');
+               const gateway_title = _('The address through which this %s is reachable').format(_('Target'));
+               const metric_title = _('Quantifies the cost or distance to a destination in a way that allows routers to make informed decisions about the optimal path to forward data packets');
+               const table_title = _('Common name or numeric ID of the %s in which this route is found').format(_('Table'));
+               const proto_title = _('The routing protocol identifier of this route');
+               const source_title = _('Network and its mask that define which source addresses use this route');
+
+               const neigh4tbl = E('table', { 'class': 'table' }, [
                        E('tr', { 'class': 'tr table-titles' }, [
                                E('th', { 'class': 'th' }, [ _('IP address') ]),
                                E('th', { 'class': 'th' }, [ _('MAC address') ]),
-                               E('th', { 'class': 'th' }, [ _('Interface') ])
+                               E('th', { 'class': 'th' }, [ _('Interface') ]),
                        ])
                ]);
 
-               var route4tbl = E('table', { 'class': 'table' }, [
+               const route4tbl = E('table', { 'class': 'table' }, [
                        E('tr', { 'class': 'tr table-titles' }, [
                                E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
                                E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
@@ -226,26 +195,26 @@ return view.extend({
                                E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
                                E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
                                E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
-                               E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ])
+                               E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
                        ])
                ]);
 
-               var rule4tbl = E('table', { 'class': 'table' }, [
+               const rule4tbl = E('table', { 'class': 'table' }, [
                        E('tr', { 'class': 'tr table-titles' }, [
                                E('th', { 'class': 'th' }, [ _('Priority') ]),
-                               E('th', { 'class': 'th' }, [ _('Rule') ])
+                               E('th', { 'class': 'th' }, [ _('Rule') ]),
                        ])
                ]);
 
-               var neigh6tbl = E('table', { 'class': 'table' }, [
+               const neigh6tbl = E('table', { 'class': 'table' }, [
                        E('tr', { 'class': 'tr table-titles' }, [
                                E('th', { 'class': 'th' }, [ _('IP address') ]),
                                E('th', { 'class': 'th' }, [ _('MAC address') ]),
-                               E('th', { 'class': 'th' }, [ _('Interface') ])
+                               E('th', { 'class': 'th' }, [ _('Interface') ]),
                        ])
                ]);
 
-               var route6tbl = E('table', { 'class': 'table' }, [
+               const route6tbl = E('table', { 'class': 'table' }, [
                        E('tr', { 'class': 'tr table-titles' }, [
                                E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
                                E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
@@ -253,37 +222,37 @@ return view.extend({
                                E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
                                E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
                                E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
-                               E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ])
+                               E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
                        ])
                ]);
 
-               var rule6tbl = E('table', { 'class': 'table' }, [
+               const rule6tbl = E('table', { 'class': 'table' }, [
                        E('tr', { 'class': 'tr table-titles' }, [
                                E('th', { 'class': 'th' }, [ _('Priority') ]),
-                               E('th', { 'class': 'th' }, [ _('Rule') ])
+                               E('th', { 'class': 'th' }, [ _('Rule') ]),
                        ])
                ]);
 
-               cbi_update_table(neigh4tbl, this.parseNeigh(ip4neigh, macdata, networks, false),
+               cbi_update_table(neigh4tbl, this.parseNeighbs(ip4neigh, macdata, networks, false),
                        E('em', _('No entries available'))
                );
-               cbi_update_table(route4tbl, this.parseRoute(ip4route, macdata, networks, false),
+               cbi_update_table(route4tbl, this.parseRoutes(ip4route, macdata, networks, false),
                        E('em', _('No entries available'))
                );
-               cbi_update_table(rule4tbl, this.parseRule(ip4rule, networks, false),
+               cbi_update_table(rule4tbl, this.parseRules(ip4rule),
                        E('em', _('No entries available'))
                );
-               cbi_update_table(neigh6tbl, this.parseNeigh(ip6neigh, macdata, networks, true),
+               cbi_update_table(neigh6tbl, this.parseNeighbs(ip6neigh, macdata, networks, true),
                        E('em', _('No entries available'))
                );
-               cbi_update_table(route6tbl, this.parseRoute(ip6route, macdata, networks, true),
+               cbi_update_table(route6tbl, this.parseRoutes(ip6route, macdata, networks, true),
                        E('em', _('No entries available'))
                );
-               cbi_update_table(rule6tbl, this.parseRule(ip6rule, networks, false),
+               cbi_update_table(rule6tbl, this.parseRules(ip6rule),
                        E('em', _('No entries available'))
                );
 
-               var view = E([], [
+               const view = E([], [
                        E('h2', {}, [ _('Routing') ]),
                        E('p', {}, [ _('The following rules are currently active on this system.') ]),
                        E('div', {}, [
@@ -295,7 +264,7 @@ return view.extend({
                                        route4tbl,
 
                                        E('h3', {}, [ _('Active IPv4 Rules') ]),
-                                       rule4tbl
+                                       rule4tbl,
                                ]),
                                E('div', { 'class': 'cbi-section', 'data-tab': 'ipv6routing', 'data-tab-title': _('IPv6 Routing') }, [
                                        E('h3', {}, [ _('IPv6 Neighbours') ]),
@@ -305,7 +274,7 @@ return view.extend({
                                        route6tbl,
 
                                        E('h3', {}, [ _('Active IPv6 Rules') ]),
-                                       rule6tbl
+                                       rule6tbl,
                                ])
                        ])
                ]);
diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routesj.js b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routesj.js
new file mode 100644 (file)
index 0000000..1f70caf
--- /dev/null
@@ -0,0 +1,344 @@
+'use strict';
+'require fs';
+'require rpc';
+'require tools.network as tn';
+'require ui';
+'require validation';
+'require view';
+
+const callNetworkInterfaceDump = rpc.declare({
+       object: 'network.interface',
+       method: 'dump',
+       expect: { interface: [] }
+});
+
+const checkUfpInstalled = rpc.declare({
+       object: 'file',
+       method: 'stat',
+       params: [ 'path' ]
+});
+
+const callUfpList = rpc.declare({
+       object: 'fingerprint',
+       method: 'fingerprint',
+       expect: { '': {} }
+});
+
+function applyMask(addr, mask, v6) {
+       const words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr);
+       const bword = v6 ? 0xffff : 0xff;
+       const bwlen = v6 ? 16 : 8;
+
+       if (!words || mask < 0 || mask > (v6 ? 128 : 32))
+               return null;
+
+       for (let i = 0; i < words.length; i++) {
+               const b = Math.min(mask, bwlen);
+               words[i] &= (bword << (bwlen - b)) & bword;
+               mask -= b;
+       }
+
+       return String.prototype.format.apply(
+               v6 ? '%x:%x:%x:%x:%x:%x:%x:%x' : '%d.%d.%d.%d', words);
+}
+
+return view.extend({
+       load() {
+               return Promise.all([
+                       checkUfpInstalled('/usr/sbin/ufpd')
+               ]).then(([ufpcheck]) => {
+                       return Promise.all([
+                               callNetworkInterfaceDump(),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-4', '-j', 'neigh', 'show' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-4', '-j', 'route', 'show', 'table', 'all' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-4', '-j', 'rule', 'show' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-6', '-j', 'neigh', 'show' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-6', '-j', 'route', 'show', 'table', 'all' ]), {}),
+                               L.resolveDefault(fs.exec('/sbin/ip', [ '-6', '-j', 'rule', 'show' ]), {}),
+                               ufpcheck?.type === 'file' ? callUfpList() : null,
+                       ]);
+               });
+       },
+
+       getNetworkByDevice(networks, dev, addr, mask, v6) {
+               const addr_arrays = [ 'ipv4-address', 'ipv6-address', 'ipv6-prefix', 'ipv6-prefix-assignment', 'route' ];
+               let matching_iface = null;
+               let matching_prefix = -1;
+
+               for (const net of networks) {
+                       if (!L.isObject(net) || (net.l3_device !== dev && net.device !== dev))
+                               continue;
+
+                       for (const key of addr_arrays) {
+                               const list = net[key];
+                               if (!Array.isArray(list)) continue;
+
+                               for (const { address, target, mask: cmp_mask } of list) {
+                                       const cmp_addr = address || target;
+                                       if (!cmp_addr) continue;
+
+                                       if (applyMask(cmp_addr, cmp_mask, v6) !== applyMask(addr, cmp_mask, v6) || mask < cmp_mask)
+                                               continue;
+
+                                       if (cmp_mask > matching_prefix) {
+                                               matching_iface = net.interface;
+                                               matching_prefix = cmp_mask;
+                                       }
+                               }
+                       }
+               }
+
+               return matching_iface;
+       },
+
+       parseJSON(string) {
+               try {
+                       return JSON.parse(string);
+               } catch (e) {
+                       return [];
+               }
+       },
+
+       parseNeighbs(nbs, macs, networks, v6) {
+               const res = [];
+
+               for (const n of this.parseJSON(nbs)) {
+                       let vendor;
+                       if (n.dst.match(/^fe[89a-f][0-9a-f]:/))
+                               continue;
+
+                       if (n.state.find(f => {return f == 'FAILED'}))
+                               continue;
+
+                       for (let mac in macs) {
+                               if (n?.lladdr === mac)
+                                       vendor = macs[mac].vendor;
+                       }
+
+                       const net = this.getNetworkByDevice(networks, n?.dev, n?.dst, v6 ? 128 : 32, v6);
+
+                       res.push([
+                               E('div', { 'data-tooltip': JSON.stringify(n) }, [
+                                       '#',
+                                       n?.nud ? `; ${_('NUD')}: ${n?.nud}` : '',
+                                       n?.proxy === null ? `; ${_('Proxy')}: ✅` : '',
+                                       n?.nomaster === null ? `;  ${_('No master')} : ✅` : '',
+                                       n?.vrf ? `; ${_('VRF')}: ${n?.vrf}` : '',
+                               ]),
+
+                               n?.dst,
+                               n?.lladdr?.toUpperCase() + (vendor ? ` (${vendor})` : ''),
+                               E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(n?.dev) ]),
+                       ]);
+               }
+
+               return res;
+       },
+
+       parseRoutes(routes, macs, networks, v6) {
+               const res = [];
+
+               for (const rt of this.parseJSON(routes)) {
+                       const dest = rt.dst == 'default' ? (v6 ? '::/0' : '0.0.0.0/0') : rt.dst;
+                       if (dest == 'fe80::/64' || dest == 'ff00::/8')
+                               continue;
+
+                       const [addr, bits = (v6 ? 128 : 32)] = dest.split('/');
+                       const net = this.getNetworkByDevice(networks, rt.dev, addr, bits, v6);
+
+                       res.push([
+                               E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(rt.dev) ]),
+                               dest,
+                               rt?.gateway || '-',
+                               rt?.src || rt?.from || '-',
+                               String(rt?.metric || '-'),
+                               rt?.table || 'main',
+                               rt?.protocol,
+                       ]);
+               }
+
+               return res;
+       },
+
+       parseRules(rules) {
+               const r = [];
+               for (const rl of this.parseJSON(rules)) {
+                       r.push([
+                               E('div', { 'data-tooltip': JSON.stringify(rl) }, [
+                                       '#',
+                                       rl?.not === null ? `; ${_('Not')}: ✅` : '',
+                                       rl?.nop === null ? `; ${_('No-op')}: ✅` : '',
+                                       rl?.l3mdev === null ? `; ${_('L3Mdev')}: ✅` : '',
+                                       rl?.fwmark ? `; ${_('Fwmark')}:${rl?.fwmark}` : '',
+                                       rl?.from ? `; ${_('From')}:${rl?.from}` : '',
+                                       rl?.to ? `; ${_('To')}:${rl?.to}` : '',
+                                       rl?.tos ? `; ${_('ToS')}:${rl?.tos}` : '',
+                                       rl?.dsfield ? `; ${_('DSCP')}:${rl?.dsfield}` : '',
+                                       rl?.uidrange ? `; ${_('UID-range')}:${rl?.uidrange}` : '',
+                                       rl?.goto ? `; ${_('goto')}:${rl?.goto}` : '',
+                                       rl?.nat ? `; ${_('NAT')}:${rl?.nat}` : '',
+                               ]),
+
+                               rl?.priority,
+                               rl?.iif ? E('span', { 'class': 'ifacebadge' }, [ rl?.iif ]) : '-',
+                               rl?.src ? (rl?.srclen ? rl?.src + '/' + rl?.srclen : rl?.src) : _('any'),
+                               rl?.sport || '-',
+                               rl?.action || '-',
+                               tn.protocols.find(f => {return f.i == rl?.ipproto?.split?.('-')[1] })?.d || '-',
+                               rl?.oif ? E('span', { 'class': 'ifacebadge' }, [ rl?.oif ]) : '-',
+                               rl?.dst ? (rl?.dstlen ? rl?.dst + '/' + rl?.dstlen : rl?.dst) : _('any'),
+                               rl?.dport || '-',
+                               rl?.table || '-',
+                       ]);
+               }
+               return r;
+       },
+
+       render([
+               networks,
+               { stdout: ip4neighbs = '' } = {},
+               { stdout: ip4routes = '' } = {},
+               { stdout: ip4rules = '' } = {},
+               { stdout: ip6neighbs = '' } = {},
+               { stdout: ip6routes = '' } = {},
+               { stdout: ip6rules = '' } = {},
+               macdata,
+       ]) {
+
+               const device_title = _('Which is used to access this %s').format(_('Target'));
+               const target_title = _('Network and its mask that define the size of the destination');
+               const gateway_title = _('The address through which this %s is reachable').format(_('Target'));
+               const metric_title = _('Quantifies the cost or distance to a destination in a way that allows routers to make informed decisions about the optimal path to forward data packets');
+               const table_title = _('Common name or numeric ID of the %s in which this route is found').format(_('Table'));
+               const proto_title = _('The routing protocol identifier of this route');
+               const source_title = _('Network and its mask that define which source addresses use this route');
+
+               const neigh4tbl = E('table', { 'class': 'table' }, [
+                       E('tr', { 'class': 'tr table-titles' }, [
+                               E('th', { 'class': 'th' }, [ _('Entry') ]),
+                               E('th', { 'class': 'th' }, [ _('IP address') ]),
+                               E('th', { 'class': 'th' }, [ _('MAC address') ]),
+                               E('th', { 'class': 'th' }, [ _('Interface') ]),
+                       ])
+               ]);
+
+               const route4tbl = E('table', { 'class': 'table' }, [
+                       E('tr', { 'class': 'tr table-titles' }, [
+                               E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
+                               E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
+                               E('th', { 'class': 'th', 'title': gateway_title }, [ _('Gateway') ]),
+                               E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
+                               E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
+                               E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
+                               E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
+                       ])
+               ]);
+
+               const rule4tbl = E('table', { 'class': 'table' }, [
+                       E('tr', { 'class': 'tr table-titles' }, [
+                               E('th', { 'class': 'th' }, [ _('Rule') ]),
+                               E('th', { 'class': 'th' }, [ _('Priority') ]),
+                               E('th', { 'class': 'th' }, [ _('Ingress') ]),
+                               E('th', { 'class': 'th' }, [ _('Source') ]),
+                               E('th', { 'class': 'th' }, [ _('Src Port') ]),
+                               E('th', { 'class': 'th' }, [ _('Action') ]),
+                               E('th', { 'class': 'th' }, [ _('IP Protocol') ]),
+                               E('th', { 'class': 'th' }, [ _('Egress') ]),
+                               E('th', { 'class': 'th' }, [ _('Destination') ]),
+                               E('th', { 'class': 'th' }, [ _('Dest Port') ]),
+                               E('th', { 'class': 'th' }, [ _('Table') ]),
+                       ])
+               ]);
+
+               const neigh6tbl = E('table', { 'class': 'table' }, [
+                       E('tr', { 'class': 'tr table-titles' }, [
+                               E('th', { 'class': 'th' }, [ _('Entry') ]),
+                               E('th', { 'class': 'th' }, [ _('IP address') ]),
+                               E('th', { 'class': 'th' }, [ _('MAC address') ]),
+                               E('th', { 'class': 'th' }, [ _('Interface') ]),
+                       ])
+               ]);
+
+               const route6tbl = E('table', { 'class': 'table' }, [
+                       E('tr', { 'class': 'tr table-titles' }, [
+                               E('th', { 'class': 'th', 'title': device_title }, [ _('Device') ]),
+                               E('th', { 'class': 'th', 'title': target_title }, [ _('Target') ]),
+                               E('th', { 'class': 'th', 'title': gateway_title }, [ _('Gateway') ]),
+                               E('th', { 'class': 'th', 'title': source_title }, [ _('Source') ]),
+                               E('th', { 'class': 'th', 'title': metric_title }, [ _('Metric') ]),
+                               E('th', { 'class': 'th', 'title': table_title }, [ _('Table') ]),
+                               E('th', { 'class': 'th', 'title': proto_title }, [ _('Protocol') ]),
+                       ])
+               ]);
+
+               const rule6tbl = E('table', { 'class': 'table' }, [
+                       E('tr', { 'class': 'tr table-titles' }, [
+                               E('th', { 'class': 'th' }, [ _('Rule') ]),
+                               E('th', { 'class': 'th' }, [ _('Priority') ]),
+                               E('th', { 'class': 'th' }, [ _('Ingress') ]),
+                               E('th', { 'class': 'th' }, [ _('Source') ]),
+                               E('th', { 'class': 'th' }, [ _('Src Port') ]),
+                               E('th', { 'class': 'th' }, [ _('Action') ]),
+                               E('th', { 'class': 'th' }, [ _('IP Protocol') ]),
+                               E('th', { 'class': 'th' }, [ _('Egress') ]),
+                               E('th', { 'class': 'th' }, [ _('Destination') ]),
+                               E('th', { 'class': 'th' }, [ _('Dest Port') ]),
+                               E('th', { 'class': 'th' }, [ _('Table') ]),
+                       ])
+               ]);
+
+               cbi_update_table(neigh4tbl, this.parseNeighbs(ip4neighbs, macdata, networks, false),
+                       E('em', _('No entries available'))
+               );
+               cbi_update_table(route4tbl, this.parseRoutes(ip4routes, macdata, networks, false),
+                       E('em', _('No entries available'))
+               );
+               cbi_update_table(rule4tbl, this.parseRules(ip4rules),
+                       E('em', _('No entries available'))
+               );
+               cbi_update_table(neigh6tbl, this.parseNeighbs(ip6neighbs, macdata, networks, true),
+                       E('em', _('No entries available'))
+               );
+               cbi_update_table(route6tbl, this.parseRoutes(ip6routes, macdata, networks, true),
+                       E('em', _('No entries available'))
+               );
+               cbi_update_table(rule6tbl, this.parseRules(ip6rules),
+                       E('em', _('No entries available'))
+               );
+
+               const view = E([], [
+                       E('h2', {}, [ _('Routing') ]),
+                       E('p', {}, [ _('The following rules are currently active on this system.') ]),
+                       E('div', {}, [
+                               E('div', { 'class': 'cbi-section', 'data-tab': 'ipv4routing', 'data-tab-title': _('IPv4 Routing') }, [
+                                       E('h3', {}, [ _('IPv4 Neighbours') ]),
+                                       neigh4tbl,
+
+                                       E('h3', {}, [ _('Active IPv4 Routes') ]),
+                                       route4tbl,
+
+                                       E('h3', {}, [ _('Active IPv4 Rules') ]),
+                                       rule4tbl,
+                               ]),
+                               E('div', { 'class': 'cbi-section', 'data-tab': 'ipv6routing', 'data-tab-title': _('IPv6 Routing') }, [
+                                       E('h3', {}, [ _('IPv6 Neighbours') ]),
+                                       neigh6tbl,
+
+                                       E('h3', {}, [ _('Active IPv6 Routes') ]),
+                                       route6tbl,
+
+                                       E('h3', {}, [ _('Active IPv6 Rules') ]),
+                                       rule6tbl,
+                               ])
+                       ])
+               ]);
+
+               ui.tabs.initTabGroup(view.lastElementChild.childNodes);
+
+               return view;
+       },
+
+       handleSaveApply: null,
+       handleSave: null,
+       handleReset: null
+});
index 79101e9bdf5facf322412c799450e841b0e06ec0..d1300e36e01224108b1616aa1ea50d80342c5294 100644 (file)
                        "path": "status/routes"
                },
                "depends": {
-                       "acl": [ "luci-mod-status-routes" ]
+                       "acl": [ "luci-mod-status-routes" ],
+                       "fs": [
+                               { "/usr/libexec/ip-tiny": "absent" },
+                               { "/usr/libexec/ip-full": "absent" }
+                       ]
+
+               }
+       },
+
+       "admin/status/routes": {
+               "title": "Routing",
+               "order": 2,
+               "action": {
+                       "type": "view",
+                       "path": "status/routesj"
+               },
+               "depends": {
+                       "acl": [ "luci-mod-status-routes-json" ],
+                       "fs": [
+                               { "/usr/libexec/ip-tiny": "executable" },
+                               { "/usr/libexec/ip-full": "executable" }
+                       ]
+
                }
        },
 
index 8c7dbf143d3008db265691f3dfd26848c9aa8cbd..729cb177e34eb4147fda39ba382d5cdab661c4f4 100644 (file)
                }
        },
 
+       "luci-mod-status-routes-json": {
+               "description": "Grant access to routing status",
+               "read": {
+                       "file": {
+                               "/sbin/ip -[46] -j neigh show": [ "exec" ],
+                               "/sbin/ip -[46] -j route show table all": [ "exec" ],
+                               "/sbin/ip -[46] -j rule show": [ "exec" ]
+                       },
+                       "ubus": {
+                               "file": [ "exec", "stat" ],
+                               "fingerprint": [ "fingerprint" ]
+                       }
+               }
+       },
+
        "luci-mod-status-channel_analysis": {
                "description": "Grant access to wireless channel status",
                "read": {