From cbaeb5e300ef91a35aef5c9591908f08674af8c3 Mon Sep 17 00:00:00 2001 From: Paul Donald Date: Fri, 3 Oct 2025 23:49:54 +0200 Subject: [PATCH] luci-mod-status: handle vmaps in nftables - Support vmaps - Support goto, continue actions - Recognize DSCP and MAC address expressions - Support log action without prefix. Signed-off-by: Paul Donald --- .../resources/view/status/nftables.js | 87 ++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/nftables.js b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/nftables.js index 0013a3a116..4feac4690c 100644 --- a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/nftables.js +++ b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/nftables.js @@ -33,6 +33,12 @@ var expr_translations = { 'ct.status': _('Conntrack status', 'nft ct status'), 'ct.status.dnat': 'DNAT', + 'ip.dscp': 'DSCP', + 'ip6.dscp': 'DSCP', + + 'ether.saddr': _('Source MAC address', 'nft ether saddr'), + 'ether.daddr': _('Destination MAC address', 'nft ether daddr'), + 'ip.protocol': _('IP protocol', 'nft ip protocol'), 'ip.protocol.tcp': 'TCP', 'ip.protocol.udp': 'UDP', @@ -96,7 +102,10 @@ var action_translations = { 'notrack': _('Do not track', 'nft notrack action'), 'drop': _('Drop packet', 'nft drop action'), 'jump': _('Continue in %h', 'nft jump action'), - 'log': _('Log event "%h…"', 'nft log action'), + 'goto': _('Goto chain %h', 'nft goto action'), + 'continue': _('Continue to next rule', 'nft continue action'), + 'log': _('Log event', 'nft log action'), + 'log.prefix': _('Log event "%h…"', 'nft log action'), 'reject.tcp reset': _('Reject packet with TCP reset', 'nft reject with tcp reset'), 'reject.icmp': _('Reject IPv4 packet with ICMP type %h', 'nft reject with icmp type'), @@ -150,6 +159,8 @@ return view.extend({ case 'reject': case 'drop': case 'jump': + case 'goto': + case 'continue': case 'snat': case 'dnat': case 'redirect': @@ -287,6 +298,47 @@ return view.extend({ } }, + renderVMap: function(spec, table) { + // spec: { key: {...}, data: { set: [ [mapkey, actionSpec], ... ] } } + const matchElem = E('span', { 'class': 'ifacebadge' }, + _('Verdict map: %h is').format(this.exprToString(spec.key))); + + const actions = []; + const keys = []; + + if (spec && spec.data && Array.isArray(spec.data.set)) { + // For each mapping entry, render key and the action + for (var i = 0; i < spec.data.set.length; i++) { + const entry = spec.data.set[i]; + const mapKey = entry[0]; + const actionSpec = entry[1]; + + // if actionSpec is an action (accept/drop/jump/...), render it as action element + if (typeof(actionSpec) == 'object' && this.isActionExpression(actionSpec)) { + // renderExpr expects an expr object like { accept: null } or { jump: {...} } + const renderedAction = this.renderExpr(actionSpec, table); + + actions.push(E('div', { }, [ + E('span', [ this.exprToString(mapKey) + ':' ]), + ' ', + renderedAction + ])); + keys.push('%s'.format(this.exprToString(mapKey))); + } else { + // fallback: render the actionSpec as generic JSON tooltip + actions.push(E('span', { 'class': 'ifacebadge', 'data-tooltip': JSON.stringify(actionSpec) }, + this.exprToString(mapKey) + ':' + this.exprToString(actionSpec))); + keys.push('%s'.format(this.exprToString(mapKey))); + } + + } + + dom.append(matchElem, [ ' ', E('span', {}, keys.join(_(' or '))) ]); + } + + return { match: matchElem, actions: actions }; + }, + renderMatchExpr: function(spec) { switch (spec.op) { case '==': @@ -374,6 +426,16 @@ return view.extend({ 'class': 'ifacebadge' }, action_translations.jump.format(table, spec.target, spec.target)); + case 'goto': + return E('span', { + 'class': 'ifacebadge' + }, action_translations.goto.format(table, spec.target, spec.target)); + + case 'continue': + return E('span', { + 'class': 'ifacebadge' + }, action_translations.continue); + case 'return': return E('span', { 'class': 'ifacebadge' @@ -452,7 +514,9 @@ return view.extend({ case 'log': return E('span', { 'class': 'ifacebadge' - }, action_translations.log.format(spec.prefix)); + }, spec?.prefix ? + ''.format.apply(action_translations['log.prefix'], [spec?.prefix]) + : action_translations.log); default: return E('span', { @@ -494,6 +558,25 @@ return view.extend({ if (typeof(spec.expr[i]) == 'string' && spec.expr[i].match(/^flow add (@\S+)$/)) spec.expr[i] = { flow: { op: "add", flowtable: RegExp.$1 } }; + // vmap special handling + if (spec.expr[i] && spec.expr[i].vmap) { + var vm = this.renderVMap(spec.expr[i].vmap, spec.table); + + // add match summary to left column + dom.append(row.childNodes[0], [ vm.match ]); + empty = false; + + if (typeof(spec.expr[i]) == 'object' && spec.expr[i].counter) { + row.childNodes[0].appendChild( + this.renderCounter(spec.expr[i].counter)); + } + + // append each mapped action to the actions column + for (var ai = 0; ai < vm.actions.length; ai++) + dom.append(row.childNodes[1], [ vm.actions[ai] ]); + continue; + } + var res = this.renderExpr(spec.expr[i], spec.table); if (typeof(spec.expr[i]) == 'object' && spec.expr[i].counter) { -- 2.30.2