9 'require tools.prng as random';
13 'hopopt', 0, 'HOPOPT',
17 'ipencap', 4, 'IP-ENCAP',
25 'xns-idp', 22, 'XNS-IDP',
27 'iso-tp4', 29, 'ISO-TP4',
31 'idpr-cmtp', 38, 'IDPR-CMTP',
33 'ipv6-route', 43, 'IPv6-Route',
34 'ipv6-frag', 44, 'IPv6-Frag',
38 'esp', 50, 'IPSEC-ESP',
41 'icmpv6', 58, 'IPv6-ICMP',
42 'ipv6-icmp', 58, 'IPv6-ICMP',
43 'ipv6-nonxt', 59, 'IPv6-NoNxt',
44 'ipv6-opts', 60, 'IPv6-Opts',
49 'ospf', 89, 'OSPFIGP',
52 'etherip', 97, 'ETHERIP',
55 'ipcomp', 108, 'IPCOMP',
61 'mh', 135, 'Mobility-Header',
62 'ipv6-mh', 135, 'Mobility-Header',
63 'mobility-header', 135, 'Mobility-Header',
64 'udplite', 136, 'UDPLite',
65 'mpls-in-ip', 137, 'MPLS-in-IP',
66 'manet', 138, 'MANET',
68 'shim6', 140, 'Shim6',
73 function lookupProto(x) {
74 if (x == null || x === '')
77 var s = String(x).toLowerCase();
79 for (var i = 0; i < protocols.length; i += 3)
80 if (s == protocols[i] || s == protocols[i+1])
81 return [ protocols[i+1], protocols[i+2], protocols[i] ];
86 return baseclass.extend({
87 fmt: function(fmtstr, args, values) {
97 var get = function(args, key) {
98 var names = key.trim().split(/\./),
102 for (var i = 0; i < names.length; i++) {
103 if (!L.isObject(obj))
110 if (typeof(obj) == 'function')
111 return obj.call(ctx);
116 var isset = function(val) {
117 if (L.isObject(val) && !dom.elem(val)) {
119 if (val.hasOwnProperty(k))
124 else if (Array.isArray(val)) {
125 return (val.length > 0);
128 return (val !== null && val !== undefined && val !== '' && val !== false);
132 var parse = function(tokens, text) {
133 if (dom.elem(text)) {
134 tokens.push('<span data-fmt-placeholder="%d"></span>'.format(values.length));
138 tokens.push(String(text).replace(/\\(.)/g, '$1'));
142 for (var i = 0, last = 0; i <= fmtstr.length; i++) {
143 if (fmtstr.charAt(i) == '%' && fmtstr.charAt(i + 1) == '{') {
145 parse(tokens, fmtstr.substring(last, i));
147 var j = i + 1, nest = 0;
151 for (var off = j + 1, esc = false; j <= fmtstr.length; j++) {
152 var ch = fmtstr.charAt(j);
157 else if (ch == '\\') {
160 else if (ch == '{') {
163 else if (ch == '}') {
165 subexpr.push(fmtstr.substring(off, j));
169 else if (ch == '?' || ch == ':' || ch == '#') {
171 subexpr.push(fmtstr.substring(off, j));
178 var varname = subexpr[0].trim(),
179 op1 = (subexpr[1] != null) ? subexpr[1] : '?',
180 if_set = (subexpr[2] != null && subexpr[2] != '') ? subexpr[2] : '%{' + varname + '}',
181 op2 = (subexpr[3] != null) ? subexpr[3] : ':',
182 if_unset = (subexpr[4] != null) ? subexpr[4] : '';
184 /* Invalid expression */
185 if (nest != 0 || subexpr.length > 5 || varname == '') {
190 else if (op1 == '#' && subexpr.length == 3) {
191 var items = L.toArray(get(args, varname));
193 for (var k = 0; k < items.length; k++) {
194 tokens.push.apply(tokens, this.fmt(if_set, Object.assign({}, args, {
197 last: (k + 1) == items.length,
203 /* ternary expression */
204 else if (op1 == '?' && op2 == ':' && (subexpr.length == 1 || subexpr.length == 3 || subexpr.length == 5)) {
205 var val = get(args, varname);
207 if (subexpr.length == 1)
208 parse(tokens, isset(val) ? val : '');
210 tokens.push.apply(tokens, this.fmt(if_set, args, values));
212 tokens.push.apply(tokens, this.fmt(if_unset, args, values));
215 /* unrecognized command */
223 else if (i >= fmtstr.length) {
225 parse(tokens, fmtstr.substring(last, i));
230 var node = E('span', {}, tokens.join('')),
231 repl = node.querySelectorAll('span[data-fmt-placeholder]');
233 for (var i = 0; i < repl.length; i++)
234 repl[i].parentNode.replaceChild(values[repl[i].getAttribute('data-fmt-placeholder')], repl[i]);
243 map_invert: function(v, fn) {
244 return L.toArray(v).map(function(v) {
247 if (fn != null && typeof(v[fn]) == 'function')
252 inv: v.charAt(0) == '!',
253 val: v.replace(/^!\s*/, '')
258 lookupProto: lookupProto,
260 addDSCPOption: function(s, is_target) {
261 var o = s.taboption(is_target ? 'general' : 'advanced', form.Value, is_target ? 'set_dscp' : 'dscp',
262 is_target ? _('DSCP mark') : _('Match DSCP'),
263 is_target ? _('Apply the given DSCP class or value to established connections.') : _('Matches traffic carrying the specified DSCP marking.'));
266 o.rmempty = !is_target;
267 o.placeholder = _('any');
270 o.depends('target', 'DSCP');
294 o.validate = function(section_id, value) {
296 return is_target ? _('DSCP mark required') : true;
299 value = String(value).replace(/^!\s*/, '');
301 var m = value.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);
303 if (!m || (m[1] != null && +m[1] > 0x3f))
304 return _('Invalid DSCP mark');
312 addMarkOption: function(s, is_target) {
313 var o = s.taboption(is_target ? 'general' : 'advanced', form.Value,
314 (is_target > 1) ? 'set_xmark' : (is_target ? 'set_mark' : 'mark'),
315 (is_target > 1) ? _('XOR mark') : (is_target ? _('Set mark') : _('Match mark')),
316 (is_target > 1) ? _('Apply a bitwise XOR of the given value and the existing mark value on established connections. Format is value[/mask]. If a mask is specified then those bits set in the mask are zeroed out.') :
317 (is_target ? _('Set the given mark value on established connections. Format is value[/mask]. If a mask is specified then only those bits set in the mask are modified.') :
318 _('Matches a specific firewall mark or a range of different marks.')));
324 o.depends('target', 'MARK_XOR');
326 o.depends('target', 'MARK_SET');
328 o.validate = function(section_id, value) {
330 return is_target ? _('Valid firewall mark required') : true;
333 value = String(value).replace(/^!\s*/, '');
335 var m = value.match(/^(0x[0-9a-f]{1,8}|[0-9]{1,10})(?:\/(0x[0-9a-f]{1,8}|[0-9]{1,10}))?$/i);
337 if (!m || +m[1] > 0xffffffff || (m[2] != null && +m[2] > 0xffffffff))
338 return _('Expecting: %s').format(_('valid firewall mark'));
346 addLimitOption: function(s) {
347 var o = s.taboption('advanced', form.Value, 'limit',
349 _('Limits traffic matching to the specified rate.'));
353 o.placeholder = _('unlimited');
354 o.value('10/second');
355 o.value('60/minute');
358 o.validate = function(section_id, value) {
362 var m = String(value).toLowerCase().match(/^(?:0x[0-9a-f]{1,8}|[0-9]{1,10})\/([a-z]+)$/),
363 u = ['second', 'minute', 'hour', 'day'],
367 for (i = 0; i < u.length; i++)
368 if (u[i].indexOf(m[1]) == 0)
371 if (!m || i >= u.length)
372 return _('Invalid limit value');
380 addLimitBurstOption: function(s) {
381 var o = s.taboption('advanced', form.Value, 'limit_burst',
383 _('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.'));
388 o.datatype = 'uinteger';
389 o.depends({ limit: null, '!reverse': true });
394 transformHostHints: function(family, hosts) {
395 var choice_values = [], choice_labels = {};
397 if (!family || family == 'ipv4') {
398 L.sortedKeys(hosts, 'ipv4', 'addr').forEach(function(mac) {
399 var val = hosts[mac].ipv4,
400 txt = hosts[mac].name || mac;
402 choice_values.push(val);
403 choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
407 if (!family || family == 'ipv6') {
408 L.sortedKeys(hosts, 'ipv6', 'addr').forEach(function(mac) {
409 var val = hosts[mac].ipv6,
410 txt = hosts[mac].name || mac;
412 choice_values.push(val);
413 choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
417 return [choice_values, choice_labels];
420 updateHostHints: function(map, section_id, option, family, hosts) {
421 var opt = map.lookupOption(option, section_id)[0].getUIElement(section_id),
422 choices = this.transformHostHints(family, hosts);
425 opt.addChoices(choices[0], choices[1]);
428 addIPOption: function(s, tab, name, label, description, family, hosts, multiple) {
429 var o = s.taboption(tab, multiple ? form.DynamicList : form.Value, name, label, description);
432 o.datatype = 'list(neg(ipmask))';
433 o.placeholder = multiple ? _('-- add IP --') : _('any');
435 if (family != null) {
436 var choices = this.transformHostHints(family, hosts);
438 for (var i = 0; i < choices[0].length; i++)
439 o.value(choices[0][i], choices[1][choices[0][i]]);
442 /* force combobox rendering */
443 o.transformChoices = function() {
444 return this.super('transformChoices', []) || {};
450 addLocalIPOption: function(s, tab, name, label, description, devices) {
451 var o = s.taboption(tab, form.Value, name, label, description);
454 o.datatype = 'ip4addr("nomask")';
455 o.placeholder = _('any');
457 L.sortedKeys(devices, 'name').forEach(function(dev) {
458 var ip4addrs = devices[dev].ipaddrs;
460 if (!L.isObject(devices[dev].flags) || !Array.isArray(ip4addrs) || devices[dev].flags.loopback)
463 for (var i = 0; i < ip4addrs.length; i++) {
464 if (!L.isObject(ip4addrs[i]) || !ip4addrs[i].address)
467 o.value(ip4addrs[i].address, E([], [
468 ip4addrs[i].address, ' (', E('strong', {}, [dev]), ')'
476 addMACOption: function(s, tab, name, label, description, hosts) {
477 var o = s.taboption(tab, form.DynamicList, name, label, description);
480 o.datatype = 'list(macaddr)';
481 o.placeholder = _('-- add MAC --');
483 L.sortedKeys(hosts).forEach(function(mac) {
484 o.value(mac, E([], [ mac, ' (', E('strong', {}, [
485 hosts[mac].name || hosts[mac].ipv4 || hosts[mac].ipv6 || '?'
492 CBIProtocolSelect: form.MultiValue.extend({
493 __name__: 'CBI.ProtocolSelect',
495 addChoice: function(value, label) {
496 if (!Array.isArray(this.keylist) || this.keylist.indexOf(value) == -1)
497 this.value(value, label);
500 load: function(section_id) {
501 var cfgvalue = L.toArray(this.super('load', [section_id]) || this.default).sort();
503 ['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue).forEach(L.bind(function(value) {
508 this.addChoice('all', _('Any'));
512 this.addChoice('tcp', 'TCP');
513 this.addChoice('udp', 'UDP');
517 var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
518 p = lookupProto(m ? +m[1] : value);
520 this.addChoice(p[2], p[1]);
528 renderWidget: function(section_id, option_index, cfgvalue) {
529 var value = (cfgvalue != null) ? cfgvalue : this.default,
530 choices = this.transformChoices();
532 var widget = new ui.Dropdown(L.toArray(value), choices, {
533 id: this.cbid(section_id),
540 disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
541 validate: function(value) {
542 var v = L.toArray(value);
544 for (var i = 0; i < v.length; i++) {
548 var m = v[i].match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
550 if (m ? (+m[1] > 255) : (lookupProto(v[i])[0] == -1))
551 return _('Unrecognized protocol');
558 widget.createChoiceElement = function(sb, value) {
559 var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
560 p = lookupProto(lookupProto(m ? +m[1] : value)[0]);
562 return ui.Dropdown.prototype.createChoiceElement.call(this, sb, p[2], p[1]);
565 widget.createItems = function(sb, value) {
566 var values = L.toArray(value).map(function(value) {
567 var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
568 p = lookupProto(m ? +m[1] : value);
570 return (p[0] > -1) ? p[2] : value;
573 return ui.Dropdown.prototype.createItems.call(this, sb, values.join(' '));
576 widget.toggleItem = function(sb, li) {
577 var value = li.getAttribute('data-value'),
578 toggleFn = ui.Dropdown.prototype.toggleItem;
580 toggleFn.call(this, sb, li);
582 if (value == 'all') {
583 var items = li.parentNode.querySelectorAll('li[data-value]');
585 for (var j = 0; j < items.length; j++)
587 toggleFn.call(this, sb, items[j], false);
590 toggleFn.call(this, sb, li.parentNode.querySelector('li[data-value="all"]'), false);
594 return widget.render();
598 checkLegacySNAT: function() {
599 var redirects = uci.sections('firewall', 'redirect');
601 for (var i = 0; i < redirects.length; i++)
602 if ((redirects[i]['target'] || '').toLowerCase() == 'snat')
608 handleMigration: function(ev) {
609 var redirects = uci.sections('firewall', 'redirect'),
615 reflection_src: null,
617 src_dport: 'snat_port',
621 for (var i = 0; i < redirects.length; i++) {
622 if ((redirects[i]['target'] || '').toLowerCase() != 'snat')
625 var sid = uci.add('firewall', 'nat');
627 for (var opt in redirects[i]) {
628 if (opt.charAt(0) == '.')
631 if (mapping[opt] === null)
634 uci.set('firewall', sid, mapping[opt] || opt, redirects[i][opt]);
637 uci.remove('firewall', redirects[i]['.name']);
641 .then(L.bind(ui.changes.init, ui.changes))
642 .then(L.bind(ui.changes.apply, ui.changes));
645 renderMigration: function() {
646 ui.showModal(_('Firewall configuration migration'), [
647 E('p', _('The existing firewall configuration needs to be changed for LuCI to function properly.')),
648 E('p', _('Upon pressing "Continue", "redirect" sections with target "SNAT" will be converted to "nat" sections and the firewall will be restarted to apply the updated configuration.')),
649 E('div', { 'class': 'right' },
651 'class': 'btn cbi-button-action important',
652 'click': ui.createHandlerFn(this, 'handleMigration')