10 'require tools.prng as random';
14 'hopopt', 0, 'HOPOPT',
18 'ipencap', 4, 'IP-ENCAP',
26 'xns-idp', 22, 'XNS-IDP',
28 'iso-tp4', 29, 'ISO-TP4',
32 'idpr-cmtp', 38, 'IDPR-CMTP',
34 'ipv6-route', 43, 'IPv6-Route',
35 'ipv6-frag', 44, 'IPv6-Frag',
39 'esp', 50, 'IPSEC-ESP',
42 'icmpv6', 58, 'IPv6-ICMP',
43 'ipv6-icmp', 58, 'IPv6-ICMP',
44 'ipv6-nonxt', 59, 'IPv6-NoNxt',
45 'ipv6-opts', 60, 'IPv6-Opts',
50 'ospf', 89, 'OSPFIGP',
53 'etherip', 97, 'ETHERIP',
56 'ipcomp', 108, 'IPCOMP',
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',
69 'shim6', 140, 'Shim6',
74 function lookupProto(x
) {
75 if (x
== null || x
=== '')
78 var s
= String(x
).toLowerCase();
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
] ];
87 return baseclass
.extend({
88 fmt: function(fmtstr
, args
, values
) {
98 var get = function(args
, key
) {
99 var names
= key
.trim().split(/\./),
103 for (var i
= 0; i
< names
.length
; i
++) {
104 if (!L
.isObject(obj
))
111 if (typeof(obj
) == 'function')
112 return obj
.call(ctx
);
117 var isset = function(val
) {
118 if (L
.isObject(val
) && !dom
.elem(val
)) {
120 if (val
.hasOwnProperty(k
))
125 else if (Array
.isArray(val
)) {
126 return (val
.length
> 0);
129 return (val
!== null && val
!== undefined && val
!== '' && val
!== false);
133 var parse = function(tokens
, text
) {
134 if (dom
.elem(text
)) {
135 tokens
.push('<span data-fmt-placeholder="%d"></span>'.format(values
.length
));
139 tokens
.push(String(text
).replace(/\\(.)/g, '$1'));
143 for (var i
= 0, last
= 0; i
<= fmtstr
.length
; i
++) {
144 if (fmtstr
.charAt(i
) == '%' && fmtstr
.charAt(i
+ 1) == '{') {
146 parse(tokens
, fmtstr
.substring(last
, i
));
148 var j
= i
+ 1, nest
= 0;
152 for (var off
= j
+ 1, esc
= false; j
<= fmtstr
.length
; j
++) {
153 var ch
= fmtstr
.charAt(j
);
158 else if (ch
== '\\') {
161 else if (ch
== '{') {
164 else if (ch
== '}') {
166 subexpr
.push(fmtstr
.substring(off
, j
));
170 else if (ch
== '?' || ch
== ':' || ch
== '#') {
172 subexpr
.push(fmtstr
.substring(off
, j
));
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] : '';
185 /* Invalid expression */
186 if (nest
!= 0 || subexpr
.length
> 5 || varname
== '') {
191 else if (op1
== '#' && subexpr
.length
== 3) {
192 var items
= L
.toArray(get(args
, varname
));
194 for (var k
= 0; k
< items
.length
; k
++) {
195 tokens
.push
.apply(tokens
, this.fmt(if_set
, Object
.assign({}, args
, {
198 last
: (k
+ 1) == items
.length
,
204 /* ternary expression */
205 else if (op1
== '?' && op2
== ':' && (subexpr
.length
== 1 || subexpr
.length
== 3 || subexpr
.length
== 5)) {
206 var val
= get(args
, varname
);
208 if (subexpr
.length
== 1)
209 parse(tokens
, isset(val
) ? val
: '');
211 tokens
.push
.apply(tokens
, this.fmt(if_set
, args
, values
));
213 tokens
.push
.apply(tokens
, this.fmt(if_unset
, args
, values
));
216 /* unrecognized command */
224 else if (i
>= fmtstr
.length
) {
226 parse(tokens
, fmtstr
.substring(last
, i
));
231 var node
= E('span', {}, tokens
.join('')),
232 repl
= node
.querySelectorAll('span[data-fmt-placeholder]');
234 for (var i
= 0; i
< repl
.length
; i
++)
235 repl
[i
].parentNode
.replaceChild(values
[repl
[i
].getAttribute('data-fmt-placeholder')], repl
[i
]);
244 map_invert: function(v
, fn
) {
245 return L
.toArray(v
).map(function(v
) {
248 if (fn
!= null && typeof(v
[fn
]) == 'function')
253 inv
: v
.charAt(0) == '!',
254 val
: v
.replace(/^!\s*/, '')
259 lookupProto
: lookupProto
,
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.'));
267 o
.rmempty
= !is_target
;
268 o
.placeholder
= _('any');
271 o
.depends('target', 'DSCP');
295 o
.validate = function(section_id
, value
) {
297 return is_target
? _('DSCP mark required') : true;
300 value
= String(value
).replace(/^!\s*/, '');
302 var m
= value
.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);
304 if (!m
|| (m
[1] != null && +m
[1] > 0x3f))
305 return _('Invalid DSCP mark');
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.')));
325 o
.depends('target', 'MARK_XOR');
327 o
.depends('target', 'MARK_SET');
329 o
.validate = function(section_id
, value
) {
331 return is_target
? _('Valid firewall mark required') : true;
334 value
= String(value
).replace(/^!\s*/, '');
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);
338 if (!m
|| +m
[1] > 0xffffffff || (m
[2] != null && +m
[2] > 0xffffffff))
339 return _('Expecting: %s').format(_('valid firewall mark'));
347 addLimitOption: function(s
) {
348 var o
= s
.taboption('advanced', form
.Value
, 'limit',
350 _('Limits traffic matching to the specified rate.'));
354 o
.placeholder
= _('unlimited');
355 o
.value('10/second');
356 o
.value('60/minute');
359 o
.validate = function(section_id
, value
) {
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'],
368 for (i
= 0; i
< u
.length
; i
++)
369 if (u
[i
].indexOf(m
[1]) == 0)
372 if (!m
|| i
>= u
.length
)
373 return _('Invalid limit value');
381 addLimitBurstOption: function(s
) {
382 var o
= s
.taboption('advanced', form
.Value
, '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.'));
389 o
.datatype
= 'uinteger';
390 o
.depends({ limit
: null, '!reverse': true });
395 transformHostHints: function(family
, hosts
) {
396 var choice_values
= [],
401 for (var mac
in hosts
) {
402 L
.toArray(hosts
[mac
].ipaddrs
|| hosts
[mac
].ipv4
).forEach(function(ip
) {
406 L
.toArray(hosts
[mac
].ip6addrs
|| hosts
[mac
].ipv6
).forEach(function(ip
) {
411 if (!family
|| family
== 'ipv4') {
412 L
.sortedKeys(ipaddrs
, null, 'addr').forEach(function(ip
) {
414 txt
= hosts
[ipaddrs
[ip
]].name
|| ipaddrs
[ip
];
416 choice_values
.push(val
);
417 choice_labels
[val
] = E([], [ val
, ' (', E('strong', {}, [txt
]), ')' ]);
421 if (!family
|| family
== 'ipv6') {
422 L
.sortedKeys(ip6addrs
, null, 'addr').forEach(function(ip
) {
424 txt
= hosts
[ip6addrs
[ip
]].name
|| ip6addrs
[ip
];
426 choice_values
.push(val
);
427 choice_labels
[val
] = E([], [ val
, ' (', E('strong', {}, [txt
]), ')' ]);
431 return [choice_values
, choice_labels
];
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
);
439 opt
.addChoices(choices
[0], choices
[1]);
442 CBIDynamicMultiValueList
: form
.DynamicList
.extend({
443 renderWidget: function(/* ... */) {
444 var dl
= form
.DynamicList
.prototype.renderWidget
.apply(this, arguments
),
445 inst
= dom
.findClassInstance(dl
);
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);
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');
462 o
.datatype
= (fw4
&& validation
.types
.iprange
) ? 'list(neg(or(ipmask("true"),iprange)))' : 'list(neg(ipmask("true")))';
463 o
.placeholder
= multiple
? _('-- add IP --') : _('any');
465 if (family
!= null) {
466 var choices
= this.transformHostHints(family
, hosts
);
468 for (var i
= 0; i
< choices
[0].length
; i
++)
469 o
.value(choices
[0][i
], choices
[1][choices
[0][i
]]);
472 /* force combobox rendering */
473 o
.transformChoices = function() {
474 return this.super('transformChoices', []) || {};
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');
485 o
.datatype
= !fw4
?'ip4addr("nomask")':'ipaddr("nomask")';
486 o
.placeholder
= _('any');
488 L
.sortedKeys(devices
, 'name').forEach(function(dev
) {
489 var ip4addrs
= devices
[dev
].ipaddrs
;
490 var ip6addrs
= devices
[dev
].ip6addrs
;
492 if (!L
.isObject(devices
[dev
].flags
) || devices
[dev
].flags
.loopback
)
495 for (var i
= 0; Array
.isArray(ip4addrs
) && i
< ip4addrs
.length
; i
++) {
496 if (!L
.isObject(ip4addrs
[i
]) || !ip4addrs
[i
].address
)
499 o
.value(ip4addrs
[i
].address
, E([], [
500 ip4addrs
[i
].address
, ' (', E('strong', {}, [dev
]), ')'
503 for (var i
= 0; fw4
&& Array
.isArray(ip6addrs
) && i
< ip6addrs
.length
; i
++) {
504 if (!L
.isObject(ip6addrs
[i
]) || !ip6addrs
[i
].address
)
507 o
.value(ip6addrs
[i
].address
, E([], [
508 ip6addrs
[i
].address
, ' (', E('strong', {}, [dev
]), ')'
516 addMACOption: function(s
, tab
, name
, label
, description
, hosts
) {
517 var o
= s
.taboption(tab
, this.CBIDynamicMultiValueList
, name
, label
, description
);
520 o
.datatype
= 'list(macaddr)';
521 o
.placeholder
= _('-- add MAC --');
523 L
.sortedKeys(hosts
).forEach(function(mac
) {
524 o
.value(mac
, E([], [ mac
, ' (', E('strong', {}, [
526 L
.toArray(hosts
[mac
].ipaddrs
|| hosts
[mac
].ipv4
)[0] ||
527 L
.toArray(hosts
[mac
].ip6addrs
|| hosts
[mac
].ipv6
)[0] ||
535 CBIProtocolSelect
: form
.MultiValue
.extend({
536 __name__
: 'CBI.ProtocolSelect',
538 addChoice: function(value
, label
) {
539 if (!Array
.isArray(this.keylist
) || this.keylist
.indexOf(value
) == -1)
540 this.value(value
, label
);
543 load: function(section_id
) {
544 var cfgvalue
= L
.toArray(this.super('load', [section_id
]) || this.default).sort();
546 ['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue
).forEach(L
.bind(function(value
) {
551 this.addChoice('all', _('Any'));
555 this.addChoice('tcp', 'TCP');
556 this.addChoice('udp', 'UDP');
560 var m
= value
.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
561 p
= lookupProto(m
? +m
[1] : value
);
563 this.addChoice(p
[2], p
[1]);
568 if (cfgvalue
== '*' || cfgvalue
== 'any' || cfgvalue
== 'all')
574 renderWidget: function(section_id
, option_index
, cfgvalue
) {
575 var value
= (cfgvalue
!= null) ? cfgvalue
: this.default,
576 choices
= this.transformChoices();
578 var widget
= new ui
.Dropdown(L
.toArray(value
), choices
, {
579 id
: this.cbid(section_id
),
586 disabled
: (this.readonly
!= null) ? this.readonly
: this.map
.readonly
,
587 validate: function(value
) {
588 var v
= L
.toArray(value
);
590 for (var i
= 0; i
< v
.length
; i
++) {
594 var m
= v
[i
].match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
596 if (m
? (+m
[1] > 255) : (lookupProto(v
[i
])[0] == -1))
597 return _('Unrecognized protocol');
604 widget
.createChoiceElement = function(sb
, value
) {
605 var p
= lookupProto(value
);
607 return ui
.Dropdown
.prototype.createChoiceElement
.call(this, sb
, p
[2], p
[1]);
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
);
615 return (p
[0] > -1) ? p
[2] : p
[1];
620 return ui
.Dropdown
.prototype.createItems
.call(this, sb
, values
.join(' '));
623 widget
.toggleItem = function(sb
, li
) {
624 var value
= li
.getAttribute('data-value'),
625 toggleFn
= ui
.Dropdown
.prototype.toggleItem
;
627 toggleFn
.call(this, sb
, li
);
629 if (value
== 'all') {
630 var items
= li
.parentNode
.querySelectorAll('li[data-value]');
632 for (var j
= 0; j
< items
.length
; j
++)
634 toggleFn
.call(this, sb
, items
[j
], false);
637 toggleFn
.call(this, sb
, li
.parentNode
.querySelector('li[data-value="all"]'), false);
641 return widget
.render();
645 checkLegacySNAT: function() {
646 var redirects
= uci
.sections('firewall', 'redirect');
648 for (var i
= 0; i
< redirects
.length
; i
++)
649 if ((redirects
[i
]['target'] || '').toLowerCase() == 'snat')
655 handleMigration: function(ev
) {
656 var redirects
= uci
.sections('firewall', 'redirect'),
662 reflection_src
: null,
664 src_dport
: 'snat_port',
668 for (var i
= 0; i
< redirects
.length
; i
++) {
669 if ((redirects
[i
]['target'] || '').toLowerCase() != 'snat')
672 var sid
= uci
.add('firewall', 'nat');
674 for (var opt
in redirects
[i
]) {
675 if (opt
.charAt(0) == '.')
678 if (mapping
[opt
] === null)
681 uci
.set('firewall', sid
, mapping
[opt
] || opt
, redirects
[i
][opt
]);
684 uci
.remove('firewall', redirects
[i
]['.name']);
688 .then(L
.bind(ui
.changes
.init
, ui
.changes
))
689 .then(L
.bind(ui
.changes
.apply
, ui
.changes
));
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' },
698 'class': 'btn cbi-button-action important',
699 'click': ui
.createHandlerFn(this, 'handleMigration')