'use strict'; 'require baseclass'; 'require dom'; 'require ui'; 'require uci'; 'require form'; 'require network'; 'require firewall'; 'require tools.prng as random'; var protocols = [ 'ip', 0, 'IP', 'hopopt', 0, 'HOPOPT', 'icmp', 1, 'ICMP', 'igmp', 2, 'IGMP', 'ggp', 3 , 'GGP', 'ipencap', 4, 'IP-ENCAP', 'st', 5, 'ST', 'tcp', 6, 'TCP', 'egp', 8, 'EGP', 'igp', 9, 'IGP', 'pup', 12, 'PUP', 'udp', 17, 'UDP', 'hmp', 20, 'HMP', 'xns-idp', 22, 'XNS-IDP', 'rdp', 27, 'RDP', 'iso-tp4', 29, 'ISO-TP4', 'dccp', 33, 'DCCP', 'xtp', 36, 'XTP', 'ddp', 37, 'DDP', 'idpr-cmtp', 38, 'IDPR-CMTP', 'ipv6', 41, 'IPv6', 'ipv6-route', 43, 'IPv6-Route', 'ipv6-frag', 44, 'IPv6-Frag', 'idrp', 45, 'IDRP', 'rsvp', 46, 'RSVP', 'gre', 47, 'GRE', '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', 'rspf', 73, 'CPHB', 'vmtp', 81, 'VMTP', 'eigrp', 88, 'EIGRP', 'ospf', 89, 'OSPFIGP', 'ax.25', 93, 'AX.25', 'ipip', 94, 'IPIP', 'etherip', 97, 'ETHERIP', 'encap', 98, 'ENCAP', 'pim', 103, 'PIM', 'ipcomp', 108, 'IPCOMP', 'vrrp', 112, 'VRRP', 'l2tp', 115, 'L2TP', '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', 'manet', 138, 'MANET', 'hip', 139, 'HIP', 'shim6', 140, 'Shim6', 'wesp', 141, 'WESP', 'rohc', 142, 'ROHC', ]; function lookupProto(x) { if (x == null || x === '') return null; var s = String(x).toLowerCase(); for (var i = 0; i < protocols.length; i += 3) if (s == protocols[i] || s == protocols[i+1]) return [ protocols[i+1], protocols[i+2], protocols[i] ]; return [ -1, x, x ]; } return baseclass.extend({ fmt: function(fmtstr, args, values) { var repl = [], wrap = false, tokens = []; if (values == null) { values = []; wrap = true; } var get = function(args, key) { var names = key.trim().split(/\./), obj = args, ctx = obj; for (var i = 0; i < names.length; i++) { if (!L.isObject(obj)) return null; ctx = obj; obj = obj[names[i]]; } if (typeof(obj) == 'function') return obj.call(ctx); return obj; }; var isset = function(val) { if (L.isObject(val) && !dom.elem(val)) { for (var k in val) if (val.hasOwnProperty(k)) return true; return false; } else if (Array.isArray(val)) { return (val.length > 0); } else { return (val !== null && val !== undefined && val !== '' && val !== false); } }; var parse = function(tokens, text) { if (dom.elem(text)) { tokens.push(''.format(values.length)); values.push(text); } else { tokens.push(String(text).replace(/\\(.)/g, '$1')); } }; 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)); } } if (wrap) { var node = E('span', {}, tokens.join('')), repl = node.querySelectorAll('span[data-fmt-placeholder]'); for (var i = 0; i < repl.length; i++) repl[i].parentNode.replaceChild(values[repl[i].getAttribute('data-fmt-placeholder')], repl[i]); return node; } else { return tokens; } }, map_invert: function(v, fn) { return L.toArray(v).map(function(v) { v = String(v); if (fn != null && typeof(v[fn]) == 'function') v = v[fn].call(v); return { ival: v, inv: v.charAt(0) == '!', val: v.replace(/^!\s*/, '') }; }); }, lookupProto: lookupProto, addDSCPOption: function(s, is_target) { var o = s.taboption(is_target ? 'general' : 'advanced', form.Value, is_target ? 'set_dscp' : 'dscp', is_target ? _('DSCP mark') : _('Match DSCP'), is_target ? _('Apply the given DSCP class or value to established connections.') : _('Matches traffic carrying the specified DSCP marking.')); o.modalonly = true; o.rmempty = !is_target; o.placeholder = _('any'); if (is_target) o.depends('target', 'DSCP'); o.value('CS0'); o.value('CS1'); o.value('CS2'); o.value('CS3'); o.value('CS4'); o.value('CS5'); o.value('CS6'); o.value('CS7'); o.value('BE'); o.value('AF11'); o.value('AF12'); o.value('AF13'); o.value('AF21'); o.value('AF22'); o.value('AF23'); o.value('AF31'); o.value('AF32'); o.value('AF33'); o.value('AF41'); o.value('AF42'); o.value('AF43'); o.value('EF'); o.validate = function(section_id, value) { if (value == '') return is_target ? _('DSCP mark required') : true; if (!is_target) value = String(value).replace(/^!\s*/, ''); var m = value.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/); if (!m || (m[1] != null && +m[1] > 0x3f)) return _('Invalid DSCP mark'); return true; }; return o; }, addMarkOption: function(s, is_target) { var o = s.taboption(is_target ? 'general' : 'advanced', form.Value, (is_target > 1) ? 'set_xmark' : (is_target ? 'set_mark' : 'mark'), (is_target > 1) ? _('XOR mark') : (is_target ? _('Set mark') : _('Match mark')), (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.') : (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.') : _('Matches a specific firewall mark or a range of different marks.'))); o.modalonly = true; o.rmempty = true; if (is_target > 1) o.depends('target', 'MARK_XOR'); else if (is_target) o.depends('target', 'MARK_SET'); o.validate = function(section_id, value) { if (value == '') return is_target ? _('Valid firewall mark required') : true; if (!is_target) value = String(value).replace(/^!\s*/, ''); 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); if (!m || +m[1] > 0xffffffff || (m[2] != null && +m[2] > 0xffffffff)) return _('Expecting: %s').format(_('valid firewall mark')); return true; }; return o; }, addLimitOption: function(s) { var 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; }; return o; }, addLimitBurstOption: function(s) { var 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 }); return o; }, transformHostHints: function(family, hosts) { var choice_values = [], choice_labels = {}, ip6addrs = {}, ipaddrs = {}; for (var mac in hosts) { L.toArray(hosts[mac].ipaddrs).forEach(function(ip) { ipaddrs[ip] = mac; }); L.toArray(hosts[mac].ip6addrs).forEach(function(ip) { ip6addrs[ip] = mac; }); } if (!family || family == 'ipv4') { L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ip) { var val = ip, txt = hosts[ipaddrs[ip]].name || ipaddrs[ip]; choice_values.push(val); choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]); }); } if (!family || family == 'ipv6') { L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ip) { var val = ip, txt = hosts[ip6addrs[ip]].name || ip6addrs[ip]; 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]); }, CBIDynamicMultiValueList: form.DynamicList.extend({ renderWidget: function(/* ... */) { var dl = form.DynamicList.prototype.renderWidget.apply(this, arguments), inst = dom.findClassInstance(dl); inst.addItem = function(dl, value, text, flash) { var values = L.toArray(value); for (var i = 0; i < values.length; i++) ui.DynamicList.prototype.addItem.call(this, dl, values[i], null, true); }; return dl; } }), addIPOption: function(s, tab, name, label, description, family, hosts, multiple) { var o = s.taboption(tab, multiple ? this.CBIDynamicMultiValueList : form.Value, name, label, description); o.modalonly = true; o.datatype = 'list(neg(ipmask("true")))'; 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, this.CBIDynamicMultiValueList, 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].ipaddrs && hosts[mac].ipaddrs.length && hosts[mac].ipaddrs[0]) || (hosts[mac].ip6addrs && hosts[mac].ip6addrs.length && hosts[mac].ip6addrs[0]) || '?' ]), ')' ])); }); 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)); if (cfgvalue == '*' || cfgvalue == 'any' || cfgvalue == 'all') cfgvalue = 'all'; 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, disabled: (this.readonly != null) ? this.readonly : this.map.readonly, 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(); } }), checkLegacySNAT: function() { var redirects = uci.sections('firewall', 'redirect'); for (var i = 0; i < redirects.length; i++) if ((redirects[i]['target'] || '').toLowerCase() == 'snat') return true; return false; }, handleMigration: function(ev) { var redirects = uci.sections('firewall', 'redirect'), tasks = []; var mapping = { dest: 'src', reflection: null, reflection_src: null, src_dip: 'snat_ip', src_dport: 'snat_port', src: null }; for (var i = 0; i < redirects.length; i++) { if ((redirects[i]['target'] || '').toLowerCase() != 'snat') continue; var sid = uci.add('firewall', 'nat'); for (var opt in redirects[i]) { if (opt.charAt(0) == '.') continue; if (mapping[opt] === null) continue; uci.set('firewall', sid, mapping[opt] || opt, redirects[i][opt]); } uci.remove('firewall', redirects[i]['.name']); } return uci.save() .then(L.bind(ui.changes.init, ui.changes)) .then(L.bind(ui.changes.apply, ui.changes)); }, renderMigration: function() { ui.showModal(_('Firewall configuration migration'), [ E('p', _('The existing firewall configuration needs to be changed for LuCI to function properly.')), 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.')), E('div', { 'class': 'right' }, E('button', { 'class': 'btn cbi-button-action important', 'click': ui.createHandlerFn(this, 'handleMigration') }, _('Continue'))) ]); }, });