From 03a8ea4edb8cb20ad31d31d6cc851e624daa6321 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Sun, 19 Jan 2020 16:08:47 +0100 Subject: [PATCH] luci-app-firewall: rework rule descriptions, deduplicate code Use a simple custom format string DSL to assemble the rule description texts in the overview page. Also move common code for shared, complex cbi options to the firewall tool class. Signed-off-by: Jo-Philipp Wich (backported from commit 7944b0a90bbb0f0f5d1da5eaf5d9906ac546c44b) --- .../luci-static/resources/tools/firewall.js | 564 +++++++++++------- .../resources/view/firewall/forwards.js | 237 ++++---- .../resources/view/firewall/rules.js | 289 ++++----- .../resources/view/firewall/snats.js | 258 +++----- 4 files changed, 676 insertions(+), 672 deletions(-) diff --git a/applications/luci-app-firewall/htdocs/luci-static/resources/tools/firewall.js b/applications/luci-app-firewall/htdocs/luci-static/resources/tools/firewall.js index e983035b3..c60bfd028 100644 --- a/applications/luci-app-firewall/htdocs/luci-static/resources/tools/firewall.js +++ b/applications/luci-app-firewall/htdocs/luci-static/resources/tools/firewall.js @@ -36,10 +36,12 @@ var protocols = [ 'esp', 50, 'IPSEC-ESP', 'ah', 51, 'IPSEC-AH', 'skip', 57, 'SKIP', + 'icmpv6', 58, 'IPv6-ICMP', 'ipv6-icmp', 58, 'IPv6-ICMP', 'ipv6-nonxt', 59, 'IPv6-NoNxt', 'ipv6-opts', 60, 'IPv6-Opts', - 'rspf', 73, 'RSPF', 'CPHB', + 'rspf', 73, 'RSPF', + 'rspf', 73, 'CPHB', 'vmtp', 81, 'VMTP', 'eigrp', 88, 'EIGRP', 'ospf', 89, 'OSPFIGP', @@ -54,6 +56,8 @@ var protocols = [ 'isis', 124, 'ISIS', 'sctp', 132, 'SCTP', 'fc', 133, 'FC', + 'mh', 135, 'Mobility-Header', + 'ipv6-mh', 135, 'Mobility-Header', 'mobility-header', 135, 'Mobility-Header', 'udplite', 136, 'UDPLite', 'mpls-in-ip', 137, 'MPLS-in-IP', @@ -72,243 +76,184 @@ function lookupProto(x) { for (var i = 0; i < protocols.length; i += 3) if (s == protocols[i] || s == protocols[i+1]) - return [ protocols[i+1], protocols[i+2] ]; + return [ protocols[i+1], protocols[i+2], protocols[i] ]; - return [ -1, x ]; + return [ -1, x, x ]; } - return L.Class.extend({ - fmt_neg: function(x) { - var rv = E([]), - v = (typeof(x) == 'string') ? x.replace(/^ *! */, '') : ''; - - L.dom.append(rv, (v != '' && v != x) ? [ _('not') + ' ', v ] : [ '', x ]); - return rv; - }, - - fmt_mac: function(x) { - var rv = E([]), l = L.toArray(x); - - if (l.length == 0) - return null; - - L.dom.append(rv, [ _('MAC') + ' ' ]); - - for (var i = 0; i < l.length; i++) { - var n = this.fmt_neg(l[i]); - L.dom.append(rv, (i > 0) ? [ ', ', n ] : n); + fmt: function(fmtstr, args, values) { + var repl = [], + wrap = false, + tokens = []; + + if (values == null) { + values = []; + wrap = true; } - if (rv.childNodes.length > 2) - rv.firstChild.data = _('MACs') + ' '; + var get = function(args, key) { + var names = key.trim().split(/\./), + obj = args, + ctx = obj; - return rv; - }, - - fmt_port: function(x, d) { - var rv = E([]), l = L.toArray(x); + for (var i = 0; i < names.length; i++) { + if (!L.isObject(obj)) + return null; - if (l.length == 0) { - if (d) { - L.dom.append(rv, E('var', {}, d)); - return rv; + ctx = obj; + obj = obj[names[i]]; } - return null; - } - - L.dom.append(rv, [ _('port') + ' ' ]); + if (typeof(obj) == 'function') + return obj.call(ctx); - for (var i = 0; i < l.length; i++) { - var n = this.fmt_neg(l[i]), - m = n.lastChild.data.match(/^(\d+)\D+(\d+)$/); + return obj; + }; - if (i > 0) - L.dom.append(rv, [ ', ' ]); + var isset = function(val) { + if (L.isObject(val) && !L.dom.elem(val)) { + for (var k in val) + if (val.hasOwnProperty(k)) + return true; - if (m) { - rv.firstChild.data = _('ports') + ' '; - L.dom.append(rv, E('var', [ n.firstChild, m[1], '-', m[2] ])); + return false; + } + else if (Array.isArray(val)) { + return (val.length > 0); } else { - L.dom.append(rv, E('var', {}, n)); + return (val !== null && val !== undefined && val !== '' && val !== false); } - } - - if (rv.childNodes.length > 2) - rv.firstChild.data = _('ports') + ' '; - - return rv; - }, - - fmt_ip: function(x, d) { - var rv = E([]), l = L.toArray(x); + }; - if (l.length == 0) { - if (d) { - L.dom.append(rv, E('var', {}, d)); - return rv; + var parse = function(tokens, text) { + if (L.dom.elem(text)) { + tokens.push(''.format(values.length)); + values.push(text); } + else { + tokens.push(String(text).replace(/\\(.)/g, '$1')); + } + }; - return null; + for (var i = 0, last = 0; i <= fmtstr.length; i++) { + if (fmtstr.charAt(i) == '%' && fmtstr.charAt(i + 1) == '{') { + if (i > last) + parse(tokens, fmtstr.substring(last, i)); + + var j = i + 1, nest = 0; + + var subexpr = []; + + for (var off = j + 1, esc = false; j <= fmtstr.length; j++) { + var ch = fmtstr.charAt(j); + + if (esc) { + esc = false; + } + else if (ch == '\\') { + esc = true; + } + else if (ch == '{') { + nest++; + } + else if (ch == '}') { + if (--nest == 0) { + subexpr.push(fmtstr.substring(off, j)); + break; + } + } + else if (ch == '?' || ch == ':' || ch == '#') { + if (nest == 1) { + subexpr.push(fmtstr.substring(off, j)); + subexpr.push(ch); + off = j + 1; + } + } + } + + var varname = subexpr[0].trim(), + op1 = (subexpr[1] != null) ? subexpr[1] : '?', + if_set = (subexpr[2] != null && subexpr[2] != '') ? subexpr[2] : '%{' + varname + '}', + op2 = (subexpr[3] != null) ? subexpr[3] : ':', + if_unset = (subexpr[4] != null) ? subexpr[4] : ''; + + /* Invalid expression */ + if (nest != 0 || subexpr.length > 5 || varname == '') { + return fmtstr; + } + + /* enumeration */ + else if (op1 == '#' && subexpr.length == 3) { + var items = L.toArray(get(args, varname)); + + for (var k = 0; k < items.length; k++) { + tokens.push.apply(tokens, this.fmt(if_set, Object.assign({}, args, { + first: k == 0, + next: k > 0, + last: (k + 1) == items.length, + item: items[k] + }), values)); + } + } + + /* ternary expression */ + else if (op1 == '?' && op2 == ':' && (subexpr.length == 1 || subexpr.length == 3 || subexpr.length == 5)) { + var val = get(args, varname); + + if (subexpr.length == 1) + parse(tokens, isset(val) ? val : ''); + else if (isset(val)) + tokens.push.apply(tokens, this.fmt(if_set, args, values)); + else + tokens.push.apply(tokens, this.fmt(if_unset, args, values)); + } + + /* unrecognized command */ + else { + return fmtstr; + } + + last = j + 1; + i = j; + } + else if (i >= fmtstr.length) { + if (i > last) + parse(tokens, fmtstr.substring(last, i)); + } } - L.dom.append(rv, [ _('IP') + ' ' ]); - - for (var i = 0; i < l.length; i++) { - var n = this.fmt_neg(l[i]), - m = n.lastChild.data.match(/^(\S+)\/(\d+\.\S+)$/); + if (wrap) { + var node = E('span', {}, tokens.join('')), + repl = node.querySelectorAll('span[data-fmt-placeholder]'); - if (i > 0) - L.dom.append(rv, [ ', ' ]); + for (var i = 0; i < repl.length; i++) + repl[i].parentNode.replaceChild(values[repl[i].getAttribute('data-fmt-placeholder')], repl[i]); - if (m) - rv.firstChild.data = _('IP range') + ' '; - else if (n.lastChild.data.match(/^[a-zA-Z0-9_]+$/)) - rv.firstChild.data = _('Network') + ' '; - - L.dom.append(rv, E('var', {}, n)); + return node; } - - if (rv.childNodes.length > 2) - rv.firstChild.data = _('IPs') + ' '; - - return rv; - }, - - fmt_zone: function(x, d) { - if (x == '*') - return E('var', _('any zone')); - else if (x != null && x != '') - return E('var', {}, [ x ]); - else if (d != null && d != '') - return E('var', {}, d); - else - return null; - }, - - fmt_icmp_type: function(x) { - var rv = E([]), l = L.toArray(x); - - if (l.length == 0) - return null; - - L.dom.append(rv, [ _('type') + ' ' ]); - - for (var i = 0; i < l.length; i++) { - var n = this.fmt_neg(l[i]); - - if (i > 0) - L.dom.append(rv, [ ', ' ]); - - L.dom.append(rv, E('var', {}, n)); + else { + return tokens; } - - if (rv.childNodes.length > 2) - rv.firstChild.data = _('types') + ' '; - - return rv; - }, - - fmt_family: function(family) { - if (family == 'ipv4') - return _('IPv4'); - else if (family == 'ipv6') - return _('IPv6'); - else - return _('IPv4 and IPv6'); }, - fmt_proto: function(x, icmp_types) { - var rv = E([]), l = L.toArray(x); - - if (l.length == 0) - return null; - - var t = this.fmt_icmp_type(icmp_types); + map_invert: function(v, fn) { + return L.toArray(v).map(function(v) { + v = String(v); - for (var i = 0; i < l.length; i++) { - var n = this.fmt_neg(l[i]), - p = lookupProto(n.lastChild.data); - - if (n.lastChild.data == 'all') - continue; - - if (i > 0) - L.dom.append(rv, [ ', ' ]); - - if (t && (p[0] == 1 || p[0] == 58)) - L.dom.append(rv, [ _('%s%s with %s').format(n.firstChild.data, p[1], ''), t ]); - else - L.dom.append(rv, [ n.firstChild.data, p[1] ]); - } - - return rv; - }, + if (fn != null && typeof(v[fn]) == 'function') + v = v[fn].call(v); - fmt_limit: function(limit, burst) { - if (limit == null || limit == '') - return null; - - var m = String(limit).match(/^(\d+)\/(\w+)$/), - u = m[2] || 'second', - l = +(m[1] || limit), - b = +burst; - - if (!isNaN(l)) { - if (u.match(/^s/)) - u = _('second'); - else if (u.match(/^m/)) - u = _('minute'); - else if (u.match(/^h/)) - u = _('hour'); - else if (u.match(/^d/)) - u = _('day'); - - if (!isNaN(b) && b > 0) - return E('' + - _('%d pkts. per %s, burst %d pkts.').format(l, u, b) + - ''); - else - return E('' + - _('%d pkts. per %s').format(l, u) + - ''); - } + return { + ival: v, + inv: v.charAt(0) == '!', + val: v.replace(/^!\s*/, '') + }; + }); }, - fmt_target: function(x, src, dest) { - if (src == null || src == '') { - if (x == 'ACCEPT') - return _('Accept output'); - else if (x == 'REJECT') - return _('Refuse output'); - else if (x == 'NOTRACK') - return _('Do not track output'); - else /* if (x == 'DROP') */ - return _('Discard output'); - } - else if (dest != null && dest != '') { - if (x == 'ACCEPT') - return _('Accept forward'); - else if (x == 'REJECT') - return _('Refuse forward'); - else if (x == 'NOTRACK') - return _('Do not track forward'); - else /* if (x == 'DROP') */ - return _('Discard forward'); - } - else { - if (x == 'ACCEPT') - return _('Accept input'); - else if (x == 'REJECT' ) - return _('Refuse input'); - else if (x == 'NOTRACK') - return _('Do not track input'); - else /* if (x == 'DROP') */ - return _('Discard input'); - } - }, + lookupProto: lookupProto, addDSCPOption: function(s, is_target) { var o = s.taboption(is_target ? 'general' : 'advanced', form.Value, is_target ? 'set_dscp' : 'dscp', @@ -442,5 +387,208 @@ return L.Class.extend({ o.depends({ limit: null, '!reverse': true }); return o; - } + }, + + transformHostHints: function(family, hosts) { + var choice_values = [], choice_labels = {}; + + if (!family || family == 'ipv4') { + L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { + var val = hosts[mac].ipv4, + txt = hosts[mac].name || mac; + + choice_values.push(val); + choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]); + }); + } + + if (!family || family == 'ipv6') { + L.sortedKeys(hosts, 'ipv6', 'addr').forEach(function(mac) { + var val = hosts[mac].ipv6, + txt = hosts[mac].name || mac; + + choice_values.push(val); + choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]); + }); + } + + return [choice_values, choice_labels]; + }, + + updateHostHints: function(map, section_id, option, family, hosts) { + var opt = map.lookupOption(option, section_id)[0].getUIElement(section_id), + choices = this.transformHostHints(family, hosts); + + opt.clearChoices(); + opt.addChoices(choices[0], choices[1]); + }, + + addIPOption: function(s, tab, name, label, description, family, hosts, multiple) { + var o = s.taboption(tab, multiple ? form.DynamicList : form.Value, name, label, description); + + o.modalonly = true; + o.datatype = 'list(neg(ipmask))'; + o.placeholder = multiple ? _('-- add IP --') : _('any'); + + if (family != null) { + var choices = this.transformHostHints(family, hosts); + + for (var i = 0; i < choices[0].length; i++) + o.value(choices[0][i], choices[1][choices[0][i]]); + } + + /* force combobox rendering */ + o.transformChoices = function() { + return this.super('transformChoices', []) || {}; + }; + + return o; + }, + + addLocalIPOption: function(s, tab, name, label, description, devices) { + var o = s.taboption(tab, form.Value, name, label, description); + + o.modalonly = true; + o.datatype = 'ip4addr("nomask")'; + o.placeholder = _('any'); + + L.sortedKeys(devices, 'name').forEach(function(dev) { + var ip4addrs = devices[dev].ipaddrs; + + if (!L.isObject(devices[dev].flags) || !Array.isArray(ip4addrs) || devices[dev].flags.loopback) + return; + + for (var i = 0; i < ip4addrs.length; i++) { + if (!L.isObject(ip4addrs[i]) || !ip4addrs[i].address) + continue; + + o.value(ip4addrs[i].address, E([], [ + ip4addrs[i].address, ' (', E('strong', {}, [dev]), ')' + ])); + } + }); + + return o; + }, + + addMACOption: function(s, tab, name, label, description, hosts) { + var o = s.taboption(tab, form.DynamicList, name, label, description); + + o.modalonly = true; + o.datatype = 'list(macaddr)'; + o.placeholder = _('-- add MAC --'); + + L.sortedKeys(hosts).forEach(function(mac) { + o.value(mac, E([], [ mac, ' (', E('strong', {}, [ + hosts[mac].name || hosts[mac].ipv4 || hosts[mac].ipv6 || '?' + ]), ')' ])); + }); + + return o; + }, + + CBIProtocolSelect: form.MultiValue.extend({ + __name__: 'CBI.ProtocolSelect', + + addChoice: function(value, label) { + if (!Array.isArray(this.keylist) || this.keylist.indexOf(value) == -1) + this.value(value, label); + }, + + load: function(section_id) { + var cfgvalue = L.toArray(this.super('load', [section_id]) || this.default).sort(); + + ['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue).forEach(L.bind(function(value) { + switch (value) { + case 'all': + case 'any': + case '*': + this.addChoice('all', _('Any')); + break; + + case 'tcpudp': + this.addChoice('tcp', 'TCP'); + this.addChoice('udp', 'UDP'); + break; + + default: + var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/), + p = lookupProto(m ? +m[1] : value); + + this.addChoice(p[2], p[1]); + break; + } + }, this)); + + return cfgvalue; + }, + + renderWidget: function(section_id, option_index, cfgvalue) { + var value = (cfgvalue != null) ? cfgvalue : this.default, + choices = this.transformChoices(); + + var widget = new ui.Dropdown(L.toArray(value), choices, { + id: this.cbid(section_id), + sort: this.keylist, + multiple: true, + optional: false, + display_items: 10, + dropdown_items: -1, + create: true, + validate: function(value) { + var v = L.toArray(value); + + for (var i = 0; i < v.length; i++) { + if (v[i] == 'all') + continue; + + var m = v[i].match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/); + + if (m ? (+m[1] > 255) : (lookupProto(v[i])[0] == -1)) + return _('Unrecognized protocol'); + } + + return true; + } + }); + + widget.createChoiceElement = function(sb, value) { + var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/), + p = lookupProto(lookupProto(m ? +m[1] : value)[0]); + + return ui.Dropdown.prototype.createChoiceElement.call(this, sb, p[2], p[1]); + }; + + widget.createItems = function(sb, value) { + var values = L.toArray(value).map(function(value) { + var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/), + p = lookupProto(m ? +m[1] : value); + + return (p[0] > -1) ? p[2] : value; + }); + + return ui.Dropdown.prototype.createItems.call(this, sb, values.join(' ')); + }; + + widget.toggleItem = function(sb, li) { + var value = li.getAttribute('data-value'), + toggleFn = ui.Dropdown.prototype.toggleItem; + + toggleFn.call(this, sb, li); + + if (value == 'all') { + var items = li.parentNode.querySelectorAll('li[data-value]'); + + for (var j = 0; j < items.length; j++) + if (items[j] !== li) + toggleFn.call(this, sb, items[j], false); + } + else { + toggleFn.call(this, sb, li.parentNode.querySelector('li[data-value="all"]'), false); + } + }; + + return widget.render(); + } + }) }); diff --git a/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/forwards.js b/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/forwards.js index 500e68fb1..096124fcc 100644 --- a/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/forwards.js +++ b/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/forwards.js @@ -3,73 +3,85 @@ 'require rpc'; 'require uci'; 'require form'; +'require firewall as fwmodel'; 'require tools.firewall as fwtool'; 'require tools.widgets as widgets'; -function fmt(fmt /*, ...*/) { - var repl = [], wrap = false; - - for (var i = 1; i < arguments.length; i++) { - if (L.dom.elem(arguments[i])) { - switch (arguments[i].nodeType) { - case 1: - repl.push(arguments[i].outerHTML); - wrap = true; - break; - - case 3: - repl.push(arguments[i].data); - break; - - case 11: - var span = E('span'); - span.appendChild(arguments[i]); - repl.push(span.innerHTML); - wrap = true; - break; - - default: - repl.push(''); - } - } - else { - repl.push(arguments[i]); - } - } +function rule_proto_txt(s, ctHelpers) { + var proto = L.toArray(uci.get('firewall', s, 'proto')).filter(function(p) { + return (p != '*' && p != 'any' && p != 'all'); + }).map(function(p) { + var pr = fwtool.lookupProto(p); + return { + num: pr[0], + name: pr[1], + types: (pr[0] == 1 || pr[0] == 58) ? L.toArray(uci.get('firewall', s, 'icmp_type')) : null + }; + }); + + m = String(uci.get('firewall', s, 'helper') || '').match(/^(!\s*)?(\S+)$/); + var h = m ? { + val: m[0].toUpperCase(), + inv: m[1], + name: (ctHelpers.filter(function(ctH) { return ctH.name.toLowerCase() == m[2].toLowerCase() })[0] || {}).description + } : null; + + m = String(uci.get('firewall', s, 'mark')).match(/^(!\s*)?(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i); + var f = m ? { + val: m[0].toUpperCase().replace(/X/g, 'x'), + inv: m[1], + num: '0x%02X'.format(+m[2]), + mask: m[3] ? '0x%02X'.format(+m[3]) : null + } : null; + + return fwtool.fmt(_('Incoming IPv4%{proto?, protocol %{proto#%{next?, }%{item.types?%{item.name}ICMP with types %{item.types#%{next?, }%{item}}:%{item.name}}}}%{mark?, mark %{mark.val}}%{helper?, helper %{helper.inv?%{helper.val}:%{helper.val}}}'), { + proto: proto, + helper: h, + mark: f + }); +} - var rv = fmt.format.apply(fmt, repl); - return wrap ? E('span', rv) : rv; +function rule_src_txt(s, hosts) { + var z = uci.get('firewall', s, 'src'); + + return fwtool.fmt(_('From %{src}%{src_ip?, IP %{src_ip#%{next?, }%{item.ival}}}%{src_port?, port %{src_port#%{next?, }%{item.ival}}}%{src_mac?, MAC %{src_mac#%{next?, }%{item.ival}}}'), { + src: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName((z && z != '*') ? z : null) }, [(z == '*') ? E('em', _('any zone')) : (z || E('em', _('this device')))]), + src_ip: fwtool.map_invert(uci.get('firewall', s, 'src_ip'), 'toLowerCase'), + src_mac: fwtool.map_invert(uci.get('firewall', s, 'src_mac'), 'toUpperCase').map(function(v) { return Object.assign(v, { hint: hosts[v.val] }) }), + src_port: fwtool.map_invert(uci.get('firewall', s, 'src_port')) + }); } -function forward_proto_txt(s) { - return fmt('%s-%s', - fwtool.fmt_family('ipv4'), - fwtool.fmt_proto(uci.get('firewall', s, 'proto'), - uci.get('firewall', s, 'icmp_type')) || 'TCP+UDP'); +function rule_dest_txt(s) { + return fwtool.fmt(_('To %{dest}%{dest_ip?, IP %{dest_ip#%{next?, }%{item.ival}}}%{dest_port?, port %{dest_port#%{next?, }%{item.ival}}}'), { + dest: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName(null) }, [E('em', _('this device'))]), + dest_ip: fwtool.map_invert(uci.get('firewall', s, 'src_dip'), 'toLowerCase'), + dest_port: fwtool.map_invert(uci.get('firewall', s, 'src_dport')) + }); } -function forward_src_txt(s) { - var z = fwtool.fmt_zone(uci.get('firewall', s, 'src'), _('any zone')), - a = fwtool.fmt_ip(uci.get('firewall', s, 'src_ip'), _('any host')), - p = fwtool.fmt_port(uci.get('firewall', s, 'src_port')), - m = fwtool.fmt_mac(uci.get('firewall', s, 'src_mac')); - - if (p && m) - return fmt(_('From %s in %s with source %s and %s'), a, z, p, m); - else if (p || m) - return fmt(_('From %s in %s with source %s'), a, z, p || m); - else - return fmt(_('From %s in %s'), a, z); +function rule_limit_txt(s) { + var m = String(uci.get('firewall', s, 'limit')).match(/^(\d+)\/([smhd])\w*$/i), + l = m ? { + num: +m[1], + unit: ({ s: _('second'), m: _('minute'), h: _('hour'), d: _('day') })[m[2]], + burst: uci.get('firewall', s, 'limit_burst') + } : null; + + if (!l) + return ''; + + return fwtool.fmt(_('Limit matching to %{limit.num} packets per %{limit.unit}%{limit.burst? burst %{limit.burst}}'), { limit: l }); } -function forward_via_txt(s) { - var a = fwtool.fmt_ip(uci.get('firewall', s, 'src_dip'), _('any router IP')), - p = fwtool.fmt_port(uci.get('firewall', s, 'src_dport')); +function rule_target_txt(s) { + var z = uci.get('firewall', s, 'dest'); - if (p) - return fmt(_('Via %s at %s'), a, p); - else - return fmt(_('Via %s'), a); + return fwtool.fmt(_('Forward to %{dest}%{dest_ip? IP %{dest_ip}}%{dest_port? port %{dest_port}}'), { + dest: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName((z && z != '*') ? z : null) }, [(z == '*') ? E('em', _('any zone')) : (z || E('em', _('this device')))]), + dest_ip: (uci.get('firewall', s, 'dest_ip') || '').toLowerCase(), + dest_port: uci.get('firewall', s, 'dest_port') + }); } return L.view.extend({ @@ -85,16 +97,24 @@ return L.view.extend({ expect: { result: [] } }), + callNetworkDevices: rpc.declare({ + object: 'luci-rpc', + method: 'getNetworkDevices', + expect: { '': {} } + }), + load: function() { return Promise.all([ this.callHostHints(), - this.callConntrackHelpers() + this.callConntrackHelpers(), + this.callNetworkDevices() ]); }, render: function(data) { var hosts = data[0], ctHelpers = data[1], + devs = data[2], m, s, o; m = new form.Map('firewall', _('Firewall - Port Forwards'), @@ -134,24 +154,19 @@ return L.view.extend({ o.modalonly = false; o.textvalue = function(s) { return E('small', [ - forward_proto_txt(s), E('br'), - forward_src_txt(s), E('br'), - forward_via_txt(s) + rule_proto_txt(s, ctHelpers), E('br'), + rule_src_txt(s, hosts), E('br'), + rule_dest_txt(s), E('br'), + rule_limit_txt(s) ]); }; - o = s.option(form.ListValue, '_dest', _('Forward to')); + o = s.option(form.ListValue, '_dest', _('Action')); o.modalonly = false; o.textvalue = function(s) { - var z = fwtool.fmt_zone(uci.get('firewall', s, 'dest'), _('any zone')), - a = fwtool.fmt_ip(uci.get('firewall', s, 'dest_ip'), _('any host')), - p = fwtool.fmt_port(uci.get('firewall', s, 'dest_port')) || - fwtool.fmt_port(uci.get('firewall', s, 'src_dport')); - - if (p) - return fmt(_('%s, %s in %s'), a, p, z); - else - return fmt(_('%s in %s'), a, z); + return E('small', [ + rule_target_txt(s) + ]); }; o = s.option(form.Flag, 'enabled', _('Enable')); @@ -159,18 +174,9 @@ return L.view.extend({ o.default = o.enabled; o.editable = true; - o = s.taboption('general', form.Value, 'proto', _('Protocol')); + o = s.taboption('general', fwtool.CBIProtocolSelect, 'proto', _('Protocol')); o.modalonly = true; o.default = 'tcp udp'; - o.value('tcp udp', 'TCP+UDP'); - o.value('tcp', 'TCP'); - o.value('udp', 'UDP'); - o.value('icmp', 'ICMP'); - - o.cfgvalue = function(/* ... */) { - var v = this.super('cfgvalue', arguments); - return (v == 'tcpudp') ? 'tcp udp' : v; - }; o = s.taboption('general', widgets.ZoneSelect, 'src', _('Source zone')); o.modalonly = true; @@ -178,31 +184,15 @@ return L.view.extend({ o.nocreate = true; o.default = 'wan'; - o = s.taboption('advanced', form.Value, 'src_mac', _('Source MAC address'), - _('Only match incoming traffic from these MACs.')); - o.modalonly = true; + o = fwtool.addMACOption(s, 'advanced', 'src_mac', _('Source MAC address'), + _('Only match incoming traffic from these MACs.'), hosts); o.rmempty = true; - o.datatype = 'neg(macaddr)'; - o.placeholder = E('em', _('any')); - L.sortedKeys(hosts).forEach(function(mac) { - o.value(mac, '%s (%s)'.format( - mac, - hosts[mac].name || hosts[mac].ipv4 || hosts[mac].ipv6 || '?' - )); - }); - - o = s.taboption('advanced', form.Value, 'src_ip', _('Source IP address'), - _('Only match incoming traffic from this IP or range.')); - o.modalonly = true; + o.datatype = 'list(neg(macaddr))'; + + o = fwtool.addIPOption(s, 'advanced', 'src_ip', _('Source IP address'), + _('Only match incoming traffic from this IP or range.'), 'ipv4', hosts); o.rmempty = true; o.datatype = 'neg(ipmask4)'; - o.placeholder = E('em', _('any')); - L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { - o.value(hosts[mac].ipv4, '%s (%s)'.format( - hosts[mac].ipv4, - hosts[mac].name || mac - )); - }); o = s.taboption('advanced', form.Value, 'src_port', _('Source port'), _('Only match incoming traffic originating from the given source port or port range on the client host')); @@ -210,33 +200,21 @@ return L.view.extend({ o.rmempty = true; o.datatype = 'neg(portrange)'; o.placeholder = _('any'); - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - o.depends('proto', 'tcp udp'); - o.depends('proto', 'tcpudp'); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); - o = s.taboption('advanced', form.Value, 'src_dip', _('External IP address'), - _('Only match incoming traffic directed at the given IP address.')); - o.modalonly = true; - o.rmempty = true; + o = fwtool.addLocalIPOption(s, 'advanced', 'src_dip', _('External IP address'), + _('Only match incoming traffic directed at the given IP address.'), devs); o.datatype = 'neg(ipmask4)'; - o.placeholder = E('em', _('any')); - L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { - o.value(hosts[mac].ipv4, '%s (%s)'.format( - hosts[mac].ipv4, - hosts[mac].name || mac - )); - }); + o.rmempty = true; o = s.taboption('general', form.Value, 'src_dport', _('External port'), _('Match incoming traffic directed at the given destination port or port range on this host')); o.modalonly = true; o.rmempty = false; o.datatype = 'neg(portrange)'; - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - o.depends('proto', 'tcp udp'); - o.depends('proto', 'tcpudp'); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); o = s.taboption('general', widgets.ZoneSelect, 'dest', _('Internal zone')); o.modalonly = true; @@ -244,17 +222,10 @@ return L.view.extend({ o.nocreate = true; o.default = 'lan'; - o = s.taboption('general', form.Value, 'dest_ip', _('Internal IP address'), - _('Redirect matched incoming traffic to the specified internal host')); - o.modalonly = true; + o = fwtool.addIPOption(s, 'general', 'dest_ip', _('Internal IP address'), + _('Redirect matched incoming traffic to the specified internal host'), 'ipv4', hosts); o.rmempty = true; o.datatype = 'ipmask4'; - L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { - o.value(hosts[mac].ipv4, '%s (%s)'.format( - hosts[mac].ipv4, - hosts[mac].name || mac - )); - }); o = s.taboption('general', form.Value, 'dest_port', _('Internal port'), _('Redirect matched incoming traffic to the given port on the internal host')); @@ -262,10 +233,8 @@ return L.view.extend({ o.rmempty = true; o.placeholder = _('any'); o.datatype = 'portrange'; - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - o.depends('proto', 'tcp udp'); - o.depends('proto', 'tcpudp'); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); o = s.taboption('advanced', form.Flag, 'reflection', _('Enable NAT Loopback')); o.modalonly = true; diff --git a/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js b/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js index 6c6efc805..cc85e6676 100644 --- a/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js +++ b/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js @@ -3,142 +3,137 @@ 'require rpc'; 'require uci'; 'require form'; +'require firewall as fwmodel'; 'require tools.firewall as fwtool'; 'require tools.widgets as widgets'; -function fmt(fmt /*, ...*/) { - var repl = [], wrap = false; - - for (var i = 1; i < arguments.length; i++) { - if (L.dom.elem(arguments[i])) { - switch (arguments[i].nodeType) { - case 1: - repl.push(arguments[i].outerHTML); - wrap = true; - break; - - case 3: - repl.push(arguments[i].data); - break; - - case 11: - var span = E('span'); - span.appendChild(arguments[i]); - repl.push(span.innerHTML); - wrap = true; - break; - - default: - repl.push(''); - } - } - else { - repl.push(arguments[i]); - } - } - - var rv = fmt.format.apply(fmt, repl); - return wrap ? E('span', rv) : rv; +function rule_proto_txt(s, ctHelpers) { + var f = (uci.get('firewall', s, 'family') || '').toLowerCase().replace(/^(?:any|\*)$/, ''); + + var proto = L.toArray(uci.get('firewall', s, 'proto')).filter(function(p) { + return (p != '*' && p != 'any' && p != 'all'); + }).map(function(p) { + var pr = fwtool.lookupProto(p); + return { + num: pr[0], + name: pr[1], + types: (pr[0] == 1 || pr[0] == 58) ? L.toArray(uci.get('firewall', s, 'icmp_type')) : null + }; + }); + + m = String(uci.get('firewall', s, 'helper') || '').match(/^(!\s*)?(\S+)$/); + var h = m ? { + val: m[0].toUpperCase(), + inv: m[1], + name: (ctHelpers.filter(function(ctH) { return ctH.name.toLowerCase() == m[2].toLowerCase() })[0] || {}).description + } : null; + + m = String(uci.get('firewall', s, 'mark')).match(/^(!\s*)?(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i); + var f = m ? { + val: m[0].toUpperCase().replace(/X/g, 'x'), + inv: m[1], + num: '0x%02X'.format(+m[2]), + mask: m[3] ? '0x%02X'.format(+m[3]) : null + } : null; + + m = String(uci.get('firewall', s, 'dscp')).match(/^(!\s*)?(?:(CS[0-7]|BE|AF[1234][123]|EF)|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/); + var d = m ? { + val: m[0], + inv: m[1], + name: m[2], + num: m[3] ? '0x%02X'.format(+m[3]) : null + } : null; + + return fwtool.fmt(_('%{src?%{dest?Forwarded:Incoming}:Outgoing} %{ipv6?%{ipv4?IPv4 and IPv6:IPv6}:IPv4}%{proto?, protocol %{proto#%{next?, }%{item.types?%{item.name}ICMP with types %{item.types#%{next?, }%{item}}:%{item.name}}}}%{mark?, mark %{mark.val}}%{dscp?, DSCP %{dscp.inv?%{dscp.val}:%{dscp.val}}}%{helper?, helper %{helper.inv?%{helper.val}:%{helper.val}}}'), { + ipv4: (!f || f == 'ipv4'), + ipv6: (!f || f == 'ipv6'), + src: uci.get('firewall', s, 'src'), + dest: uci.get('firewall', s, 'dest'), + proto: proto, + helper: h, + mark: f, + dscp: d + }); } -function forward_proto_txt(s) { - return fmt('%s-%s', - fwtool.fmt_family(uci.get('firewall', s, 'family')), - fwtool.fmt_proto(uci.get('firewall', s, 'proto'), - uci.get('firewall', s, 'icmp_type')) || 'TCP+UDP'); +function rule_src_txt(s, hosts) { + var z = uci.get('firewall', s, 'src'), + d = (uci.get('firewall', s, 'direction') == 'in') ? uci.get('firewall', s, 'device') : null; + + return fwtool.fmt(_('From %{src}%{src_device?, interface %{src_device}}%{src_ip?, IP %{src_ip#%{next?, }%{item.ival}}}%{src_port?, port %{src_port#%{next?, }%{item.ival}}}%{src_mac?, MAC %{src_mac#%{next?, }%{item.ival}}}'), { + src: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName((z && z != '*') ? z : null) }, [(z == '*') ? E('em', _('any zone')) : (z || E('em', _('this device')))]), + src_ip: fwtool.map_invert(uci.get('firewall', s, 'src_ip'), 'toLowerCase'), + src_mac: fwtool.map_invert(uci.get('firewall', s, 'src_mac'), 'toUpperCase').map(function(v) { return Object.assign(v, { hint: hosts[v.val] }) }), + src_port: fwtool.map_invert(uci.get('firewall', s, 'src_port')), + src_device: d + }); } -function rule_src_txt(s) { - var z = fwtool.fmt_zone(uci.get('firewall', s, 'src')), - p = fwtool.fmt_port(uci.get('firewall', s, 'src_port')), - m = fwtool.fmt_mac(uci.get('firewall', s, 'src_mac')); - - // Forward/Input - if (z) { - var a = fwtool.fmt_ip(uci.get('firewall', s, 'src_ip'), _('any host')); - if (p && m) - return fmt(_('From %s in %s with source %s and %s'), a, z, p, m); - else if (p || m) - return fmt(_('From %s in %s with source %s'), a, z, p || m); - else - return fmt(_('From %s in %s'), a, z); - } - - // Output - else { - var a = fwtool.fmt_ip(uci.get('firewall', s, 'src_ip'), _('any router IP')); - if (p && m) - return fmt(_('From %s on this device with source %s and %s'), a, p, m); - else if (p || m) - return fmt(_('From %s on this device with source %s'), a, p || m); - else - return fmt(_('From %s on this device'), a); - } +function rule_dest_txt(s) { + var z = uci.get('firewall', s, 'dest'), + d = (uci.get('firewall', s, 'direction') == 'out') ? uci.get('firewall', s, 'device') : null; + + return fwtool.fmt(_('To %{dest}%{dest_device?, interface %{dest_device}}%{dest_ip?, IP %{dest_ip#%{next?, }%{item.ival}}}%{dest_port?, port %{dest_port#%{next?, }%{item.ival}}}'), { + dest: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName((z && z != '*') ? z : null) }, [(z == '*') ? E('em', _('any zone')) : (z || E('em', _('this device')))]), + dest_ip: fwtool.map_invert(uci.get('firewall', s, 'dest_ip'), 'toLowerCase'), + dest_port: fwtool.map_invert(uci.get('firewall', s, 'dest_port')), + dest_device: d + }); } -function rule_dest_txt(s) { - var z = fwtool.fmt_zone(uci.get('firewall', s, 'dest')), - p = fwtool.fmt_port(uci.get('firewall', s, 'dest_port')); - - // Forward - if (z) { - var a = fwtool.fmt_ip(uci.get('firewall', s, 'dest_ip'), _('any host')); - if (p) - return fmt(_('To %s, %s in %s'), a, p, z); - else - return fmt(_('To %s in %s'), a, z); - } +function rule_limit_txt(s) { + var m = String(uci.get('firewall', s, 'limit')).match(/^(\d+)\/([smhd])\w*$/i), + l = m ? { + num: +m[1], + unit: ({ s: _('second'), m: _('minute'), h: _('hour'), d: _('day') })[m[2]], + burst: uci.get('firewall', s, 'limit_burst') + } : null; - // Input - else { - var a = fwtool.fmt_ip(uci.get('firewall', s, 'dest_ip'), _('any router IP')); - if (p) - return fmt(_('To %s at %s on this device'), a, p); - else - return fmt(_('To %s on this device'), a); - } + if (!l) + return ''; + + return fwtool.fmt(_('Limit matching to %{limit.num} packets per %{limit.unit}%{limit.burst? burst %{limit.burst}}'), { limit: l }); } -function rule_target_txt(s) { - var t = fwtool.fmt_target(uci.get('firewall', s, 'target'), uci.get('firewall', s, 'src'), uci.get('firewall', s, 'dest')), - l = fwtool.fmt_limit(uci.get('firewall', s, 'limit'), uci.get('firewall', s, 'limit_burst')); +function rule_target_txt(s, ctHelpers) { + var t = uci.get('firewall', s, 'target'), + h = (uci.get('firewall', s, 'set_helper') || '').toUpperCase(), + s = { + target: t, + src: uci.get('firewall', s, 'src'), + dest: uci.get('firewall', s, 'dest'), + set_helper: h, + set_mark: uci.get('firewall', s, 'set_mark'), + set_xmark: uci.get('firewall', s, 'set_xmark'), + set_dscp: uci.get('firewall', s, 'set_dscp'), + helper_name: (ctHelpers.filter(function(ctH) { return ctH.name.toUpperCase() == h })[0] || {}).description + }; - if (l) - return fmt(_('%s and limit to %s'), t, l); - else - return fmt('%s', t); -} + switch (t) { + case 'DROP': + return fwtool.fmt(_('Drop %{src?%{dest?forward:input}:output}'), s); -function update_ip_hints(map, section_id, family, hosts) { - var elem_src_ip = map.lookupOption('src_ip', section_id)[0].getUIElement(section_id), - elem_dst_ip = map.lookupOption('dest_ip', section_id)[0].getUIElement(section_id), - choice_values = [], choice_labels = {}; + case 'ACCEPT': + return fwtool.fmt(_('Accept %{src?%{dest?forward:input}:output}'), s); - elem_src_ip.clearChoices(); - elem_dst_ip.clearChoices(); + case 'REJECT': + return fwtool.fmt(_('Reject %{src?%{dest?forward:input}:output}'), s); - if (!family || family == 'ipv4') { - L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { - var val = hosts[mac].ipv4, - txt = '%s (%s)'.format(val, hosts[mac].name || mac); + case 'NOTRACK': + return fwtool.fmt(_('Do not track %{src?%{dest?forward:input}:output}'), s); - choice_values.push(val); - choice_labels[val] = txt; - }); - } + case 'HELPER': + return fwtool.fmt(_('Assign conntrack helper %{set_helper}'), s); - if (!family || family == 'ipv6') { - L.sortedKeys(hosts, 'ipv6', 'addr').forEach(function(mac) { - var val = hosts[mac].ipv6, - txt = '%s (%s)'.format(val, hosts[mac].name || mac); + case 'MARK': + return fwtool.fmt(_('%{set_mark?Assign:XOR} firewall mark %{set_mark?:%{set_xmark}}'), s); - choice_values.push(val); - choice_labels[val] = txt; - }); - } + case 'DSCP': + return fwtool.fmt(_('Assign DSCP classification %{set_dscp}'), s); - elem_src_ip.addChoices(choice_values, choice_labels); - elem_dst_ip.addChoices(choice_values, choice_labels); + default: + return t; + } } return L.view.extend({ @@ -215,16 +210,17 @@ return L.view.extend({ o.modalonly = false; o.textvalue = function(s) { return E('small', [ - forward_proto_txt(s), E('br'), - rule_src_txt(s), E('br'), - rule_dest_txt(s) + rule_proto_txt(s, ctHelpers), E('br'), + rule_src_txt(s, hosts), E('br'), + rule_dest_txt(s), E('br'), + rule_limit_txt(s) ]); }; o = s.option(form.ListValue, '_target', _('Action')); o.modalonly = false; o.textvalue = function(s) { - return rule_target_txt(s); + return rule_target_txt(s, ctHelpers); }; o = s.option(form.Flag, 'enabled', _('Enable')); @@ -268,22 +264,14 @@ return L.view.extend({ o.value('ipv4', _('IPv4 only')); o.value('ipv6', _('IPv6 only')); o.validate = function(section_id, value) { - update_ip_hints(this.map, section_id, value, hosts); + fwtool.updateHostHints(this.map, section_id, 'src_ip', value, hosts); + fwtool.updateHostHints(this.map, section_id, 'dest_ip', value, hosts); return true; }; - o = s.taboption('general', form.Value, 'proto', _('Protocol')); + o = s.taboption('general', fwtool.CBIProtocolSelect, 'proto', _('Protocol')); o.modalonly = true; o.default = 'tcp udp'; - o.value('all', _('Any')); - o.value('tcp udp', 'TCP+UDP'); - o.value('tcp', 'TCP'); - o.value('udp', 'UDP'); - o.value('icmp', 'ICMP'); - o.cfgvalue = function(/* ... */) { - var v = this.super('cfgvalue', arguments); - return (v == 'tcpudp') ? 'tcp udp' : v; - }; o = s.taboption('advanced', form.MultiValue, 'icmp_type', _('Match ICMP type')); o.modalonly = true; @@ -330,7 +318,8 @@ return L.view.extend({ o.value('TOS-network-unreachable'); o.value('ttl-zero-during-reassembly'); o.value('ttl-zero-during-transit'); - o.depends('proto', 'icmp'); + o.depends({ proto: 'icmp', '!contains': true }); + o.depends({ proto: 'icmpv6', '!contains': true }); o = s.taboption('general', widgets.ZoneSelect, 'src', _('Source zone')); o.modalonly = true; @@ -338,31 +327,15 @@ return L.view.extend({ o.allowany = true; o.allowlocal = 'src'; - o = s.taboption('advanced', form.Value, 'src_mac', _('Source MAC address')); - o.modalonly = true; - o.datatype = 'list(macaddr)'; - o.placeholder = _('any'); - L.sortedKeys(hosts).forEach(function(mac) { - o.value(mac, '%s (%s)'.format( - mac, - hosts[mac].name || hosts[mac].ipv4 || hosts[mac].ipv6 || '?' - )); - }); - - o = s.taboption('general', form.Value, 'src_ip', _('Source address')); - o.modalonly = true; - o.datatype = 'list(neg(ipmask))'; - o.placeholder = _('any'); - o.transformChoices = function() { return {} }; /* force combobox rendering */ + fwtool.addMACOption(s, 'advanced', 'src_mac', _('Source MAC address'), null, hosts); + fwtool.addIPOption(s, 'general', 'src_ip', _('Source address'), null, '', hosts, true); o = s.taboption('general', form.Value, 'src_port', _('Source port')); o.modalonly = true; o.datatype = 'list(neg(portrange))'; o.placeholder = _('any'); - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - o.depends('proto', 'tcp udp'); - o.depends('proto', 'tcpudp'); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); o = s.taboption('general', widgets.ZoneSelect, 'dest', _('Destination zone')); o.modalonly = true; @@ -370,20 +343,14 @@ return L.view.extend({ o.allowany = true; o.allowlocal = true; - o = s.taboption('general', form.Value, 'dest_ip', _('Destination address')); - o.modalonly = true; - o.datatype = 'list(neg(ipmask))'; - o.placeholder = _('any'); - o.transformChoices = function() { return {} }; /* force combobox rendering */ + fwtool.addIPOption(s, 'general', 'dest_ip', _('Destination address'), null, '', hosts, true); o = s.taboption('general', form.Value, 'dest_port', _('Destination port')); o.modalonly = true; o.datatype = 'list(neg(portrange))'; o.placeholder = _('any'); - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - o.depends('proto', 'tcp udp'); - o.depends('proto', 'tcpudp'); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); o = s.taboption('general', form.ListValue, 'target', _('Action')); o.modalonly = true; diff --git a/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js b/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js index 919a418fe..2db02d944 100644 --- a/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js +++ b/applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js @@ -3,126 +3,90 @@ 'require rpc'; 'require uci'; 'require form'; +'require firewall as fwmodel'; 'require tools.firewall as fwtool'; 'require tools.widgets as widgets'; -function fmt(fmtstr, args) { - var repl = [], wrap = false; - var tokens = []; - - for (var i = 0, last = 0; i <= fmtstr.length; i++) { - if (fmtstr.charAt(i) == '%' && fmtstr.charAt(i + 1) == '{') { - if (i > last) - tokens.push(fmtstr.substring(last, i)); - - var j = i + 1, nest = 0; - - var subexpr = []; - - for (var off = j + 1, esc = false; j <= fmtstr.length; j++) { - if (esc) { - esc = false; - } - else if (fmtstr.charAt(j) == '\\') { - esc = true; - } - else if (fmtstr.charAt(j) == '{') { - nest++; - } - else if (fmtstr.charAt(j) == '}') { - if (--nest == 0) { - subexpr.push(fmtstr.substring(off, j)); - break; - } - } - else if (fmtstr.charAt(j) == '?' || fmtstr.charAt(j) == ':') { - if (nest == 1) { - subexpr.push(fmtstr.substring(off, j)); - subexpr.push(fmtstr.charAt(j)); - off = j + 1; - } - } - } - - var varname = subexpr[0].trim(), - op1 = (subexpr[1] != null) ? subexpr[1] : '?', - if_set = (subexpr[2] != null && subexpr[2] != '') ? subexpr[2] : '%{' + varname + '}', - op2 = (subexpr[3] != null) ? subexpr[3] : ':', - if_unset = (subexpr[4] != null) ? subexpr[4] : ''; - - /* Invalid expression */ - if (nest != 0 || subexpr.length > 5 || varname == '' || op1 != '?' || op2 != ':') - return fmtstr; - - if (subexpr.length == 1) - tokens.push(args[varname] != null ? args[varname] : ''); - else if (args[varname] != null) - tokens.push(fmt(if_set.replace(/\\(.)/g, '$1'), args)); - else - tokens.push(fmt(if_unset.replace(/\\(.)/g, '$1'), args)); - - last = j + 1; - i = last; - } - else if (i >= fmtstr.length) { - if (i > last) - tokens.push(fmtstr.substring(last, i)); - } - } - - for (var i = 0; i < tokens.length; i++) - if (typeof(tokens[i]) == 'object') - return E('span', {}, tokens); +function rule_proto_txt(s) { + var proto = L.toArray(uci.get('firewall', s, 'proto')).filter(function(p) { + return (p != '*' && p != 'any' && p != 'all'); + }).map(function(p) { + var pr = fwtool.lookupProto(p); + return { + num: pr[0], + name: pr[1] + }; + }); - return tokens.join(''); + m = String(uci.get('firewall', s, 'mark')).match(/^(!\s*)?(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i); + var f = m ? { + val: m[0].toUpperCase().replace(/X/g, 'x'), + inv: m[1], + num: '0x%02X'.format(+m[2]), + mask: m[3] ? '0x%02X'.format(+m[3]) : null + } : null; + + return fwtool.fmt(_('Forwarded IPv4%{proto?, protocol %{proto#%{next?, }%{item.name}}}%{mark?, mark %{mark.val}}'), { + proto: proto, + mark: f + }); } -function snat_proto_txt(s) { - var m = uci.get('firewall', s, 'mark'), - p = uci.get('firewall', s, 'proto'); +function rule_src_txt(s, hosts) { + var z = uci.get('firewall', s, 'src'); - return fmt(_('Match %{protocol?%{family} %{protocol} traffic:any %{family} traffic} %{mark?with firewall mark %{mark}} %{limit?limited to %{limit}}'), { - protocol: (p && p != 'all' && p != 'any' && p != '*') ? fwtool.fmt_proto(uci.get('firewall', s, 'proto')) : null, - family: fwtool.fmt_family('ipv4'), - mark: m ? E('var', {}, fwtool.fmt_neg(m)) : null, - limit: fwtool.fmt_limit(uci.get('firewall', s, 'limit'), uci.get('firewall', s, 'limit_burst')) + return fwtool.fmt(_('From %{src}%{src_device?, interface %{src_device}}%{src_ip?, IP %{src_ip#%{next?, }%{item.ival}}}%{src_port?, port %{src_port#%{next?, }%{item.ival}}}'), { + src: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName(null) }, [E('em', _('any zone'))]), + src_ip: fwtool.map_invert(uci.get('firewall', s, 'src_ip'), 'toLowerCase'), + src_port: fwtool.map_invert(uci.get('firewall', s, 'src_port')) }); } -function snat_src_txt(s) { - return fmt(_('From %{ipaddr?:any host} %{port?with source %{port}}'), { - ipaddr: fwtool.fmt_ip(uci.get('firewall', s, 'src_ip')), - port: fwtool.fmt_port(uci.get('firewall', s, 'src_port')) +function rule_dest_txt(s) { + var z = uci.get('firewall', s, 'src'); + + return fwtool.fmt(_('To %{dest}%{dest_device?, via interface %{dest_device}}%{dest_ip?, IP %{dest_ip#%{next?, }%{item.ival}}}%{dest_port?, port %{dest_port#%{next?, }%{item.ival}}}'), { + dest: E('span', { 'class': 'zonebadge', 'style': 'background-color:' + fwmodel.getColorForName((z && z != '*') ? z : null) }, [(z == '*') ? E('em', _('any zone')) : (z || E('em', _('this device')))]), + dest_ip: fwtool.map_invert(uci.get('firewall', s, 'dest_ip'), 'toLowerCase'), + dest_port: fwtool.map_invert(uci.get('firewall', s, 'dest_port')), + dest_device: uci.get('firewall', s, 'device') }); } -function snat_dest_txt(s) { - var z = uci.get('firewall', s, 'src'), - d = uci.get('firewall', s, 'device'); +function rule_limit_txt(s) { + var m = String(uci.get('firewall', s, 'limit')).match(/^(\d+)\/([smhd])\w*$/i), + l = m ? { + num: +m[1], + unit: ({ s: _('second'), m: _('minute'), h: _('hour'), d: _('day') })[m[2]], + burst: uci.get('firewall', s, 'limit_burst') + } : null; - return fmt(_('To %{ipaddr?:any destination} %{port?at %{port}} %{zone?via zone %{zone}} %{device?egress device %{device}}'), { - port: fwtool.fmt_port(uci.get('firewall', s, 'dest_port')), - ipaddr: fwtool.fmt_ip(uci.get('firewall', s, 'dest_ip')), - zone: (z != '*') ? fwtool.fmt_zone(z) : null, - device: d ? E('var', {}, [d]) : null - }); + if (!l) + return ''; + + return fwtool.fmt(_('Limit matching to %{limit.num} packets per %{limit.unit}%{limit.burst? burst %{limit.burst}}'), { limit: l }); } -function snat_rewrite_txt(s) { +function rule_target_txt(s) { var t = uci.get('firewall', s, 'target'), - l = fwtool.fmt_limit(uci.get('firewall', s, 'limit'), uci.get('firewall', s, 'limit_burst')); + s = { + target: t, + snat_ip: uci.get('firewall', s, 'snat_ip'), + snat_port: uci.get('firewall', s, 'snat_port') + }; - if (t == 'SNAT') { - return fmt(_('Rewrite to %{ipaddr?%{port?%{ipaddr}, %{port}:%{ipaddr}}:%{port}}'), { - ipaddr: fwtool.fmt_ip(uci.get('firewall', s, 'snat_ip')), - port: fwtool.fmt_port(uci.get('firewall', s, 'snat_port')) - }); - } - else if (t == 'MASQUERADE') { - return _('Rewrite to outbound device IP'); - } - else if (t == 'ACCEPT') { - return _('Do not rewrite'); + switch (t) { + case 'SNAT': + return fwtool.fmt(_('Statically rewrite to source %{snat_ip?IP %{snat_ip}} %{snat_port?port %{snat_port}}'), s); + + case 'MASQUERADE': + return fwtool.fmt(_('Automatically rewrite source IP')); + + case 'ACCEPT': + return fwtool.fmt(_('Prevent source rewrite')); + + default: + return t; } } @@ -175,16 +139,17 @@ return L.view.extend({ o.modalonly = false; o.textvalue = function(s) { return E('small', [ - snat_proto_txt(s), E('br'), - snat_src_txt(s), E('br'), - snat_dest_txt(s) + rule_proto_txt(s), E('br'), + rule_src_txt(s, hosts), E('br'), + rule_dest_txt(s), E('br'), + rule_limit_txt(s) ]); }; - o = s.option(form.ListValue, '_dest', _('Rewrite to')); + o = s.option(form.ListValue, '_target', _('Action')); o.modalonly = false; o.textvalue = function(s) { - return snat_rewrite_txt(s); + return rule_target_txt(s); }; o = s.option(form.Flag, 'enabled', _('Enable')); @@ -192,17 +157,9 @@ return L.view.extend({ o.default = o.enabled; o.editable = true; - o = s.taboption('general', form.Value, 'proto', _('Protocol')); + o = s.taboption('general', fwtool.CBIProtocolSelect, 'proto', _('Protocol')); o.modalonly = true; o.default = 'all'; - o.value('all', _('Any')); - o.value('tcp udp', 'TCP+UDP'); - o.value('tcp', 'TCP'); - o.value('udp', 'UDP'); - o.cfgvalue = function(/* ... */) { - var v = this.super('cfgvalue', arguments); - return (v == 'tcpudp') ? 'tcp udp' : v; - }; o = s.taboption('general', widgets.ZoneSelect, 'src', _('Outbound zone')); o.modalonly = true; @@ -211,18 +168,10 @@ return L.view.extend({ o.allowany = true; o.default = 'lan'; - o = s.taboption('general', form.Value, 'src_ip', _('Source IP address'), - _('Match forwarded traffic from this IP or range.')); - o.modalonly = true; + o = fwtool.addIPOption(s, 'general', 'src_ip', _('Source address'), + _('Match forwarded traffic from this IP or range.'), 'ipv4', hosts); o.rmempty = true; o.datatype = 'neg(ipmask4)'; - o.placeholder = E('em', _('any')); - L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { - o.value(hosts[mac].ipv4, '%s (%s)'.format( - hosts[mac].ipv4, - hosts[mac].name || mac - )); - }); o = s.taboption('general', form.Value, 'src_port', _('Source port'), _('Match forwarded traffic originating from the given source port or port range.')); @@ -230,23 +179,13 @@ return L.view.extend({ o.rmempty = true; o.datatype = 'neg(portrange)'; o.placeholder = _('any'); - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - o.depends('proto', 'tcp udp'); - o.depends('proto', 'tcpudp'); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); - o = s.taboption('general', form.Value, 'dest_ip', _('Destination IP address'), - _('Match forwarded traffic directed at the given IP address.')); - o.modalonly = true; + o = fwtool.addIPOption(s, 'general', 'dest_ip', _('Destination address'), + _('Match forwarded traffic directed at the given IP address.'), 'ipv4', hosts); o.rmempty = true; o.datatype = 'neg(ipmask4)'; - o.placeholder = E('em', _('any')); - L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) { - o.value(hosts[mac].ipv4, '%s (%s)'.format( - hosts[mac].ipv4, - hosts[mac].name || mac - )); - }); o = s.taboption('general', form.Value, 'dest_port', _('Destination port'), _('Match forwarded traffic directed at the given destination port or port range.')); @@ -254,10 +193,8 @@ return L.view.extend({ o.rmempty = true; o.placeholder = _('any'); o.datatype = 'neg(portrange)'; - o.depends('proto', 'tcp'); - o.depends('proto', 'udp'); - o.depends('proto', 'tcp udp'); - o.depends('proto', 'tcpudp'); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); o = s.taboption('general', form.ListValue, 'target', _('Action')); o.modalonly = true; @@ -266,35 +203,20 @@ return L.view.extend({ o.value('MASQUERADE', _('MASQUERADE - Automatically rewrite to outbound interface IP')); o.value('ACCEPT', _('ACCEPT - Disable address rewriting')); - o = s.taboption('general', form.Value, 'snat_ip', _('Rewrite IP address'), - _('Rewrite matched traffic to the specified source IP address.')); - o.modalonly = true; - o.rmempty = true; - o.placeholder = _('do not rewrite'); - o.datatype = 'ip4addr("nomask")'; + o = fwtool.addLocalIPOption(s, 'general', 'snat_ip', _('Rewrite IP address'), + _('Rewrite matched traffic to the specified source IP address.'), devs); + o.placeholder = null; + o.depends('target', 'SNAT'); o.validate = function(section_id, value) { var port = this.map.lookupOption('snat_port', section_id), + a = this.formvalue(section_id), p = port ? port[0].formvalue(section_id) : null; - if ((value == null || value == '') && (p == null || p == '')) + if ((a == null || a == '') && (p == null || p == '')) return _('A rewrite IP must be specified!'); return true; }; - o.depends('target', 'SNAT'); - L.sortedKeys(devs, 'name').forEach(function(dev) { - var ip4addrs = devs[dev].ipaddrs; - - if (!L.isObject(devs[dev].flags) || !Array.isArray(ip4addrs) || devs[dev].flags.loopback) - return; - - for (var i = 0; i < ip4addrs.length; i++) { - if (!L.isObject(ip4addrs[i]) || !ip4addrs[i].address) - continue; - - o.value(ip4addrs[i].address, '%s (%s)'.format(ip4addrs[i].address, dev)); - } - }); o = s.taboption('general', form.Value, 'snat_port', _('Rewrite port'), _('Rewrite matched traffic to the specified source port or port range.')); @@ -302,10 +224,8 @@ return L.view.extend({ o.rmempty = true; o.placeholder = _('do not rewrite'); o.datatype = 'portrange'; - o.depends({ target: 'SNAT', proto: 'tcp' }); - o.depends({ target: 'SNAT', proto: 'udp' }); - o.depends({ target: 'SNAT', proto: 'tcp udp' }); - o.depends({ target: 'SNAT', proto: 'tcpudp' }); + o.depends({ proto: 'tcp', '!contains': true }); + o.depends({ proto: 'udp', '!contains': true }); o = s.taboption('advanced', widgets.DeviceSelect, 'device', _('Outbound device'), _('Matches forwarded traffic using the specified outbound network device.')); -- 2.25.1