From 89ae891462ea9928fc2b01e25c1611b1b71352d5 Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Sun, 12 Oct 2025 18:37:57 +0200 Subject: [PATCH] luci-mod-status: routes; refactor and add routesj page for ip -j output 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 --- .../resources/view/status/routes.js | 215 +++++------ .../resources/view/status/routesj.js | 344 ++++++++++++++++++ .../share/luci/menu.d/luci-mod-status.json | 24 +- .../usr/share/rpcd/acl.d/luci-mod-status.json | 15 + 4 files changed, 474 insertions(+), 124 deletions(-) create mode 100644 modules/luci-mod-status/htdocs/luci-static/resources/view/status/routesj.js 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 index 59ce7b7cd9..e48c1d6534 100644 --- 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 @@ -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 index 0000000000..1f70caf4ad --- /dev/null +++ b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routesj.js @@ -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 +}); diff --git a/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json b/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json index 79101e9bdf..d1300e36e0 100644 --- a/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json +++ b/modules/luci-mod-status/root/usr/share/luci/menu.d/luci-mod-status.json @@ -19,7 +19,29 @@ "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" } + ] + } }, diff --git a/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json b/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json index 8c7dbf143d..729cb177e3 100644 --- a/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json +++ b/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json @@ -55,6 +55,21 @@ } }, + "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": { -- 2.30.2