luci-app-firewall: allow ipv6 setup
[project/luci.git] / applications / luci-app-firewall / htdocs / luci-static / resources / view / firewall / snats.js
index b46791587a15f1070a0c1322744a48cb6d2d7073..a36dfee6bbbbe22bbaaf6050dfbc9386518f7099 100644 (file)
 'use strict';
+'require view';
 'require ui';
 '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 family = (uci.get('firewall', s, 'family') || '').toLowerCase().replace(/^(?:any|\*)$/, '');
+       var sip = uci.get('firewall', s, 'src_ip') || '';
+       var dip = uci.get('firewall', s, 'dest_ip') || '';
+       var rwip = uci.get('firewall', s, 'snat_ip') || '';
+       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 %{ipv6?%{ipv4?<var>IPv4</var> and <var>IPv6</var>:<var>IPv6</var>}:<var>IPv4</var>}%{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>}'), {
+               ipv4: (family == 'ipv4' || (!family && (sip.indexOf(':') == -1 && dip.indexOf(':') == -1 && rwip.indexOf(':') == -1))),
+               ipv6: (family == 'ipv6' || (!family && (sip.indexOf(':') != -1 || dip.indexOf(':') != -1 || rwip.indexOf(':') != -1))),
+               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': fwmodel.getZoneColorStyle(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': fwmodel.getZoneColorStyle(z) }, [(z == '*') ? E('em', _('any zone')) : (z ? E('strong', 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');
+       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;
        }
-       else if (t == 'ACCEPT') {
-               return _('Do not rewrite');
+}
+
+function validate_opt_family(m, section_id, opt) {
+       var sopt = m.section.getOption('src_ip'),
+           dopt = m.section.getOption('dest_ip'),
+           rwopt = m.section.getOption('snat_ip'),
+           fmopt = m.section.getOption('family'),
+           tgopt = m.section.getOption('target');
+
+       if (!sopt.isValid(section_id) && opt != 'src_ip')
+               return true;
+       if (!dopt.isValid(section_id) && opt != 'dest_ip')
+               return true;
+       if (!rwopt.isValid(section_id) && opt != 'snat_ip')
+               return true;
+       if (!fmopt.isValid(section_id) && opt != 'family')
+               return true;
+       if (!tgopt.isValid(section_id) && opt != 'target')
+               return true;
+
+       var sip = sopt.formvalue(section_id) || '',
+           dip = dopt.formvalue(section_id) || '',
+           rwip = rwopt.formvalue(section_id) || '',
+           fm = fmopt.formvalue(section_id) || '',
+           tg = tgopt.formvalue(section_id);
+
+       if (fm == 'ipv6' && (sip.indexOf(':') != -1 || sip == '') && (dip.indexOf(':') != -1 || dip == '') && ((rwip.indexOf(':') != -1 && tg == 'SNAT') || rwip == ''))
+               return true;
+       if (fm == 'ipv4' && (sip.indexOf(':') == -1) && (dip.indexOf(':') == -1) && ((rwip.indexOf(':') == -1 && tg == 'SNAT') || rwip == ''))
+               return true;
+       if (fm == '') {
+               if ((sip.indexOf(':') != -1 || sip == '') && (dip.indexOf(':') != -1 || dip == '') && ((rwip.indexOf(':') != -1 && tg == 'SNAT') || rwip == ''))
+                       return true;
+               if ((sip.indexOf(':') == -1) && (dip.indexOf(':') == -1) && ((rwip.indexOf(':') == -1 && tg == 'SNAT') || rwip == ''))
+                       return true;
        }
+
+       return _('Address family, source address, destination address, rewrite IP address must match');
 }
 
-return L.view.extend({
+return view.extend({
        callHostHints: rpc.declare({
                object: 'luci-rpc',
                method: 'getHostHints',
@@ -142,14 +151,23 @@ return L.view.extend({
        load: function() {
                return Promise.all([
                        this.callHostHints(),
-                       this.callNetworkDevices()
+                       this.callNetworkDevices(),
+                       uci.load('firewall')
                ]);
        },
 
        render: function(data) {
+               if (fwtool.checkLegacySNAT())
+                       return fwtool.renderMigration();
+               else
+                       return this.renderNats(data);
+       },
+
+       renderNats: function(data) {
                var hosts = data[0],
                    devs = data[1],
                    m, s, o;
+               var fw4 = L.hasSystemFeature('firewall4');
 
                m = new form.Map('firewall', _('Firewall - NAT Rules'),
                        _('NAT rules allow fine grained control over the source IP to use for outbound or forwarded traffic.'));
@@ -175,16 +193,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 +211,33 @@ return L.view.extend({
                o.default = o.enabled;
                o.editable = true;
 
-               o = s.taboption('general', form.Value, 'proto', _('Protocol'));
+               if (fw4) {
+                       o = s.taboption('general', form.ListValue, 'family', _('Restrict to address family'));
+                       o.modalonly = true;
+                       o.rmempty = true;
+                       o.value('ipv4', _('IPv4 only'));
+                       o.value('ipv6', _('IPv6 only'));
+                       o.value('', _('automatic'));  // infer from zone or used IP addresses
+                       o.cfgvalue = function(section_id) {
+                               var val = this.map.data.get(this.map.config, section_id, 'family');
+
+                               if (!val || val == 'any' || val == 'all' || val == '*')
+                                       return '';
+                               else if (val == 'inet' || String(val).indexOf('4') != -1)
+                                       return 'ipv4';
+                               else if (String(val).indexOf('6') != -1)
+                                       return 'ipv6';
+                       };
+                       o.validate = function(section_id, value) {
+                               fwtool.updateHostHints(this.map, section_id, 'src_ip', value, hosts);
+                               fwtool.updateHostHints(this.map, section_id, 'dest_ip', value, hosts);
+                               return !fw4?true:validate_opt_family(this, section_id, 'family');
+                       };
+               }
+
+               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 +246,13 @@ 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.'), !fw4?'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.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
+               o.validate = function(section_id, value) {
+                       return !fw4?true:validate_opt_family(this, section_id, 'src_ip');
+               };
 
                o = s.taboption('general', form.Value, 'src_port', _('Source port'),
                        _('Match forwarded traffic originating from the given source port or port range.'));
@@ -230,23 +260,16 @@ 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.'), !fw4?'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.datatype = !fw4?'neg(ipmask4("true"))':'neg(ipmask("true"))';
+               o.validate = function(section_id, value) {
+                       return !fw4?true:validate_opt_family(this, section_id, 'dest_ip');
+               };
 
                o = s.taboption('general', form.Value, 'dest_port', _('Destination port'),
                        _('Match forwarded traffic directed at the given destination port or port range.'));
@@ -254,10 +277,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;
@@ -265,36 +286,23 @@ return L.view.extend({
                o.value('SNAT', _('SNAT - Rewrite to specific source IP or port'));
                o.value('MASQUERADE', _('MASQUERADE - Automatically rewrite to outbound interface IP'));
                o.value('ACCEPT', _('ACCEPT - Disable address rewriting'));
+               o.validate = function(section_id, value) {
+                       return !fw4?true:validate_opt_family(this, section_id, 'target');
+               };
 
-               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),
-                           p = port ? port[0].formvalue(section_id) : null;
+                       var a = this.formvalue(section_id),
+                           p = this.section.formvalue(section_id, 'snat_port');
 
-                       if ((value == null || value == '') && (p == null || p == ''))
+                       if ((a == null || a == '') && (p == null || p == '') && value == '')
                                return _('A rewrite IP must be specified!');
 
-                       return true;
+                       return !fw4?true:validate_opt_family(this, section_id, 'snat_ip');
                };
-               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 +310,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.'));
@@ -313,62 +319,16 @@ return L.view.extend({
                o.modalonly = true;
                o.rmempty = true;
 
-               o = s.taboption('advanced', form.Value, 'mark', _('Match mark'),
-                       _('Matches a specific firewall mark or a range of different marks.'));
-               o.modalonly = true;
-               o.rmempty = true;
-               o.validate = function(section_id, value) {
-                       if (value == '')
-                               return true;
-
-                       var m = String(value).match(/^(?:!\s*)?(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i);
-
-                       if (!m || +m[1] > 0xffffffff || (m[2] != null && +m[2] > 0xffffffff))
-                               return _('Expecting: %s').format(_('valid firewall mark'));
-
-                       return true;
-               };
-
-               o = s.taboption('advanced', form.Value, 'limit', _('Limit matching'),
-                       _('Limits traffic matching to the specified rate.'));
-               o.modalonly = true;
-               o.rmempty = true;
-               o.placeholder = _('unlimited');
-               o.value('10/second');
-               o.value('60/minute');
-               o.value('3/hour');
-               o.value('500/day');
-               o.validate = function(section_id, value) {
-                       if (value == '')
-                               return true;
-
-                       var m = String(value).toLowerCase().match(/^(?:0x[0-9a-f]{1,8}|[0-9]{1,10})\/([a-z]+)$/),
-                           u = ['second', 'minute', 'hour', 'day'],
-                           i = 0;
-
-                       if (m)
-                               for (i = 0; i < u.length; i++)
-                                       if (u[i].indexOf(m[1]) == 0)
-                                               break;
-
-                       if (!m || i >= u.length)
-                               return _('Invalid limit value');
-
-                       return true;
-               };
-
-               o = s.taboption('advanced', form.Value, 'limit_burst', _('Limit burst'),
-                       _('Maximum initial number of packets to match: this number gets recharged by one every time the limit specified above is not reached, up to this number.'));
-               o.modalonly = true;
-               o.rmempty = true;
-               o.placeholder = '5';
-               o.datatype = 'uinteger';
-               o.depends({ limit: null, '!reverse': true });
+               fwtool.addMarkOption(s, false);
+               fwtool.addLimitOption(s);
+               fwtool.addLimitBurstOption(s);
 
-               o = s.taboption('advanced', form.Value, 'extra', _('Extra arguments'),
-                       _('Passes additional arguments to iptables. Use with care!'));
-               o.modalonly = true;
-               o.rmempty = true;
+               if (!L.hasSystemFeature('firewall4')) {
+                       o = s.taboption('advanced', form.Value, 'extra', _('Extra arguments'),
+                               _('Passes additional arguments to iptables. Use with care!'));
+                       o.modalonly = true;
+                       o.rmempty = true;
+               }
 
                o = s.taboption('timed', form.MultiValue, 'weekdays', _('Week Days'));
                o.modalonly = true;
@@ -397,11 +357,11 @@ return L.view.extend({
                for (var i = 1; i <= 31; i++)
                        o.value(i);
 
-               o = s.taboption('timed', form.Value, 'start_time', _('Start Time (hh.mm.ss)'));
+               o = s.taboption('timed', form.Value, 'start_time', _('Start Time (hh:mm:ss)'));
                o.modalonly = true;
                o.datatype = 'timehhmmss';
 
-               o = s.taboption('timed', form.Value, 'stop_time', _('Stop Time (hh.mm.ss)'));
+               o = s.taboption('timed', form.Value, 'stop_time', _('Stop Time (hh:mm:ss)'));
                o.modalonly = true;
                o.datatype = 'timehhmmss';