7 'require tools.prng as random';
11 'hopopt', 0, 'HOPOPT',
15 'ipencap', 4, 'IP-ENCAP',
23 'xns-idp', 22, 'XNS-IDP',
25 'iso-tp4', 29, 'ISO-TP4',
29 'idpr-cmtp', 38, 'IDPR-CMTP',
31 'ipv6-route', 43, 'IPv6-Route',
32 'ipv6-frag', 44, 'IPv6-Frag',
36 'esp', 50, 'IPSEC-ESP',
39 'icmpv6', 58, 'IPv6-ICMP',
40 'ipv6-icmp', 58, 'IPv6-ICMP',
41 'ipv6-nonxt', 59, 'IPv6-NoNxt',
42 'ipv6-opts', 60, 'IPv6-Opts',
47 'ospf', 89, 'OSPFIGP',
50 'etherip', 97, 'ETHERIP',
53 'ipcomp', 108, 'IPCOMP',
59 'mh', 135, 'Mobility-Header',
60 'ipv6-mh', 135, 'Mobility-Header',
61 'mobility-header', 135, 'Mobility-Header',
62 'udplite', 136, 'UDPLite',
63 'mpls-in-ip', 137, 'MPLS-in-IP',
64 'manet', 138, 'MANET',
66 'shim6', 140, 'Shim6',
71 function lookupProto(x
) {
72 if (x
== null || x
== '')
75 var s
= String(x
).toLowerCase();
77 for (var i
= 0; i
< protocols
.length
; i
+= 3)
78 if (s
== protocols
[i
] || s
== protocols
[i
+1])
79 return [ protocols
[i
+1], protocols
[i
+2], protocols
[i
] ];
84 return L
.Class
.extend({
85 fmt: function(fmtstr
, args
, values
) {
95 var get = function(args
, key
) {
96 var names
= key
.trim().split(/\./),
100 for (var i
= 0; i
< names
.length
; i
++) {
101 if (!L
.isObject(obj
))
108 if (typeof(obj
) == 'function')
109 return obj
.call(ctx
);
114 var isset = function(val
) {
115 if (L
.isObject(val
) && !L
.dom
.elem(val
)) {
117 if (val
.hasOwnProperty(k
))
122 else if (Array
.isArray(val
)) {
123 return (val
.length
> 0);
126 return (val
!== null && val
!== undefined && val
!== '' && val
!== false);
130 var parse = function(tokens
, text
) {
131 if (L
.dom
.elem(text
)) {
132 tokens
.push('<span data-fmt-placeholder="%d"></span>'.format(values
.length
));
136 tokens
.push(String(text
).replace(/\\(.)/g, '$1'));
140 for (var i
= 0, last
= 0; i
<= fmtstr
.length
; i
++) {
141 if (fmtstr
.charAt(i
) == '%' && fmtstr
.charAt(i
+ 1) == '{') {
143 parse(tokens
, fmtstr
.substring(last
, i
));
145 var j
= i
+ 1, nest
= 0;
149 for (var off
= j
+ 1, esc
= false; j
<= fmtstr
.length
; j
++) {
150 var ch
= fmtstr
.charAt(j
);
155 else if (ch
== '\\') {
158 else if (ch
== '{') {
161 else if (ch
== '}') {
163 subexpr
.push(fmtstr
.substring(off
, j
));
167 else if (ch
== '?' || ch
== ':' || ch
== '#') {
169 subexpr
.push(fmtstr
.substring(off
, j
));
176 var varname
= subexpr
[0].trim(),
177 op1
= (subexpr
[1] != null) ? subexpr
[1] : '?',
178 if_set
= (subexpr
[2] != null && subexpr
[2] != '') ? subexpr
[2] : '%{' + varname
+ '}',
179 op2
= (subexpr
[3] != null) ? subexpr
[3] : ':',
180 if_unset
= (subexpr
[4] != null) ? subexpr
[4] : '';
182 /* Invalid expression */
183 if (nest
!= 0 || subexpr
.length
> 5 || varname
== '') {
188 else if (op1
== '#' && subexpr
.length
== 3) {
189 var items
= L
.toArray(get(args
, varname
));
191 for (var k
= 0; k
< items
.length
; k
++) {
192 tokens
.push
.apply(tokens
, this.fmt(if_set
, Object
.assign({}, args
, {
195 last
: (k
+ 1) == items
.length
,
201 /* ternary expression */
202 else if (op1
== '?' && op2
== ':' && (subexpr
.length
== 1 || subexpr
.length
== 3 || subexpr
.length
== 5)) {
203 var val
= get(args
, varname
);
205 if (subexpr
.length
== 1)
206 parse(tokens
, isset(val
) ? val
: '');
208 tokens
.push
.apply(tokens
, this.fmt(if_set
, args
, values
));
210 tokens
.push
.apply(tokens
, this.fmt(if_unset
, args
, values
));
213 /* unrecognized command */
221 else if (i
>= fmtstr
.length
) {
223 parse(tokens
, fmtstr
.substring(last
, i
));
228 var node
= E('span', {}, tokens
.join('')),
229 repl
= node
.querySelectorAll('span[data-fmt-placeholder]');
231 for (var i
= 0; i
< repl
.length
; i
++)
232 repl
[i
].parentNode
.replaceChild(values
[repl
[i
].getAttribute('data-fmt-placeholder')], repl
[i
]);
241 map_invert: function(v
, fn
) {
242 return L
.toArray(v
).map(function(v
) {
245 if (fn
!= null && typeof(v
[fn
]) == 'function')
250 inv
: v
.charAt(0) == '!',
251 val
: v
.replace(/^!\s*/, '')
256 lookupProto
: lookupProto
,
258 addDSCPOption: function(s
, is_target
) {
259 var o
= s
.taboption(is_target
? 'general' : 'advanced', form
.Value
, is_target
? 'set_dscp' : 'dscp',
260 is_target
? _('DSCP mark') : _('Match DSCP'),
261 is_target
? _('Apply the given DSCP class or value to established connections.') : _('Matches traffic carrying the specified DSCP marking.'));
264 o
.rmempty
= !is_target
;
265 o
.placeholder
= _('any');
268 o
.depends('target', 'DSCP');
292 o
.validate = function(section_id
, value
) {
294 return is_target
? _('DSCP mark required') : true;
297 value
= String(value
).replace(/^!\s*/, '');
299 var m
= value
.match(/^(?:CS[0-7]|BE|AF[1234][123]|EF|(0x[0-9a-f]{1,2}|[0-9]{1,2}))$/);
301 if (!m
|| (m
[1] != null && +m
[1] > 0x3f))
302 return _('Invalid DSCP mark');
310 addMarkOption: function(s
, is_target
) {
311 var o
= s
.taboption(is_target
? 'general' : 'advanced', form
.Value
,
312 (is_target
> 1) ? 'set_xmark' : (is_target
? 'set_mark' : 'mark'),
313 (is_target
> 1) ? _('XOR mark') : (is_target
? _('Set mark') : _('Match mark')),
314 (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.') :
315 (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.') :
316 _('Matches a specific firewall mark or a range of different marks.')));
322 o
.depends('target', 'MARK_XOR');
324 o
.depends('target', 'MARK_SET');
326 o
.validate = function(section_id
, value
) {
328 return is_target
? _('Valid firewall mark required') : true;
331 value
= String(value
).replace(/^!\s*/, '');
333 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);
335 if (!m
|| +m
[1] > 0xffffffff || (m
[2] != null && +m
[2] > 0xffffffff))
336 return _('Expecting: %s').format(_('valid firewall mark'));
344 addLimitOption: function(s
) {
345 var o
= s
.taboption('advanced', form
.Value
, 'limit',
347 _('Limits traffic matching to the specified rate.'));
351 o
.placeholder
= _('unlimited');
352 o
.value('10/second');
353 o
.value('60/minute');
356 o
.validate = function(section_id
, value
) {
360 var m
= String(value
).toLowerCase().match(/^(?:0x[0-9a-f]{1,8}|[0-9]{1,10})\/([a-z]+)$/),
361 u
= ['second', 'minute', 'hour', 'day'],
365 for (i
= 0; i
< u
.length
; i
++)
366 if (u
[i
].indexOf(m
[1]) == 0)
369 if (!m
|| i
>= u
.length
)
370 return _('Invalid limit value');
378 addLimitBurstOption: function(s
) {
379 var o
= s
.taboption('advanced', form
.Value
, 'limit_burst',
381 _('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.'));
386 o
.datatype
= 'uinteger';
387 o
.depends({ limit
: null, '!reverse': true });
392 transformHostHints: function(family
, hosts
) {
393 var choice_values
= [], choice_labels
= {};
395 if (!family
|| family
== 'ipv4') {
396 L
.sortedKeys(hosts
, 'ipv4', 'addr').forEach(function(mac
) {
397 var val
= hosts
[mac
].ipv4
,
398 txt
= hosts
[mac
].name
|| mac
;
400 choice_values
.push(val
);
401 choice_labels
[val
] = E([], [ val
, ' (', E('strong', {}, [txt
]), ')' ]);
405 if (!family
|| family
== 'ipv6') {
406 L
.sortedKeys(hosts
, 'ipv6', 'addr').forEach(function(mac
) {
407 var val
= hosts
[mac
].ipv6
,
408 txt
= hosts
[mac
].name
|| mac
;
410 choice_values
.push(val
);
411 choice_labels
[val
] = E([], [ val
, ' (', E('strong', {}, [txt
]), ')' ]);
415 return [choice_values
, choice_labels
];
418 updateHostHints: function(map
, section_id
, option
, family
, hosts
) {
419 var opt
= map
.lookupOption(option
, section_id
)[0].getUIElement(section_id
),
420 choices
= this.transformHostHints(family
, hosts
);
423 opt
.addChoices(choices
[0], choices
[1]);
426 addIPOption: function(s
, tab
, name
, label
, description
, family
, hosts
, multiple
) {
427 var o
= s
.taboption(tab
, multiple
? form
.DynamicList
: form
.Value
, name
, label
, description
);
430 o
.datatype
= 'list(neg(ipmask))';
431 o
.placeholder
= multiple
? _('-- add IP --') : _('any');
433 if (family
!= null) {
434 var choices
= this.transformHostHints(family
, hosts
);
436 for (var i
= 0; i
< choices
[0].length
; i
++)
437 o
.value(choices
[0][i
], choices
[1][choices
[0][i
]]);
440 /* force combobox rendering */
441 o
.transformChoices = function() {
442 return this.super('transformChoices', []) || {};
448 addLocalIPOption: function(s
, tab
, name
, label
, description
, devices
) {
449 var o
= s
.taboption(tab
, form
.Value
, name
, label
, description
);
452 o
.datatype
= 'ip4addr("nomask")';
453 o
.placeholder
= _('any');
455 L
.sortedKeys(devices
, 'name').forEach(function(dev
) {
456 var ip4addrs
= devices
[dev
].ipaddrs
;
458 if (!L
.isObject(devices
[dev
].flags
) || !Array
.isArray(ip4addrs
) || devices
[dev
].flags
.loopback
)
461 for (var i
= 0; i
< ip4addrs
.length
; i
++) {
462 if (!L
.isObject(ip4addrs
[i
]) || !ip4addrs
[i
].address
)
465 o
.value(ip4addrs
[i
].address
, E([], [
466 ip4addrs
[i
].address
, ' (', E('strong', {}, [dev
]), ')'
474 addMACOption: function(s
, tab
, name
, label
, description
, hosts
) {
475 var o
= s
.taboption(tab
, form
.DynamicList
, name
, label
, description
);
478 o
.datatype
= 'list(macaddr)';
479 o
.placeholder
= _('-- add MAC --');
481 L
.sortedKeys(hosts
).forEach(function(mac
) {
482 o
.value(mac
, E([], [ mac
, ' (', E('strong', {}, [
483 hosts
[mac
].name
|| hosts
[mac
].ipv4
|| hosts
[mac
].ipv6
|| '?'
490 CBIProtocolSelect
: form
.MultiValue
.extend({
491 __name__
: 'CBI.ProtocolSelect',
493 addChoice: function(value
, label
) {
494 if (!Array
.isArray(this.keylist
) || this.keylist
.indexOf(value
) == -1)
495 this.value(value
, label
);
498 load: function(section_id
) {
499 var cfgvalue
= L
.toArray(this.super('load', [section_id
]) || this.default).sort();
501 ['all', 'tcp', 'udp', 'icmp'].concat(cfgvalue
).forEach(L
.bind(function(value
) {
506 this.addChoice('all', _('Any'));
510 this.addChoice('tcp', 'TCP');
511 this.addChoice('udp', 'UDP');
515 var m
= value
.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
516 p
= lookupProto(m
? +m
[1] : value
);
518 this.addChoice(p
[2], p
[1]);
526 renderWidget: function(section_id
, option_index
, cfgvalue
) {
527 var value
= (cfgvalue
!= null) ? cfgvalue
: this.default,
528 choices
= this.transformChoices();
530 var widget
= new ui
.Dropdown(L
.toArray(value
), choices
, {
531 id
: this.cbid(section_id
),
538 validate: function(value
) {
539 var v
= L
.toArray(value
);
541 for (var i
= 0; i
< v
.length
; i
++) {
545 var m
= v
[i
].match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/);
547 if (m
? (+m
[1] > 255) : (lookupProto(v
[i
])[0] == -1))
548 return _('Unrecognized protocol');
555 widget
.createChoiceElement = function(sb
, value
) {
556 var m
= value
.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
557 p
= lookupProto(lookupProto(m
? +m
[1] : value
)[0]);
559 return ui
.Dropdown
.prototype.createChoiceElement
.call(this, sb
, p
[2], p
[1]);
562 widget
.createItems = function(sb
, value
) {
563 var values
= L
.toArray(value
).map(function(value
) {
564 var m
= value
.match(/^(0x[0-9a-f]{1,2}|[0-9]{1,3})$/),
565 p
= lookupProto(m
? +m
[1] : value
);
567 return (p
[0] > -1) ? p
[2] : value
;
570 return ui
.Dropdown
.prototype.createItems
.call(this, sb
, values
.join(' '));
573 widget
.toggleItem = function(sb
, li
) {
574 var value
= li
.getAttribute('data-value'),
575 toggleFn
= ui
.Dropdown
.prototype.toggleItem
;
577 toggleFn
.call(this, sb
, li
);
579 if (value
== 'all') {
580 var items
= li
.parentNode
.querySelectorAll('li[data-value]');
582 for (var j
= 0; j
< items
.length
; j
++)
584 toggleFn
.call(this, sb
, items
[j
], false);
587 toggleFn
.call(this, sb
, li
.parentNode
.querySelector('li[data-value="all"]'), false);
591 return widget
.render();
595 checkLegacySNAT: function() {
596 var redirects
= uci
.sections('firewall', 'redirect');
598 for (var i
= 0; i
< redirects
.length
; i
++)
599 if ((redirects
[i
]['target'] || '').toLowerCase() == 'snat')
605 handleMigration: function(ev
) {
606 var redirects
= uci
.sections('firewall', 'redirect'),
612 reflection_src
: null,
614 src_dport
: 'snat_port',
618 for (var i
= 0; i
< redirects
.length
; i
++) {
619 if ((redirects
[i
]['target'] || '').toLowerCase() != 'snat')
622 var sid
= uci
.add('firewall', 'nat');
624 for (var opt
in redirects
[i
]) {
625 if (opt
.charAt(0) == '.')
628 if (mapping
[opt
] === null)
631 uci
.set('firewall', sid
, mapping
[opt
] || opt
, redirects
[i
][opt
]);
634 uci
.remove('firewall', redirects
[i
]['.name']);
638 .then(L
.bind(ui
.changes
.init
, ui
.changes
))
639 .then(L
.bind(ui
.changes
.apply
, ui
.changes
));
642 renderMigration: function() {
643 ui
.showModal(_('Firewall configuration migration'), [
644 E('p', _('The existing firewall configuration needs to be changed for LuCI to function properly.')),
645 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.')),
646 E('div', { 'class': 'right' },
648 'class': 'btn cbi-button-action important',
649 'click': ui
.createHandlerFn(this, 'handleMigration')