luci-app-firewall: add address range inputs for traffic rules
[project/luci.git] / applications / luci-app-firewall / htdocs / luci-static / resources / tools / firewall.js
1 'use strict';
2 'require baseclass';
3 'require dom';
4 'require ui';
5 'require uci';
6 'require form';
7 'require network';
8 'require firewall';
9 'require validation';
10 'require tools.prng as random';
11
12 var protocols = [
13 'ip', 0, 'IP',
14 'hopopt', 0, 'HOPOPT',
15 'icmp', 1, 'ICMP',
16 'igmp', 2, 'IGMP',
17 'ggp', 3 , 'GGP',
18 'ipencap', 4, 'IP-ENCAP',
19 'st', 5, 'ST',
20 'tcp', 6, 'TCP',
21 'egp', 8, 'EGP',
22 'igp', 9, 'IGP',
23 'pup', 12, 'PUP',
24 'udp', 17, 'UDP',
25 'hmp', 20, 'HMP',
26 'xns-idp', 22, 'XNS-IDP',
27 'rdp', 27, 'RDP',
28 'iso-tp4', 29, 'ISO-TP4',
29 'dccp', 33, 'DCCP',
30 'xtp', 36, 'XTP',
31 'ddp', 37, 'DDP',
32 'idpr-cmtp', 38, 'IDPR-CMTP',
33 'ipv6', 41, 'IPv6',
34 'ipv6-route', 43, 'IPv6-Route',
35 'ipv6-frag', 44, 'IPv6-Frag',
36 'idrp', 45, 'IDRP',
37 'rsvp', 46, 'RSVP',
38 'gre', 47, 'GRE',
39 'esp', 50, 'IPSEC-ESP',
40 'ah', 51, 'IPSEC-AH',
41 'skip', 57, 'SKIP',
42 'icmpv6', 58, 'IPv6-ICMP',
43 'ipv6-icmp', 58, 'IPv6-ICMP',
44 'ipv6-nonxt', 59, 'IPv6-NoNxt',
45 'ipv6-opts', 60, 'IPv6-Opts',
46 'rspf', 73, 'RSPF',
47 'rspf', 73, 'CPHB',
48 'vmtp', 81, 'VMTP',
49 'eigrp', 88, 'EIGRP',
50 'ospf', 89, 'OSPFIGP',
51 'ax.25', 93, 'AX.25',
52 'ipip', 94, 'IPIP',
53 'etherip', 97, 'ETHERIP',
54 'encap', 98, 'ENCAP',
55 'pim', 103, 'PIM',
56 'ipcomp', 108, 'IPCOMP',
57 'vrrp', 112, 'VRRP',
58 'l2tp', 115, 'L2TP',
59 'isis', 124, 'ISIS',
60 'sctp', 132, 'SCTP',
61 'fc', 133, 'FC',
62 'mh', 135, 'Mobility-Header',
63 'ipv6-mh', 135, 'Mobility-Header',
64 'mobility-header', 135, 'Mobility-Header',
65 'udplite', 136, 'UDPLite',
66 'mpls-in-ip', 137, 'MPLS-in-IP',
67 'manet', 138, 'MANET',
68 'hip', 139, 'HIP',
69 'shim6', 140, 'Shim6',
70 'wesp', 141, 'WESP',
71 'rohc', 142, 'ROHC',
72 ];
73
74 function lookupProto(x) {
75 if (x == null || x === '')
76 return null;
77
78 var s = String(x).toLowerCase();
79
80 for (var i = 0; i < protocols.length; i += 3)
81 if (s == protocols[i] || s == protocols[i+1])
82 return [ protocols[i+1], protocols[i+2], protocols[i] ];
83
84 return [ -1, x, x ];
85 }
86
87 return baseclass.extend({
88 fmt: function(fmtstr, args, values) {
89 var repl = [],
90 wrap = false,
91 tokens = [];
92
93 if (values == null) {
94 values = [];
95 wrap = true;
96 }
97
98 var get = function(args, key) {
99 var names = key.trim().split(/\./),
100 obj = args,
101 ctx = obj;
102
103 for (var i = 0; i < names.length; i++) {
104 if (!L.isObject(obj))
105 return null;
106
107 ctx = obj;
108 obj = obj[names[i]];
109 }
110
111 if (typeof(obj) == 'function')
112 return obj.call(ctx);
113
114 return obj;
115 };
116
117 var isset = function(val) {
118 if (L.isObject(val) && !dom.elem(val)) {
119 for (var k in val)
120 if (val.hasOwnProperty(k))
121 return true;
122
123 return false;
124 }
125 else if (Array.isArray(val)) {
126 return (val.length > 0);
127 }
128 else {
129 return (val !== null && val !== undefined && val !== '' && val !== false);
130 }
131 };
132
133 var parse = function(tokens, text) {
134 if (dom.elem(text)) {
135 tokens.push('<span data-fmt-placeholder="%d"></span>'.format(values.length));
136 values.push(text);
137 }
138 else {
139 tokens.push(String(text).replace(/\\(.)/g, '$1'));
140 }
141 };
142
143 for (var i = 0, last = 0; i <= fmtstr.length; i++) {
144 if (fmtstr.charAt(i) == '%' && fmtstr.charAt(i + 1) == '{') {
145 if (i > last)
146 parse(tokens, fmtstr.substring(last, i));
147
148 var j = i + 1, nest = 0;
149
150 var subexpr = [];
151
152 for (var off = j + 1, esc = false; j <= fmtstr.length; j++) {
153 var ch = fmtstr.charAt(j);
154
155 if (esc) {
156 esc = false;
157 }
158 else if (ch == '\\') {
159 esc = true;
160 }
161 else if (ch == '{') {
162 nest++;
163 }
164 else if (ch == '}') {
165 if (--nest == 0) {
166 subexpr.push(fmtstr.substring(off, j));
167 break;
168 }
169 }
170 else if (ch == '?' || ch == ':' || ch == '#') {
171 if (nest == 1) {
172 subexpr.push(fmtstr.substring(off, j));
173 subexpr.push(ch);
174 off = j + 1;
175 }
176 }
177 }
178
179 var varname = subexpr[0].trim(),
180 op1 = (subexpr[1] != null) ? subexpr[1] : '?',
181 if_set = (subexpr[2] != null && subexpr[2] != '') ? subexpr[2] : '%{' + varname + '}',
182 op2 = (subexpr[3] != null) ? subexpr[3] : ':',
183 if_unset = (subexpr[4] != null) ? subexpr[4] : '';
184
185 /* Invalid expression */
186 if (nest != 0 || subexpr.length > 5 || varname == '') {
187 return fmtstr;
188 }
189
190 /* enumeration */
191 else if (op1 == '#' && subexpr.length == 3) {
192 var items = L.toArray(get(args, varname));
193
194 for (var k = 0; k < items.length; k++) {
195 tokens.push.apply(tokens, this.fmt(if_set, Object.assign({}, args, {
196 first: k == 0,
197 next: k > 0,
198 last: (k + 1) == items.length,
199 item: items[k]
200 }), values));
201 }
202 }
203
204 /* ternary expression */
205 else if (op1 == '?' && op2 == ':' && (subexpr.length == 1 || subexpr.length == 3 || subexpr.length == 5)) {
206 var val = get(args, varname);
207
208 if (subexpr.length == 1)
209 parse(tokens, isset(val) ? val : '');
210 else if (isset(val))
211 tokens.push.apply(tokens, this.fmt(if_set, args, values));
212 else
213 tokens.push.apply(tokens, this.fmt(if_unset, args, values));
214 }
215
216 /* unrecognized command */
217 else {
218 return fmtstr;
219 }
220
221 last = j + 1;
222 i = j;
223 }
224 else if (i >= fmtstr.length) {
225 if (i > last)
226 parse(tokens, fmtstr.substring(last, i));
227 }
228 }
229
230 if (wrap) {
231 var node = E('span', {}, tokens.join('')),
232 repl = node.querySelectorAll('span[data-fmt-placeholder]');
233
234 for (var i = 0; i < repl.length; i++)
235 repl[i].parentNode.replaceChild(values[repl[i].getAttribute('data-fmt-placeholder')], repl[i]);
236
237 return node;
238 }
239 else {
240 return tokens;
241 }
242 },
243
244 map_invert: function(v, fn) {
245 return L.toArray(v).map(function(v) {
246 v = String(v);
247
248 if (fn != null && typeof(v[fn]) == 'function')
249 v = v[fn].call(v);
250
251 return {
252 ival: v,
253 inv: v.charAt(0) == '!',
254 val: v.replace(/^!\s*/, '')
255 };
256 });
257 },
258
259 lookupProto: lookupProto,
260
261 addDSCPOption: function(s, is_target) {
262 var o = s.taboption(is_target ? 'general' : 'advanced', form.Value, is_target ? 'set_dscp' : 'dscp',
263 is_target ? _('DSCP mark') : _('Match DSCP'),
264 is_target ? _('Apply the given DSCP class or value to established connections.') : _('Matches traffic carrying the specified DSCP marking.'));
265
266 o.modalonly = true;
267 o.rmempty = !is_target;
268 o.placeholder = _('any');
269
270 if (is_target)
271 o.depends('target', 'DSCP');
272
273 o.value('CS0');
274 o.value('CS1');
275 o.value('CS2');
276 o.value('CS3');
277 o.value('CS4');
278 o.value('CS5');
279 o.value('CS6');
280 o.value('CS7');
281 o.value('BE');
282 o.value('AF11');
283 o.value('AF12');
284 o.value('AF13');
285 o.value('AF21');
286 o.value('AF22');
287 o.value('AF23');
288 o.value('AF31');
289 o.value('AF32');
290 o.value('AF33');
291 o.value('AF41');
292 o.value('AF42');
293 o.value('AF43');
294 o.value('EF');
295 o.validate = function(section_id, value) {
296 if (value == '')
297 return is_target ? _('DSCP mark required') : true;
298
299 if (!is_target)
300 value = String(value).replace(/^!\s*/, '');
301
302 var m = value.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);
303
304 if (!m || (m[1] != null && +m[1] > 0x3f))
305 return _('Invalid DSCP mark');
306
307 return true;
308 };
309
310 return o;
311 },
312
313 addMarkOption: function(s, is_target) {
314 var o = s.taboption(is_target ? 'general' : 'advanced', form.Value,
315 (is_target > 1) ? 'set_xmark' : (is_target ? 'set_mark' : 'mark'),
316 (is_target > 1) ? _('XOR mark') : (is_target ? _('Set mark') : _('Match mark')),
317 (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.') :
318 (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.') :
319 _('Matches a specific firewall mark or a range of different marks.')));
320
321 o.modalonly = true;
322 o.rmempty = true;
323
324 if (is_target > 1)
325 o.depends('target', 'MARK_XOR');
326 else if (is_target)
327 o.depends('target', 'MARK_SET');
328
329 o.validate = function(section_id, value) {
330 if (value == '')
331 return is_target ? _('Valid firewall mark required') : true;
332
333 if (!is_target)
334 value = String(value).replace(/^!\s*/, '');
335
336 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
338 if (!m || +m[1] > 0xffffffff || (m[2] != null && +m[2] > 0xffffffff))
339 return _('Expecting: %s').format(_('valid firewall mark'));
340
341 return true;
342 };
343
344 return o;
345 },
346
347 addLimitOption: function(s) {
348 var o = s.taboption('advanced', form.Value, 'limit',
349 _('Limit matching'),
350 _('Limits traffic matching to the specified rate.'));
351
352 o.modalonly = true;
353 o.rmempty = true;
354 o.placeholder = _('unlimited');
355 o.value('10/second');
356 o.value('60/minute');
357 o.value('3/hour');
358 o.value('500/day');
359 o.validate = function(section_id, value) {
360 if (value == '')
361 return true;
362
363 var m = String(value).toLowerCase().match(/^(?:0x[0-9a-f]{1,8}|[0-9]{1,10})\/([a-z]+)$/),
364 u = ['second', 'minute', 'hour', 'day'],
365 i = 0;
366
367 if (m)
368 for (i = 0; i < u.length; i++)
369 if (u[i].indexOf(m[1]) == 0)
370 break;
371
372 if (!m || i >= u.length)
373 return _('Invalid limit value');
374
375 return true;
376 };
377
378 return o;
379 },
380
381 addLimitBurstOption: function(s) {
382 var o = s.taboption('advanced', form.Value, 'limit_burst',
383 _('Limit burst'),
384 _('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.'));
385
386 o.modalonly = true;
387 o.rmempty = true;
388 o.placeholder = '5';
389 o.datatype = 'uinteger';
390 o.depends({ limit: null, '!reverse': true });
391
392 return o;
393 },
394
395 transformHostHints: function(family, hosts) {
396 var choice_values = [],
397 choice_labels = {},
398 ip6addrs = {},
399 ipaddrs = {};
400
401 for (var mac in hosts) {
402 L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4).forEach(function(ip) {
403 ipaddrs[ip] = mac;
404 });
405
406 L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6).forEach(function(ip) {
407 ip6addrs[ip] = mac;
408 });
409 }
410
411 if (!family || family == 'ipv4') {
412 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ip) {
413 var val = ip,
414 txt = hosts[ipaddrs[ip]].name || ipaddrs[ip];
415
416 choice_values.push(val);
417 choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
418 });
419 }
420
421 if (!family || family == 'ipv6') {
422 L.sortedKeys(ip6addrs, null, 'addr').forEach(function(ip) {
423 var val = ip,
424 txt = hosts[ip6addrs[ip]].name || ip6addrs[ip];
425
426 choice_values.push(val);
427 choice_labels[val] = E([], [ val, ' (', E('strong', {}, [txt]), ')' ]);
428 });
429 }
430
431 return [choice_values, choice_labels];
432 },
433
434 updateHostHints: function(map, section_id, option, family, hosts) {
435 var opt = map.lookupOption(option, section_id)[0].getUIElement(section_id),
436 choices = this.transformHostHints(family, hosts);
437
438 opt.clearChoices();
439 opt.addChoices(choices[0], choices[1]);
440 },
441
442 CBIDynamicMultiValueList: form.DynamicList.extend({
443 renderWidget: function(/* ... */) {
444 var dl = form.DynamicList.prototype.renderWidget.apply(this, arguments),
445 inst = dom.findClassInstance(dl);
446
447 inst.addItem = function(dl, value, text, flash) {
448 var values = L.toArray(value);
449 for (var i = 0; i < values.length; i++)
450 ui.DynamicList.prototype.addItem.call(this, dl, values[i], null, true);
451 };
452
453 return dl;
454 }
455 }),
456
457 addIPOption: function(s, tab, name, label, description, family, hosts, multiple) {
458 var o = s.taboption(tab, multiple ? this.CBIDynamicMultiValueList : form.Value, name, label, description);
459 var fw4 = L.hasSystemFeature('firewall4');
460
461 o.modalonly = true;
462 o.datatype = (fw4 && validation.types.iprange) ? 'list(neg(or(ipmask("true"),iprange)))' : 'list(neg(ipmask("true")))';
463 o.placeholder = multiple ? _('-- add IP --') : _('any');
464
465 if (family != null) {
466 var choices = this.transformHostHints(family, hosts);
467
468 for (var i = 0; i < choices[0].length; i++)
469 o.value(choices[0][i], choices[1][choices[0][i]]);
470 }
471
472 /* force combobox rendering */
473 o.transformChoices = function() {
474 return this.super('transformChoices', []) || {};
475 };
476
477 return o;
478 },
479
480 addLocalIPOption: function(s, tab, name, label, description, devices) {
481 var o = s.taboption(tab, form.Value, name, label, description);
482 var fw4 = L.hasSystemFeature('firewall4');
483
484 o.modalonly = true;
485 o.datatype = !fw4?'ip4addr("nomask")':'ipaddr("nomask")';
486 o.placeholder = _('any');
487
488 L.sortedKeys(devices, 'name').forEach(function(dev) {
489 var ip4addrs = devices[dev].ipaddrs;
490 var ip6addrs = devices[dev].ip6addrs;
491
492 if (!L.isObject(devices[dev].flags) || devices[dev].flags.loopback)
493 return;
494
495 for (var i = 0; Array.isArray(ip4addrs) && i < ip4addrs.length; i++) {
496 if (!L.isObject(ip4addrs[i]) || !ip4addrs[i].address)
497 continue;
498
499 o.value(ip4addrs[i].address, E([], [
500 ip4addrs[i].address, ' (', E('strong', {}, [dev]), ')'
501 ]));
502 }
503 for (var i = 0; fw4 && Array.isArray(ip6addrs) && i < ip6addrs.length; i++) {
504 if (!L.isObject(ip6addrs[i]) || !ip6addrs[i].address)
505 continue;
506
507 o.value(ip6addrs[i].address, E([], [
508 ip6addrs[i].address, ' (', E('strong', {}, [dev]), ')'
509 ]));
510 }
511 });
512
513 return o;
514 },
515
516 addMACOption: function(s, tab, name, label, description, hosts) {
517 var o = s.taboption(tab, this.CBIDynamicMultiValueList, name, label, description);
518
519 o.modalonly = true;
520 o.datatype = 'list(macaddr)';
521 o.placeholder = _('-- add MAC --');
522
523 L.sortedKeys(hosts).forEach(function(mac) {
524 o.value(mac, E([], [ mac, ' (', E('strong', {}, [
525 hosts[mac].name ||
526 L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0] ||
527 L.toArray(hosts[mac].ip6addrs || hosts[mac].ipv6)[0] ||
528 '?'
529 ]), ')' ]));
530 });
531
532 return o;
533 },
534
535 CBIProtocolSelect: form.MultiValue.extend({
536 __name__: 'CBI.ProtocolSelect',
537
538 addChoice: function(value, label) {
539 if (!Array.isArray(this.keylist) || this.keylist.indexOf(value) == -1)
540 this.value(value, label);
541 },
542
543 load: function(section_id) {
544 var cfgvalue = L.toArray(this.super('load', [section_id]) || this.default).sort();
545
546 ['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue).forEach(L.bind(function(value) {
547 switch (value) {
548 case 'all':
549 case 'any':
550 case '*':
551 this.addChoice('all', _('Any'));
552 break;
553
554 case 'tcpudp':
555 this.addChoice('tcp', 'TCP');
556 this.addChoice('udp', 'UDP');
557 break;
558
559 default:
560 var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
561 p = lookupProto(m ? +m[1] : value);
562
563 this.addChoice(p[2], p[1]);
564 break;
565 }
566 }, this));
567
568 if (cfgvalue == '*' || cfgvalue == 'any' || cfgvalue == 'all')
569 cfgvalue = 'all';
570
571 return cfgvalue;
572 },
573
574 renderWidget: function(section_id, option_index, cfgvalue) {
575 var value = (cfgvalue != null) ? cfgvalue : this.default,
576 choices = this.transformChoices();
577
578 var widget = new ui.Dropdown(L.toArray(value), choices, {
579 id: this.cbid(section_id),
580 sort: this.keylist,
581 multiple: true,
582 optional: false,
583 display_items: 10,
584 dropdown_items: -1,
585 create: true,
586 disabled: (this.readonly != null) ? this.readonly : this.map.readonly,
587 validate: function(value) {
588 var v = L.toArray(value);
589
590 for (var i = 0; i < v.length; i++) {
591 if (v[i] == 'all')
592 continue;
593
594 var m = v[i].match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
595
596 if (m ? (+m[1] > 255) : (lookupProto(v[i])[0] == -1))
597 return _('Unrecognized protocol');
598 }
599
600 return true;
601 }
602 });
603
604 widget.createChoiceElement = function(sb, value) {
605 var p = lookupProto(value);
606
607 return ui.Dropdown.prototype.createChoiceElement.call(this, sb, p[2], p[1]);
608 };
609
610 widget.createItems = function(sb, value) {
611 var values = L.toArray(value).map(function(value) {
612 var m = value.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
613 p = lookupProto(m ? +m[1] : value);
614
615 return (p[0] > -1) ? p[2] : p[1];
616 });
617
618 values.sort();
619
620 return ui.Dropdown.prototype.createItems.call(this, sb, values.join(' '));
621 };
622
623 widget.toggleItem = function(sb, li) {
624 var value = li.getAttribute('data-value'),
625 toggleFn = ui.Dropdown.prototype.toggleItem;
626
627 toggleFn.call(this, sb, li);
628
629 if (value == 'all') {
630 var items = li.parentNode.querySelectorAll('li[data-value]');
631
632 for (var j = 0; j < items.length; j++)
633 if (items[j] !== li)
634 toggleFn.call(this, sb, items[j], false);
635 }
636 else {
637 toggleFn.call(this, sb, li.parentNode.querySelector('li[data-value="all"]'), false);
638 }
639 };
640
641 return widget.render();
642 }
643 }),
644
645 checkLegacySNAT: function() {
646 var redirects = uci.sections('firewall', 'redirect');
647
648 for (var i = 0; i < redirects.length; i++)
649 if ((redirects[i]['target'] || '').toLowerCase() == 'snat')
650 return true;
651
652 return false;
653 },
654
655 handleMigration: function(ev) {
656 var redirects = uci.sections('firewall', 'redirect'),
657 tasks = [];
658
659 var mapping = {
660 dest: 'src',
661 reflection: null,
662 reflection_src: null,
663 src_dip: 'snat_ip',
664 src_dport: 'snat_port',
665 src: null
666 };
667
668 for (var i = 0; i < redirects.length; i++) {
669 if ((redirects[i]['target'] || '').toLowerCase() != 'snat')
670 continue;
671
672 var sid = uci.add('firewall', 'nat');
673
674 for (var opt in redirects[i]) {
675 if (opt.charAt(0) == '.')
676 continue;
677
678 if (mapping[opt] === null)
679 continue;
680
681 uci.set('firewall', sid, mapping[opt] || opt, redirects[i][opt]);
682 }
683
684 uci.remove('firewall', redirects[i]['.name']);
685 }
686
687 return uci.save()
688 .then(L.bind(ui.changes.init, ui.changes))
689 .then(L.bind(ui.changes.apply, ui.changes));
690 },
691
692 renderMigration: function() {
693 ui.showModal(_('Firewall configuration migration'), [
694 E('p', _('The existing firewall configuration needs to be changed for LuCI to function properly.')),
695 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.')),
696 E('div', { 'class': 'right' },
697 E('button', {
698 'class': 'btn cbi-button-action important',
699 'click': ui.createHandlerFn(this, 'handleMigration')
700 }, _('Continue')))
701 ]);
702 },
703 });