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