'use strict';'require view';'require poll';'require fs';'require ui';'require dom';var expr_translations={'meta.iifname':_('Ingress device name','nft meta iifname'),'meta.oifname':_('Egress device name','nft meta oifname'),'meta.iif':_('Ingress device id','nft meta iif'),'meta.oif':_('Egress device id','nft meta oif'),'meta.l4proto':_('IP protocol','nft meta l4proto'),'meta.l4proto.tcp':'TCP','meta.l4proto.udp':'UDP','meta.l4proto.icmp':'ICMP','meta.l4proto.icmpv6':'ICMPv6','meta.l4proto.ipv6-icmp':'ICMPv6','meta.nfproto':_('Address family','nft meta nfproto'),'meta.nfproto.ipv4':'IPv4','meta.nfproto.ipv6':'IPv6','meta.mark':_('Packet mark','nft meta mark'),'meta.time':_('Packet receive time','nft meta time'),'meta.hour':_('Current time','nft meta hour'),'meta.day':_('Current weekday','nft meta day'),'ct.state':_('Conntrack state','nft ct state'),'ct.status':_('Conntrack status','nft ct status'),'ct.status.dnat':'DNAT','ip.protocol':_('IP protocol','nft ip protocol'),'ip.protocol.tcp':'TCP','ip.protocol.udp':'UDP','ip.protocol.icmp':'ICMP','ip.protocol.icmpv6':'ICMPv6','ip.protocol.ipv6-icmp':'ICMPv6','ip.saddr':_('Source IP','nft ip saddr'),'ip.daddr':_('Destination IP','nft ip daddr'),'ip.sport':_('Source port','nft ip sport'),'ip.dport':_('Destination port','nft ip dport'),'ip6.saddr':_('Source IPv6','nft ip6 saddr'),'ip6.daddr':_('Destination IPv6','nft ip6 daddr'),'icmp.code':_('ICMP code','nft icmp code'),'icmp.type':_('ICMP type','nft icmp type'),'icmpv6.code':_('ICMPv6 code','nft icmpv6 code'),'icmpv6.type':_('ICMPv6 type','nft icmpv6 type'),'tcp.sport':_('TCP source port','nft tcp sport'),'tcp.dport':_('TCP destination port','nft tcp dport'),'udp.sport':_('UDP source port','nft udp sport'),'udp.dport':_('UDP destination port','nft udp dport'),'tcp.flags':_('TCP flags','nft tcp flags'),'th.sport':_('Transport header source port','nft th sport'),'th.dport':_('Transport header destination port','nft th dport'),'natflag.random':_('Randomize source port mapping','nft nat flag random'),'natflag.fully-random':_('Full port randomization','nft nat flag fully-random'),'natflag.persistent':_('Use same source and destination for each connection','nft nat flag persistent'),'rt.mtu':_('Effective route MTU','nft rt mtu'),'tcpoption.maxseg.size':_('TCP MSS','nft tcp option maxseg size'),'unit.packets':_('packets','nft unit'),'unit.mbytes':_('MiB','nft unit'),'unit.kbytes':_('KiB','nft unit'),'unit.week':_('week','nft unit'),'unit.day':_('day','nft unit'),'unit.hour':_('hour','nft unit'),'unit.minute':_('minute','nft unit'),'payload.ll':_('Link layer header bits %d-%d','nft @ll,off,len'),'payload.nh':_('Network header bits %d-%d','nft @nh,off,len'),'payload.th':_('Transport header bits %d-%d','nft @th,off,len')};var op_translations={'==':_('<var>%s</var> is <strong>%s</strong>','nft relational "==" operator expression'),'!=':_('<var>%s</var> not <strong>%s</strong>','nft relational "!=" operator expression'),'>=':_('<var>%s</var> greater than or equal to <strong>%s</strong>','nft relational ">=" operator expression'),'<=':_('<var>%s</var> lower than or equal to <strong>%s</strong>','nft relational "<=" operator expression'),'>':_('<var>%s</var> greater than <strong>%s</strong>','nft relational ">" operator expression'),'<':_('<var>%s</var> lower than <strong>%s</strong>','nft relational "<" operator expression'),'in':_('<var>%s</var> is one of <strong>%s</strong>','nft relational "in" operator expression'),'in_set':_('<var>%s</var> in set <strong>%s</strong>','nft set match expression'),'not_in_set':_('<var>%s</var> not in set <strong>%s</strong>','nft not in set match expression'),};var action_translations={'accept':_('Accept packet','nft accept action'),'drop':_('Drop packet','nft drop action'),'jump':_('Continue in <strong><a href="#%q.%q">%h</a></strong>','nft jump action'),'log':_('Log event "<strong>%h</strong>…"','nft log action'),'reject.tcp reset':_('Reject packet with <strong>TCP reset</strong>','nft reject with tcp reset'),'reject.icmp':_('Reject IPv4 packet with <strong>ICMP type %h</strong>','nft reject with icmp type'),'reject.icmpv6':_('Reject packet with <strong>ICMPv6 type %h</strong>','nft reject with icmpv6 type'),'reject.icmpx':_('Reject packet with <strong>ICMP type %h</strong>','nft reject with icmpx type'),'snat.ip.addr':_('Rewrite source to <strong>%h</strong>','nft snat ip to addr'),'snat.ip.addr.port':_('Rewrite source to <strong>%h</strong>, port <strong>%h</strong>','nft snat ip to addr:port'),'snat.ip6.addr':_('Rewrite source to <strong>%h</strong>','nft snat ip6 to addr'),'snat.ip6.addr.port':_('Rewrite source to <strong>%h</strong>, port <strong>%h</strong>','nft snat ip6 to addr:port'),'dnat.ip.addr':_('Rewrite destination to <strong>%h</strong>','nft dnat ip to addr'),'dnat.ip.addr.port':_('Rewrite destination to <strong>%h</strong>, port <strong>%h</strong>','nft dnat ip to addr:port'),'dnat.ip6.addr':_('Rewrite destination to <strong>%h</strong>','nft dnat ip6 to addr'),'dnat.ip6.addr.port':_('Rewrite destination to <strong>%h</strong>, port <strong>%h</strong>','nft dnat ip6 to addr:port'),'redirect':_('Redirect to local system','nft redirect'),'redirect.port':_('Redirect to local port <strong>%h</strong>','nft redirect to port'),'masquerade':_('Rewrite to egress device address'),'mangle':_('Set header field <var>%s</var> to <strong>%s</strong>','nft mangle'),'limit':_('At most <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),'limit.burst':_('At most <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),'limit.inv':_('At least <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),'limit.inv.burst':_('At least <strong>%h</strong> per <strong>%h</strong>, burst of <strong>%h</strong>'),'return':_('Continue in calling chain'),'flow':_('Utilize flow table <strong>%h</strong>')};return view.extend({load:function(){return Promise.all([L.resolveDefault(fs.exec_direct('/usr/sbin/nft',['--terse','--json','list','ruleset'],'json'),{}),L.resolveDefault(fs.exec_direct('/usr/sbin/iptables-save'),''),L.resolveDefault(fs.exec_direct('/usr/sbin/ip6tables-save'),'')]);},isActionExpression:function(expr){for(var k in expr){if(expr.hasOwnProperty(k)){switch(k){case'accept':case'reject':case'drop':case'jump':case'snat':case'dnat':case'redirect':case'mangle':case'masquerade':case'return':case'flow':case'log':return true;}}}
return false;},exprToKey:function(expr){var kind,spec;if(!Array.isArray(expr)&&typeof(expr)=='object'){for(var k in expr){if(expr.hasOwnProperty(k)){kind=k;spec=expr[k];break;}}}
switch(kind||'-'){case'meta':case'ct':case'rt':return'%h.%h'.format(kind,spec.key);case'tcp option':return'tcpoption.%h.%h'.format(spec.name,spec.field);case'reject':return'reject.%h'.format(spec.type);}
return null;},exprToString:function(expr,hint){var kind,spec;if(typeof(expr)!='object'){var s;if(hint)
s=expr_translations['%s.%h'.format(hint,expr)];return s||'%h'.format(expr);}
if(Array.isArray(expr)){kind='list';spec=expr;}
else{for(var k in expr){if(expr.hasOwnProperty(k)){kind=k;spec=expr[k];}}}
if(!kind)
return'';switch(kind){case'prefix':return'%h/%d'.format(spec.addr,spec.len);case'set':case'list':var items=[],lis=[];for(var i=0;i<spec.length;i++){items.push('<span class="nft-set-item">%s</span>'.format(this.exprToString(spec[i])));lis.push('<span class="ifacebadge">%s</span>'.format(this.exprToString(spec[i])));}
var tpl;if(kind=='set')
tpl='<div class="nft-set cbi-tooltip-container">{ <span class="nft-set-items">%s</span> }<div class="cbi-tooltip">%s</div></div>';else
tpl='<div class="nft-list cbi-tooltip-container"><span class="nft-list-items">%s</span><div class="cbi-tooltip">%s</div></div>';return tpl.format(items.join(', '),lis.join('<br />'));case'concat':var items=[];for(var i=0;i<spec.length;i++)
items.push(this.exprToString(spec[i]));return items.join('+');case'range':return'%s-%s'.format(this.exprToString(spec[0],hint),this.exprToString(spec[1],hint));case'payload':if(spec.protocol&&spec.field){var k='%h.%h'.format(spec.protocol,spec.field);return expr_translations[k]||'<em>%s</em>'.format(k);}
else if(spec.base&&spec.offset!=null&&spec.len!=null){var k='payload.%h'.format(spec.base);return(expr_translations[k]||'<em>@%s,%%d,%%d</em>'.format(spec.base)).format(spec.offset+1,spec.offset+spec.len+1);}
return'payload: %s'.format(kind,JSON.stringify(spec));case'&':case'|':case'^':return'%s %h %s'.format(this.exprToString(spec[0],hint),kind,Array.isArray(spec[1])?'(%h)'.format(spec[1].join('|')):this.exprToString(spec[1],hint));default:var k=this.exprToKey(expr);if(k)
return expr_translations[k]||'<em>%s</em>'.format(k);return'%s: %s'.format(kind,JSON.stringify(spec));}},renderMatchExpr:function(spec){switch(spec.op){case'==':case'!=':if((typeof(spec.right)=='object'&&spec.right.set)||(typeof(spec.right)=='string'&&spec.right.charAt(0)=='@'))
spec.op=(spec.op=='==')?'in_set':'not_in_set';break;case'in':if(typeof(spec.right)!='object')
spec.op='==';break;}
return E('span',{'class':'ifacebadge'},(op_translations[spec.op]||'<var>%%s</var> %h <strong>%%s</strong>'.format(spec.op)).format(this.exprToString(spec.left),this.exprToString(spec.right,this.exprToKey(spec.left))));},renderNatFlags:function(spec){var f=[];if(spec&&Array.isArray(spec.flags)){for(var i=0;i<spec.flags.length;i++)
f.push(expr_translations['natflag.%h'.format(spec.flags[i])]||spec.flags[i]);}
return f.length?E('small',{'class':'cbi-tooltip-container'},[' (',N_(f.length,'1 flag','%d flags','nft amount of flags').format(f.length),')',E('span',{'class':'cbi-tooltip'},f.join('<br />'))]):E([]);},renderRateUnit:function(value,unit){if(!unit)
unit='packets';return'%d\xa0%s'.format(value,expr_translations['unit.%h'.format(unit)]||unit);},renderExpr:function(expr,table){var kind,spec;for(var k in expr){if(expr.hasOwnProperty(k)){kind=k;spec=expr[k];}}
if(!kind)
return E([]);switch(kind){case'match':return this.renderMatchExpr(spec);case'reject':var k='reject.%s'.format(spec.type);return E('span',{'class':'ifacebadge'},(action_translations[k]||k).format(this.exprToString(spec.expr)));case'accept':case'drop':return E('span',{'class':'ifacebadge'},action_translations[kind]||'<em>%h</em>'.format(kind));case'jump':return E('span',{'class':'ifacebadge'},action_translations.jump.format(table,spec.target,spec.target));case'return':return E('span',{'class':'ifacebadge'},action_translations.return);case'snat':case'dnat':var k='%h.%h'.format(kind,spec.family),a=[];if(spec.addr){k+='.addr';a.push(this.exprToString(spec.addr));}
if(spec.port){k+='.port';a.push(this.exprToString(spec.port));}
return E('span',{'class':'ifacebadge'},[E('span',''.format.apply(action_translations[k]||k,a)),this.renderNatFlags(spec)]);case'redirect':var k='redirect',a=[];if(spec&&spec.port){k+='.port';a.push(this.exprToString(spec.port));}
return E('span',{'class':'ifacebadge'},[E('span',''.format.apply(action_translations[k]||k,a)),this.renderNatFlags(spec)]);case'masquerade':return E('span',{'class':'ifacebadge'},[E('span',action_translations.masquerade),this.renderNatFlags(spec)]);case'mangle':return E('span',{'class':'ifacebadge'},action_translations.mangle.format(this.exprToString(spec.key),this.exprToString(spec.value)));case'limit':var k='limit';var a=[this.renderRateUnit(spec.rate,spec.rate_unit),expr_translations['unit.%h'.format(spec.per)]||spec.per];if(spec.inv)
k+='.inv';if(spec.burst){k+='.burst';a.push(this.renderRateUnit(spec.burst,spec.burst_unit));}
return E('span',{'class':'ifacebadge','cbi-tooltip':JSON.stringify(spec)},''.format.apply(action_translations[k]||k,a));case'flow':return E('span',{'class':'ifacebadge'},action_translations.flow.format(spec.flowtable.replace(/^@/,'')));case'log':return E('span',{'class':'ifacebadge'},action_translations.log.format(spec.prefix));default:return E('span',{'class':'ifacebadge','data-tooltip':JSON.stringify(spec)},['{ ',E('strong',{},[kind]),' }']);}},renderCounter:function(data){return E('span',{'class':'ifacebadge cbi-tooltip-container nft-counter'},[E('var',['%.1024mB'.format(data.bytes)]),E('div',{'class':'cbi-tooltip'},[_('Traffic matched by rule: %.1000mPackets, %.1024mBytes','nft counter').format(data.packets,data.bytes)])]);},renderComment:function(comment){return E('span',{'class':'ifacebadge cbi-tooltip-container nft-comment'},[E('var',['#']),E('div',{'class':'cbi-tooltip'},[_('Rule comment: %s','nft comment').format(comment.replace(/^!fw4: /,''))])]);},renderRule:function(data,spec){var empty=true;var row=E('tr',{'class':'tr'},[E('td',{'class':'td','style':'width:60%'}),E('td',{'class':'td','style':'width:40%'})]);if(Array.isArray(spec.expr)){for(var i=0;i<spec.expr.length;i++){if(typeof(spec.expr[i])=='string'&&spec.expr[i].match(/^flow add (@\S+)$/))
spec.expr[i]={flow:{op:"add",flowtable:RegExp.$1}};var res=this.renderExpr(spec.expr[i],spec.table);if(typeof(spec.expr[i])=='object'&&spec.expr[i].counter){row.childNodes[0].insertBefore(this.renderCounter(spec.expr[i].counter),row.childNodes[0].firstChild);}
else if(this.isActionExpression(spec.expr[i])){dom.append(row.childNodes[1],[res]);}
else{dom.append(row.childNodes[0],[res]);empty=false;}}}
if(spec.comment){row.childNodes[0].insertBefore(this.renderComment(spec.comment),row.childNodes[0].firstChild);}
if(empty)
dom.append(row.childNodes[0],E('span',{'class':'ifacebadge'},'<em>%h</em>'.format(_('Any packet','nft match any traffic'))));return row;},renderChain:function(data,spec){var title,policy,hook;switch(spec.type){case'filter':title=_('Traffic filter chain "%h"').format(spec.name);break;case'route':title=_('Route action chain "%h"').format(spec.name);break;case'nat':title=_('NAT action chain "%h"').format(spec.name);break;default:title=_('Rule container chain "%h"').format(spec.name);break;}
switch(spec.policy){case'drop':policy=_('Drop unmatched packets','Chain policy: drop');break;default:policy=_('Continue processing unmatched packets','Chain policy: accept');break;}
switch(spec.hook){case'ingress':hook=_('Capture packets directly after the NIC received them','Chain hook: ingress');break;case'prerouting':hook=_('Capture incoming packets before any routing decision','Chain hook: prerouting');break;case'input':hook=_('Capture incoming packets routed to the local system','Chain hook: input');break;case'forward':hook=_('Capture incoming packets addressed to other hosts','Chain hook: forward');break;case'output':hook=_('Capture outgoing packets originating from the local system','Chain hook: output');break;case'postrouting':hook=_('Capture outgoing packets after any routing decision','Chain hook: postrouting');break;default:hook=_('Chain hook "%h"','Yet unknown nftables chain hook').format(spec.hook);break;}
var node=E('div',{'class':'nft-chain'},[E('h4',{'id':'%h.%h'.format(spec.table,spec.name)},[title])]);if(spec.hook){node.appendChild(E('div',{'class':'nft-chain-hook'},[E('ul',{},[E('li',{},_('Hook: <strong>%h</strong> (%h), Priority: <strong>%d</strong>','Chain hook description').format(spec.hook,hook,spec.prio)),E('li',{},_('Policy: <strong>%h</strong> (%h)','Chain hook policy').format(spec.policy,policy))])]));}
node.appendChild(E('table',{'class':'nft-rules table cbi-section-table'},[E('tr',{'class':'tr table-titles'},[E('th',{'class':'th','style':'width:60%'},[_('Rule matches')]),E('th',{'class':'th','style':'width:40%'},[_('Rule actions')])])]));for(var i=0;i<data.length;i++)
if(typeof(data[i].rule)=='object'&&data[i].rule.table==spec.table&&data[i].rule.chain==spec.name)
node.lastElementChild.appendChild(this.renderRule(data,data[i].rule));if(node.lastElementChild.childNodes.length==1)
node.lastElementChild.appendChild(E('tr',{'class':'tr'},[E('td',{'class':'td center','colspan':3},[E('em',[_('No rules in this chain','nft chain is empty')])])]));return node;},renderTable:function(data,spec){var title;switch(spec.family){case'ip':title=_('IPv4 traffic table "%h"').format(spec.name);break;case'ip6':title=_('IPv6 traffic table "%h"').format(spec.name);break;case'inet':title=_('IPv4/IPv6 traffic table "%h"').format(spec.name);break;case'arp':title=_('ARP traffic table "%h"').format(spec.name);break;case'bridge':title=_('Bridge traffic table "%h"').format(spec.name);break;case'netdev':title=_('Network device table "%h"').format(spec.name);break;default:title=_('"%h" table "%h"','Yet unknown nftables table family ("family" table "name")').format(spec.family,spec.name);break;}
var node=E([],[E('style',{'type':'text/css'},['.nft-rules .ifacebadge { margin: .125em }','.nft-rules tr > td { padding: .25em !important }','.nft-set, .nft-list { display: inline-block; vertical-align: middle }','.nft-set-items, .nft-list-items { display: inline-block; vertical-align: middle; max-width: 200px; overflow: hidden; text-overflow: ellipsis }','.ifacebadge.cbi-tooltip-container { cursor: help }','.ifacebadge.cbi-tooltip-container .cbi-tooltip { padding: .5em }']),E('div',{'class':'nft-table'},[E('h3',[title]),E('div',{'class':'nft-chains'})])]);for(var i=0;i<data.length;i++)
if(typeof(data[i].chain)=='object'&&data[i].chain.table==spec.name)
node.lastElementChild.lastElementChild.appendChild(this.renderChain(data,data[i].chain));return node;},checkLegacyRules:function(ipt4save,ipt6save){if(ipt4save.match(/\n-A /)||ipt6save.match(/\n-A /)){ui.addNotification(_('Legacy rules detected'),[E('button',{'class':'btn cbi-button','click':function(){location.href='nftables/iptables'}},_('Open iptables rules overview…'))],'warning');}},render:function(data){var view=E('div'),nft=data[0],ipt=data[1],ipt6=data[2];this.checkLegacyRules(ipt,ipt6);if(!Array.isArray(nft.nftables))
return E('em',_('No nftables ruleset loaded.'));for(var i=0;i<nft.nftables.length;i++)
if(nft.nftables[i].hasOwnProperty('table'))
view.appendChild(this.renderTable(nft.nftables,nft.nftables[i].table));return view;},handleSaveApply:null,handleSave:null,handleReset:null});