luci-app-firewall: rework rule descriptions, deduplicate code
authorJo-Philipp Wich <jo@mein.io>
Sun, 19 Jan 2020 15:08:47 +0000 (16:08 +0100)
committerJo-Philipp Wich <jo@mein.io>
Tue, 28 Jan 2020 17:34:01 +0000 (18:34 +0100)
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 <jo@mein.io>
(backported from commit 7944b0a90bbb0f0f5d1da5eaf5d9906ac546c44b)

applications/luci-app-firewall/htdocs/luci-static/resources/tools/firewall.js
applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/forwards.js
applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/rules.js
applications/luci-app-firewall/htdocs/luci-static/resources/view/firewall/snats.js

index e983035b3d0a04c951dcd565859f689883d2b680..c60bfd028c484b460543a6afb85752bbf4dc1fff 100644 (file)
@@ -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('<span data-fmt-placeholder="%d"></span>'.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('<span>' +
-                                       _('<var>%d</var> pkts. per <var>%s</var>, burst <var>%d</var> pkts.').format(l, u, b) +
-                               '</span>');
-                       else
-                               return E('<span>' +
-                                       _('<var>%d</var> pkts. per <var>%s</var>').format(l, u) +
-                               '</span>');
-               }
+                       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();
+               }
+       })
 });
index 500e68fb174fd6222ca3157dd6c74076d630b8dd..096124fccafa18340fd7ae1f398e22415a68b225 100644 (file)
@@ -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?<var class="cbi-tooltip-container">%{item.name}<span class="cbi-tooltip">ICMP with types %{item.types#%{next?, }<var>%{item}</var>}</span></var>:<var>%{item.name}</var>}}}%{mark?, mark <var%{mark.inv? data-tooltip="Match fwmarks except %{mark.num}%{mark.mask? with mask %{mark.mask}}.":%{mark.mask? data-tooltip="Mask fwmark value with %{mark.mask} before compare."}}>%{mark.val}</var>}%{helper?, helper %{helper.inv?<var data-tooltip="Match any helper except &quot;%{helper.name}&quot;">%{helper.val}</var>:<var data-tooltip="%{helper.name}">%{helper.val}</var>}}'), {
+               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?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{src_port?, port %{src_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}%{src_mac?, MAC %{src_mac#%{next?, }<var%{item.inv? data-tooltip="Match MACs except %{item.val}%{item.hint.name? a.k.a. %{item.hint.name}}.":%{item.hint.name? data-tooltip="%{item.hint.name}"}}>%{item.ival}</var>}}'), {
+               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?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{dest_port?, port %{dest_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}'), {
+               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 <var>%{limit.num}</var> packets per <var>%{limit.unit}</var>%{limit.burst? burst <var>%{limit.burst}</var>}'), { 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(_('<var data-tooltip="DNAT">Forward</var> to %{dest}%{dest_ip? IP <var>%{dest_ip}</var>}%{dest_port? port <var>%{dest_port}</var>}'), {
+               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;
index 6c6efc805f313be84020974cd5e7db0ad73a7866..cc85e667695f13eef9a3fe700745b17bbf6fc852 100644 (file)
 '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?<var>IPv4</var> and <var>IPv6</var>:<var>IPv6</var>}:<var>IPv4</var>}%{proto?, protocol %{proto#%{next?, }%{item.types?<var class="cbi-tooltip-container">%{item.name}<span class="cbi-tooltip">ICMP with types %{item.types#%{next?, }<var>%{item}</var>}</span></var>:<var>%{item.name}</var>}}}%{mark?, mark <var%{mark.inv? data-tooltip="Match fwmarks except %{mark.num}%{mark.mask? with mask %{mark.mask}}.":%{mark.mask? data-tooltip="Mask fwmark value with %{mark.mask} before compare."}}>%{mark.val}</var>}%{dscp?, DSCP %{dscp.inv?<var data-tooltip="Match DSCP classifications except %{dscp.num?:%{dscp.name}}">%{dscp.val}</var>:<var>%{dscp.val}</var>}}%{helper?, helper %{helper.inv?<var data-tooltip="Match any helper except &quot;%{helper.name}&quot;">%{helper.val}</var>:<var data-tooltip="%{helper.name}">%{helper.val}</var>}}'), {
+               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 <var>%{src_device}</var>}%{src_ip?, IP %{src_ip#%{next?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{src_port?, port %{src_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}%{src_mac?, MAC %{src_mac#%{next?, }<var%{item.inv? data-tooltip="Match MACs except %{item.val}%{item.hint.name? a.k.a. %{item.hint.name}}.":%{item.hint.name? data-tooltip="%{item.hint.name}"}}>%{item.ival}</var>}}'), {
+               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 <var>this device</var> with source %s and %s'), a, p, m);
-               else if (p || m)
-                       return fmt(_('From %s on <var>this device</var> with source %s'), a, p || m);
-               else
-                       return fmt(_('From %s on <var>this device</var>'), 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 <var>%{dest_device}</var>}%{dest_ip?, IP %{dest_ip#%{next?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{dest_port?, port %{dest_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}'), {
+               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 <var>this device</var>'), a, p);
-               else
-                       return fmt(_('To %s on <var>this device</var>'), a);
-       }
+       if (!l)
+               return '';
+
+       return fwtool.fmt(_('Limit matching to <var>%{limit.num}</var> packets per <var>%{limit.unit}</var>%{limit.burst? burst <var>%{limit.burst}</var>}'), { 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(_('<var>%s</var> and limit to %s'), t, l);
-       else
-               return fmt('<var>%s</var>', t);
-}
+       switch (t) {
+       case 'DROP':
+               return fwtool.fmt(_('<var data-tooltip="DROP">Drop</var> %{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(_('<var data-tooltip="ACCEPT">Accept</var> %{src?%{dest?forward:input}:output}'), s);
 
-       elem_src_ip.clearChoices();
-       elem_dst_ip.clearChoices();
+       case 'REJECT':
+               return fwtool.fmt(_('<var data-tooltip="REJECT">Reject</var> %{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 (<strong>%s</strong>)'.format(val, hosts[mac].name || mac);
+       case 'NOTRACK':
+               return fwtool.fmt(_('<var data-tooltip="NOTRACK">Do not track</var> %{src?%{dest?forward:input}:output}'), s);
 
-                       choice_values.push(val);
-                       choice_labels[val] = txt;
-               });
-       }
+       case 'HELPER':
+               return fwtool.fmt(_('<var data-tooltip="HELPER">Assign conntrack</var> helper <var%{helper_name? data-tooltip="%{helper_name}"}>%{set_helper}</var>'), s);
 
-       if (!family || family == 'ipv6') {
-               L.sortedKeys(hosts, 'ipv6', 'addr').forEach(function(mac) {
-                       var val = hosts[mac].ipv6,
-                           txt = '%s (<strong>%s</strong>)'.format(val, hosts[mac].name || mac);
+       case 'MARK':
+               return fwtool.fmt(_('<var data-tooltip="MARK">%{set_mark?Assign:XOR}</var> firewall mark <var>%{set_mark?:%{set_xmark}}</var>'), s);
 
-                       choice_values.push(val);
-                       choice_labels[val] = txt;
-               });
-       }
+       case 'DSCP':
+               return fwtool.fmt(_('<var data-tooltip="DSCP">Assign DSCP</var> classification <var>%{set_dscp}</var>'), 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;
index 919a418fe6a6c7fd15a87b71eb089766dc2f4abb..2db02d9444c18e4bc2507f944d9bb342426f46ca 100644 (file)
 '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?, }<var>%{item.name}</var>}}%{mark?, mark <var%{mark.inv? data-tooltip="Match fwmarks except %{mark.num}%{mark.mask? with mask %{mark.mask}}.":%{mark.mask? data-tooltip="Mask fwmark value with %{mark.mask} before compare."}}>%{mark.val}</var>}'), {
+               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 <var>%{src_device}</var>}%{src_ip?, IP %{src_ip#%{next?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{src_port?, port %{src_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}'), {
+               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 <var>%{dest_device}</var>}%{dest_ip?, IP %{dest_ip#%{next?, }<var%{item.inv? data-tooltip="Match IP addresses except %{item.val}."}>%{item.ival}</var>}}%{dest_port?, port %{dest_port#%{next?, }<var%{item.inv? data-tooltip="Match ports except %{item.val}."}>%{item.ival}</var>}}'), {
+               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 <var>%{limit.num}</var> packets per <var>%{limit.unit}</var>%{limit.burst? burst <var>%{limit.burst}</var>}'), { 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(_('<var data-tooltip="SNAT">Statically rewrite</var> to source %{snat_ip?IP <var>%{snat_ip}</var>} %{snat_port?port <var>%{snat_port}</var>}'), s);
+
+       case 'MASQUERADE':
+               return fwtool.fmt(_('<var data-tooltip="MASQUERADE">Automatically rewrite</var> source IP'));
+
+       case 'ACCEPT':
+               return fwtool.fmt(_('<var data-tooltip="ACCEPT">Prevent source rewrite</var>'));
+
+       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.'));