luci-mod-network: make condition for disabling legacy bridging more specific
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / tools / network.js
1 'use strict';
2 'require ui';
3 'require uci';
4 'require form';
5 'require network';
6 'require baseclass';
7 'require validation';
8 'require tools.widgets as widgets';
9
10 function validateAddr(section_id, value) {
11 if (value == '')
12 return true;
13
14 var ipv6 = /6$/.test(this.section.formvalue(section_id, 'mode')),
15 addr = ipv6 ? validation.parseIPv6(value) : validation.parseIPv4(value);
16
17 return addr ? true : (ipv6 ? _('Expecting a valid IPv6 address') : _('Expecting a valid IPv4 address'));
18 }
19
20 function setIfActive(section_id, value) {
21 if (this.isActive(section_id)) {
22 uci.set('network', section_id, this.ucioption, value);
23
24 /* Requires http://lists.openwrt.org/pipermail/openwrt-devel/2020-July/030397.html */
25 if (false && this.option == 'ifname_multi') {
26 var devname = this.section.formvalue(section_id, 'name_complex'),
27 m = devname ? devname.match(/^br-([A-Za-z0-9_]+)$/) : null;
28
29 if (m && uci.get('network', m[1], 'type') == 'bridge') {
30 uci.set('network', m[1], 'ifname', devname);
31 uci.unset('network', m[1], 'type');
32 }
33 }
34 }
35 }
36
37 function validateQoSMap(section_id, value) {
38 if (value == '')
39 return true;
40
41 var m = value.match(/^(\d+):(\d+)$/);
42
43 if (!m || +m[1] > 0xFFFFFFFF || +m[2] > 0xFFFFFFFF)
44 return _('Expecting two priority values separated by a colon');
45
46 return true;
47 }
48
49 function deviceSectionExists(section_id, devname, devtype) {
50 var exists = false;
51
52 uci.sections('network', 'device', function(ss) {
53 exists = exists || (ss['.name'] != section_id && ss.name == devname && (!devtype || devtype == ss.type));
54 });
55
56 return exists;
57 }
58
59 function isBridgePort(dev) {
60 if (!dev)
61 return false;
62
63 if (dev.isBridgePort())
64 return true;
65
66 var isPort = false;
67
68 uci.sections('network', null, function(s) {
69 if (s['.type'] != 'interface' && s['.type'] != 'device')
70 return;
71
72 if (s.type == 'bridge' && L.toArray(s.ifname).indexOf(dev.getName()) > -1)
73 isPort = true;
74 });
75
76 return isPort;
77 }
78
79 function renderDevBadge(dev) {
80 var type = dev.getType(), up = dev.isUp();
81
82 return E('span', { 'class': 'ifacebadge', 'style': 'font-weight:normal' }, [
83 E('img', {
84 'class': 'middle',
85 'src': L.resource('icons/%s%s.png').format(type, up ? '' : '_disabled')
86 }),
87 '\x0a', dev.getName()
88 ]);
89 }
90
91 function lookupDevName(s, section_id) {
92 var typeui = s.getUIElement(section_id, 'type'),
93 typeval = typeui ? typeui.getValue() : s.cfgvalue(section_id, 'type'),
94 ifnameui = s.getUIElement(section_id, 'ifname_single'),
95 ifnameval = ifnameui ? ifnameui.getValue() : s.cfgvalue(section_id, 'ifname_single');
96
97 return (typeval == 'bridge') ? 'br-%s'.format(section_id) : ifnameval;
98 }
99
100 function lookupDevSection(s, section_id, autocreate) {
101 var devname = lookupDevName(s, section_id),
102 devsection = null;
103
104 uci.sections('network', 'device', function(ds) {
105 if (ds.name == devname)
106 devsection = ds['.name'];
107 });
108
109 if (autocreate && !devsection) {
110 devsection = uci.add('network', 'device');
111 uci.set('network', devsection, 'name', devname);
112 }
113
114 return devsection;
115 }
116
117 function getDeviceValue(dev, method) {
118 if (dev && dev.getL3Device)
119 dev = dev.getL3Device();
120
121 if (dev && typeof(dev[method]) == 'function')
122 return dev[method].apply(dev);
123
124 return '';
125 }
126
127 function deviceCfgValue(section_id) {
128 if (arguments.length == 2)
129 return;
130
131 var ds = lookupDevSection(this.section, section_id, false);
132
133 return (ds ? uci.get('network', ds, this.option) : null) ||
134 uci.get('network', section_id, this.option) ||
135 this.default;
136 }
137
138 function deviceWrite(section_id, formvalue) {
139 var ds = lookupDevSection(this.section, section_id, true);
140
141 uci.set('network', ds, this.option, formvalue);
142 uci.unset('network', section_id, this.option);
143 }
144
145 function deviceRemove(section_id) {
146 var ds = lookupDevSection(this.section, section_id, false),
147 sv = ds ? uci.get('network', ds) : null;
148
149 if (sv) {
150 var empty = true;
151
152 for (var opt in sv) {
153 if (opt.charAt(0) == '.' || opt == 'name' || opt == this.option)
154 continue;
155
156 empty = false;
157 }
158
159 if (empty)
160 uci.remove('network', ds);
161 else
162 uci.unset('network', ds, this.option);
163 }
164
165 uci.unset('network', section_id, this.option);
166 }
167
168 function deviceRefresh(section_id) {
169 var dev = network.instantiateDevice(lookupDevName(this.section, section_id)),
170 uielem = this.getUIElement(section_id);
171
172 if (uielem) {
173 switch (this.option) {
174 case 'mtu':
175 case 'mtu6':
176 uielem.setPlaceholder(dev.getMTU());
177 break;
178
179 case 'macaddr':
180 uielem.setPlaceholder(dev.getMAC());
181 break;
182 }
183
184 uielem.setValue(this.cfgvalue(section_id));
185 }
186 }
187
188
189 var cbiTagValue = form.Value.extend({
190 renderWidget: function(section_id, option_index, cfgvalue) {
191 var widget = new ui.Dropdown(cfgvalue || ['-'], {
192 '-': E([], [
193 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '—' ]),
194 E('span', { 'class': 'hide-close' }, [ _('Do not participate', 'VLAN port state') ])
195 ]),
196 'u': E([], [
197 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 'u' ]),
198 E('span', { 'class': 'hide-close' }, [ _('Egress untagged', 'VLAN port state') ])
199 ]),
200 't': E([], [
201 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ 't' ]),
202 E('span', { 'class': 'hide-close' }, [ _('Egress tagged', 'VLAN port state') ])
203 ]),
204 '*': E([], [
205 E('span', { 'class': 'hide-open', 'style': 'font-family:monospace' }, [ '*' ]),
206 E('span', { 'class': 'hide-close' }, [ _('Primary VLAN ID', 'VLAN port state') ])
207 ])
208 }, {
209 id: this.cbid(section_id),
210 sort: [ '-', 'u', 't', '*' ],
211 optional: false,
212 multiple: true
213 });
214
215 var field = this;
216
217 widget.toggleItem = function(sb, li, force_state) {
218 var lis = li.parentNode.querySelectorAll('li'),
219 toggle = ui.Dropdown.prototype.toggleItem;
220
221 toggle.apply(this, [sb, li, force_state]);
222
223 if (force_state != null)
224 return;
225
226 switch (li.getAttribute('data-value'))
227 {
228 case '-':
229 if (li.hasAttribute('selected')) {
230 for (var i = 0; i < lis.length; i++) {
231 switch (lis[i].getAttribute('data-value')) {
232 case '-':
233 break;
234
235 case '*':
236 toggle.apply(this, [sb, lis[i], false]);
237 lis[i].setAttribute('unselectable', '');
238 break;
239
240 default:
241 toggle.apply(this, [sb, lis[i], false]);
242 }
243 }
244 }
245 break;
246
247 case 't':
248 case 'u':
249 if (li.hasAttribute('selected')) {
250 for (var i = 0; i < lis.length; i++) {
251 switch (lis[i].getAttribute('data-value')) {
252 case li.getAttribute('data-value'):
253 break;
254
255 case '*':
256 lis[i].removeAttribute('unselectable');
257 break;
258
259 default:
260 toggle.apply(this, [sb, lis[i], false]);
261 }
262 }
263 }
264 else {
265 toggle.apply(this, [sb, li, true]);
266 }
267 break;
268
269 case '*':
270 if (li.hasAttribute('selected')) {
271 var section_ids = field.section.cfgsections();
272
273 for (var i = 0; i < section_ids.length; i++) {
274 var other_widget = field.getUIElement(section_ids[i]),
275 other_value = L.toArray(other_widget.getValue());
276
277 if (other_widget === this)
278 continue;
279
280 var new_value = other_value.filter(function(v) { return v != '*' });
281
282 if (new_value.length == other_value.length)
283 continue;
284
285 other_widget.setValue(new_value);
286 break;
287 }
288 }
289 }
290 };
291
292 var node = widget.render();
293
294 node.style.minWidth = '4em';
295
296 if (cfgvalue == '-')
297 node.querySelector('li[data-value="*"]').setAttribute('unselectable', '');
298
299 return E('div', { 'style': 'display:inline-block' }, node);
300 },
301
302 cfgvalue: function(section_id) {
303 var pname = this.port,
304 spec = L.toArray(uci.get('network', section_id, 'ports')).filter(function(p) { return p.replace(/:[ut*]+$/, '') == pname })[0];
305
306 if (spec && spec.match(/t/))
307 return spec.match(/\*/) ? ['t', '*'] : ['t'];
308 else if (spec)
309 return spec.match(/\*/) ? ['u', '*'] : ['u'];
310 else
311 return ['-'];
312 },
313
314 write: function(section_id, value) {
315 var ports = [];
316
317 for (var i = 0; i < this.section.children.length; i++) {
318 var opt = this.section.children[i];
319
320 if (opt.port) {
321 var val = L.toArray(opt.formvalue(section_id)).join('');
322
323 switch (val) {
324 case '-':
325 break;
326
327 case 'u':
328 ports.push(opt.port);
329 break;
330
331 default:
332 ports.push('%s:%s'.format(opt.port, val));
333 break;
334 }
335 }
336 }
337
338 uci.set('network', section_id, 'ports', ports);
339 },
340
341 remove: function() {}
342 });
343
344 return baseclass.extend({
345 replaceOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) {
346 var o = s.getOption(optionName);
347
348 if (o) {
349 if (o.tab) {
350 s.tabs[o.tab].children = s.tabs[o.tab].children.filter(function(opt) {
351 return opt.option != optionName;
352 });
353 }
354
355 s.children = s.children.filter(function(opt) {
356 return opt.option != optionName;
357 });
358 }
359
360 return s.taboption(tabName, optionClass, optionName, optionTitle, optionDescription);
361 },
362
363 addOption: function(s, tabName, optionClass, optionName, optionTitle, optionDescription) {
364 var o = this.replaceOption(s, tabName, optionClass, optionName, optionTitle, optionDescription);
365
366 if (s.sectiontype == 'interface' && optionName != 'type' && optionName != 'vlan_filtering') {
367 o.cfgvalue = deviceCfgValue;
368 o.write = deviceWrite;
369 o.remove = deviceRemove;
370 o.refresh = deviceRefresh;
371 }
372
373 return o;
374 },
375
376 addDeviceOptions: function(s, dev, isNew) {
377 var isIface = (s.sectiontype == 'interface'),
378 ifc = isIface ? network.instantiateNetwork(s.section) : null,
379 gensection = ifc ? 'physical' : 'devgeneral',
380 advsection = ifc ? 'physical' : 'devadvanced',
381 simpledep = ifc ? { type: '', ifname_single: /^[^@]/ } : { type: '' },
382 disableLegacyBridging = isIface && deviceSectionExists(null, 'br-%s'.format(ifc.getName()), 'bridge'),
383 o, ss;
384
385 /* If an externally configured br-xxx interface already exists,
386 * then disable legacy bridge configuration */
387 if (disableLegacyBridging) {
388 o = this.addOption(s, gensection, form.HiddenValue, 'type');
389 o.cfgvalue = function() { return '' };
390 }
391 else if (isIface) {
392 var type;
393
394 type = this.addOption(s, gensection, form.Flag, 'type', _('Bridge interfaces'), _('Creates a bridge over specified interface(s)'));
395 type.modalonly = true;
396 type.disabled = '';
397 type.enabled = 'bridge';
398 type.write = type.remove = function(section_id, value) {
399 var protoname = this.section.formvalue(section_id, 'proto'),
400 protocol = network.getProtocol(protoname),
401 new_ifnames = this.isActive(section_id) ? L.toArray(this.section.formvalue(section_id, value ? 'ifname_multi' : 'ifname_single')) : [];
402
403 if (!protocol.isVirtual() && !this.isActive(section_id))
404 return;
405
406 var old_ifnames = [],
407 devs = ifc.getDevices() || L.toArray(ifc.getDevice());
408
409 for (var i = 0; i < devs.length; i++)
410 old_ifnames.push(devs[i].getName());
411
412 if (!value)
413 new_ifnames.length = Math.max(new_ifnames.length, 1);
414
415 old_ifnames.sort();
416 new_ifnames.sort();
417
418 for (var i = 0; i < Math.max(old_ifnames.length, new_ifnames.length); i++) {
419 if (old_ifnames[i] != new_ifnames[i]) {
420 // backup_ifnames()
421 for (var j = 0; j < old_ifnames.length; j++)
422 ifc.deleteDevice(old_ifnames[j]);
423
424 for (var j = 0; j < new_ifnames.length; j++)
425 ifc.addDevice(new_ifnames[j]);
426
427 break;
428 }
429 }
430
431 if (value)
432 uci.set('network', section_id, 'type', 'bridge');
433 else
434 uci.unset('network', section_id, 'type');
435 };
436 }
437 else {
438 s.tab('devgeneral', _('General device options'));
439 s.tab('devadvanced', _('Advanced device options'));
440 s.tab('brport', _('Bridge port specific options'));
441 s.tab('bridgevlan', _('Bridge VLAN filtering'));
442
443 o = this.addOption(s, gensection, form.ListValue, 'type', _('Device type'));
444 o.readonly = !isNew;
445 o.value('', _('Network device'));
446 o.value('bridge', _('Bridge device'));
447 o.value('8021q', _('VLAN (802.1q)'));
448 o.value('8021ad', _('VLAN (802.1ad)'));
449 o.value('macvlan', _('MAC VLAN'));
450 o.value('veth', _('Virtual Ethernet'));
451
452 o = this.addOption(s, gensection, widgets.DeviceSelect, 'name_simple', _('Existing device'));
453 o.readonly = !isNew;
454 o.rmempty = false;
455 o.noaliases = true;
456 o.default = (dev ? dev.getName() : '');
457 o.ucioption = 'name';
458 o.write = o.remove = setIfActive;
459 o.filter = function(section_id, value) {
460 return !deviceSectionExists(section_id, value);
461 };
462 o.validate = function(section_id, value) {
463 return deviceSectionExists(section_id, value) ? _('A configuration for the device "%s" already exists').format(value) : true;
464 };
465 o.depends('type', '');
466 }
467
468 o = this.addOption(s, gensection, widgets.DeviceSelect, 'ifname_single', isIface ? _('Interface') : _('Base device'));
469 o.readonly = !isNew;
470 o.rmempty = false;
471 o.noaliases = !isIface;
472 o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/\.\d+$/, '') : '';
473 o.ucioption = 'ifname';
474 o.validate = function(section_id, value) {
475 var type = this.section.formvalue(section_id, 'type'),
476 name = this.section.getUIElement(section_id, 'name_complex');
477
478 if (type == 'macvlan' && value && name && !name.isChanged()) {
479 var i = 0;
480
481 while (deviceSectionExists(section_id, '%smac%d'.format(value, i)))
482 i++;
483
484 name.setValue('%smac%d'.format(value, i));
485 name.triggerValidation();
486 }
487
488 return true;
489 };
490 if (isIface) {
491 o.write = o.remove = function() {};
492 o.cfgvalue = function(section_id) {
493 return (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) {
494 return dev.getName();
495 });
496 };
497 o.onchange = function(ev, section_id, values) {
498 for (var i = 0, co; (co = this.section.children[i]) != null; i++)
499 if (co !== this && co.refresh)
500 co.refresh(section_id);
501
502 };
503 o.depends('type', '');
504 }
505 else {
506 o.write = o.remove = setIfActive;
507 o.depends('type', '8021q');
508 o.depends('type', '8021ad');
509 o.depends('type', 'macvlan');
510 }
511
512 o = this.addOption(s, gensection, form.Value, 'vid', _('VLAN ID'));
513 o.readonly = !isNew;
514 o.datatype = 'range(1, 4094)';
515 o.rmempty = false;
516 o.default = (dev ? dev.getName() : '').match(/^.+\.\d+$/) ? dev.getName().replace(/^.+\./, '') : '';
517 o.validate = function(section_id, value) {
518 var base = this.section.formvalue(section_id, 'ifname_single'),
519 vid = this.section.formvalue(section_id, 'vid'),
520 name = this.section.getUIElement(section_id, 'name_complex');
521
522 if (base && vid && name && !name.isChanged()) {
523 name.setValue('%s.%d'.format(base, vid));
524 name.triggerValidation();
525 }
526
527 return true;
528 };
529 o.depends('type', '8021q');
530 o.depends('type', '8021ad');
531
532 o = this.addOption(s, gensection, form.ListValue, 'mode', _('Mode'));
533 o.value('vepa', _('VEPA (Virtual Ethernet Port Aggregator)', 'MACVLAN mode'));
534 o.value('private', _('Private (Prevent communication between MAC VLANs)', 'MACVLAN mode'));
535 o.value('bridge', _('Bridge (Support direct communication between MAC VLANs)', 'MACVLAN mode'));
536 o.value('passthru', _('Pass-through (Mirror physical device to single MAC VLAN)', 'MACVLAN mode'));
537 o.depends('type', 'macvlan');
538
539 if (!isIface) {
540 o = this.addOption(s, gensection, form.Value, 'name_complex', _('Device name'));
541 o.rmempty = false;
542 o.datatype = 'maxlength(15)';
543 o.readonly = !isNew;
544 o.ucioption = 'name';
545 o.write = o.remove = setIfActive;
546 o.validate = function(section_id, value) {
547 return deviceSectionExists(section_id, value) ? _('The device name "%s" is already taken').format(value) : true;
548 };
549 o.depends({ type: '', '!reverse': true });
550 }
551
552 o = this.addOption(s, advsection, form.DynamicList, 'ingress_qos_mapping', _('Ingress QoS mapping'), _('Defines a mapping of VLAN header priority to the Linux internal packet priority on incoming frames'));
553 o.rmempty = true;
554 o.validate = validateQoSMap;
555 o.depends('type', '8021q');
556 o.depends('type', '8021ad');
557
558 o = this.addOption(s, advsection, form.DynamicList, 'egress_qos_mapping', _('Egress QoS mapping'), _('Defines a mapping of Linux internal packet priority to VLAN header priority but for outgoing frames'));
559 o.rmempty = true;
560 o.validate = validateQoSMap;
561 o.depends('type', '8021q');
562 o.depends('type', '8021ad');
563
564 o = this.addOption(s, gensection, widgets.DeviceSelect, 'ifname_multi', _('Bridge ports'));
565 o.size = 10;
566 o.rmempty = true;
567 o.multiple = true;
568 o.noaliases = true;
569 o.nobridges = true;
570 o.ucioption = 'ifname';
571 if (isIface) {
572 o.write = o.remove = function() {};
573 o.cfgvalue = function(section_id) {
574 return (ifc.getDevices() || L.toArray(ifc.getDevice())).map(function(dev) { return dev.getName() });
575 };
576 }
577 else {
578 o.write = o.remove = setIfActive;
579 o.default = L.toArray(dev ? dev.getPorts() : null).filter(function(p) { return p.getType() != 'wifi' }).map(function(p) { return p.getName() });
580 o.filter = function(section_id, device_name) {
581 var bridge_name = uci.get('network', section_id, 'name'),
582 choice_dev = network.instantiateDevice(device_name),
583 parent_dev = choice_dev.getParent();
584
585 /* only show wifi networks which are already present in "option ifname" */
586 if (choice_dev.getType() == 'wifi') {
587 var ifnames = L.toArray(uci.get('network', section_id, 'ifname'));
588
589 for (var i = 0; i < ifnames.length; i++)
590 if (ifnames[i] == device_name)
591 return true;
592
593 return false;
594 }
595
596 return (!parent_dev || parent_dev.getName() != bridge_name);
597 };
598 o.description = _('Specifies the wired ports to attach to this bridge. In order to attach wireless networks, choose the associated interface as network in the wireless settings.')
599 }
600 o.onchange = function(ev, section_id, values) {
601 ss.updatePorts(values);
602
603 return ss.parse().then(function() {
604 ss.redraw();
605 });
606 };
607 o.depends('type', 'bridge');
608
609 o = this.replaceOption(s, gensection, form.Flag, 'bridge_empty', _('Bring up empty bridge'), _('Bring up the bridge interface even if no ports are attached'));
610 o.default = o.disabled;
611 o.depends('type', 'bridge');
612
613 o = this.replaceOption(s, advsection, form.Value, 'priority', _('Priority'));
614 o.placeholder = '32767';
615 o.datatype = 'range(0, 65535)';
616 o.depends('type', 'bridge');
617
618 o = this.replaceOption(s, advsection, form.Value, 'ageing_time', _('Ageing time'), _('Timeout in seconds for learned MAC addresses in the forwarding database'));
619 o.placeholder = '30';
620 o.datatype = 'uinteger';
621 o.depends('type', 'bridge');
622
623 o = this.replaceOption(s, advsection, form.Flag, 'stp', _('Enable <abbr title="Spanning Tree Protocol">STP</abbr>'), _('Enables the Spanning Tree Protocol on this bridge'));
624 o.default = o.disabled;
625 o.depends('type', 'bridge');
626
627 o = this.replaceOption(s, advsection, form.Value, 'hello_time', _('Hello interval'), _('Interval in seconds for STP hello packets'));
628 o.placeholder = '2';
629 o.datatype = 'range(1, 10)';
630 o.depends({ type: 'bridge', stp: '1' });
631
632 o = this.replaceOption(s, advsection, form.Value, 'forward_delay', _('Forward delay'), _('Time in seconds to spend in listening and learning states'));
633 o.placeholder = '15';
634 o.datatype = 'range(2, 30)';
635 o.depends({ type: 'bridge', stp: '1' });
636
637 o = this.replaceOption(s, advsection, form.Value, 'max_age', _('Maximum age'), _('Timeout in seconds until topology updates on link loss'));
638 o.placeholder = '20';
639 o.datatype = 'range(6, 40)';
640 o.depends({ type: 'bridge', stp: '1' });
641
642
643 o = this.replaceOption(s, advsection, form.Flag, 'igmp_snooping', _('Enable <abbr title="Internet Group Management Protocol">IGMP</abbr> snooping'), _('Enables IGMP snooping on this bridge'));
644 o.default = o.disabled;
645 o.depends('type', 'bridge');
646
647 o = this.replaceOption(s, advsection, form.Value, 'hash_max', _('Maximum snooping table size'));
648 o.placeholder = '512';
649 o.datatype = 'uinteger';
650 o.depends({ type: 'bridge', igmp_snooping: '1' });
651
652 o = this.replaceOption(s, advsection, form.Flag, 'multicast_querier', _('Enable multicast querier'));
653 o.defaults = { '1': [{'igmp_snooping': '1'}], '0': [{'igmp_snooping': '0'}] };
654 o.depends('type', 'bridge');
655
656 o = this.replaceOption(s, advsection, form.Value, 'robustness', _('Robustness'), _('The robustness value allows tuning for the expected packet loss on the network. If a network is expected to be lossy, the robustness value may be increased. IGMP is robust to (Robustness-1) packet losses'));
657 o.placeholder = '2';
658 o.datatype = 'min(1)';
659 o.depends({ type: 'bridge', multicast_querier: '1' });
660
661 o = this.replaceOption(s, advsection, form.Value, 'query_interval', _('Query interval'), _('Interval in centiseconds between multicast general queries. By varying the value, an administrator may tune the number of IGMP messages on the subnet; larger values cause IGMP Queries to be sent less often'));
662 o.placeholder = '12500';
663 o.datatype = 'uinteger';
664 o.depends({ type: 'bridge', multicast_querier: '1' });
665
666 o = this.replaceOption(s, advsection, form.Value, 'query_response_interval', _('Query response interval'), _('The max response time in centiseconds inserted into the periodic general queries. By varying the value, an administrator may tune the burstiness of IGMP messages on the subnet; larger values make the traffic less bursty, as host responses are spread out over a larger interval'));
667 o.placeholder = '1000';
668 o.datatype = 'uinteger';
669 o.validate = function(section_id, value) {
670 var qiopt = L.toArray(this.map.lookupOption('query_interval', section_id))[0],
671 qival = qiopt ? (qiopt.formvalue(section_id) || qiopt.placeholder) : '';
672
673 if (value != '' && qival != '' && +value >= +qival)
674 return _('The query response interval must be lower than the query interval value');
675
676 return true;
677 };
678 o.depends({ type: 'bridge', multicast_querier: '1' });
679
680 o = this.replaceOption(s, advsection, form.Value, 'last_member_interval', _('Last member interval'), _('The max response time in centiseconds inserted into group-specific queries sent in response to leave group messages. It is also the amount of time between group-specific query messages. This value may be tuned to modify the "leave latency" of the network. A reduced value results in reduced time to detect the loss of the last member of a group'));
681 o.placeholder = '100';
682 o.datatype = 'uinteger';
683 o.depends({ type: 'bridge', multicast_querier: '1' });
684
685 o = this.addOption(s, gensection, form.Value, 'mtu', _('MTU'));
686 o.placeholder = getDeviceValue(ifc || dev, 'getMTU');
687 o.datatype = 'max(9200)';
688 o.depends(simpledep);
689
690 o = this.addOption(s, gensection, form.Value, 'macaddr', _('MAC address'));
691 o.placeholder = getDeviceValue(ifc || dev, 'getMAC');
692 o.datatype = 'macaddr';
693 o.depends(simpledep);
694 o.depends('type', 'macvlan');
695 o.depends('type', 'veth');
696
697 o = this.addOption(s, gensection, form.Value, 'peer_name', _('Peer device name'));
698 o.rmempty = true;
699 o.datatype = 'maxlength(15)';
700 o.depends('type', 'veth');
701 o.load = function(section_id) {
702 var sections = uci.sections('network', 'device'),
703 idx = 0;
704
705 for (var i = 0; i < sections.length; i++)
706 if (sections[i]['.name'] == section_id)
707 break;
708 else if (sections[i].type == 'veth')
709 idx++;
710
711 this.placeholder = 'veth%d'.format(idx);
712
713 return form.Value.prototype.load.apply(this, arguments);
714 };
715
716 o = this.addOption(s, gensection, form.Value, 'peer_macaddr', _('Peer MAC address'));
717 o.rmempty = true;
718 o.datatype = 'macaddr';
719 o.depends('type', 'veth');
720
721 o = this.addOption(s, gensection, form.Value, 'txqueuelen', _('TX queue length'));
722 o.placeholder = dev ? dev._devstate('qlen') : '';
723 o.datatype = 'uinteger';
724 o.depends(simpledep);
725
726 o = this.addOption(s, advsection, form.Flag, 'promisc', _('Enable promiscious mode'));
727 o.default = o.disabled;
728 o.depends(simpledep);
729
730 o = this.addOption(s, advsection, form.ListValue, 'rpfilter', _('Reverse path filter'));
731 o.default = '';
732 o.value('', _('disabled'));
733 o.value('loose', _('Loose filtering'));
734 o.value('strict', _('Strict filtering'));
735 o.cfgvalue = function(section_id) {
736 var val = form.ListValue.prototype.cfgvalue.apply(this, [section_id]);
737
738 switch (val || '') {
739 case 'loose':
740 case '1':
741 return 'loose';
742
743 case 'strict':
744 case '2':
745 return 'strict';
746
747 default:
748 return '';
749 }
750 };
751 o.depends(simpledep);
752
753 o = this.addOption(s, advsection, form.Flag, 'acceptlocal', _('Accept local'), _('Accept packets with local source addresses'));
754 o.default = o.disabled;
755 o.depends(simpledep);
756
757 o = this.addOption(s, advsection, form.Flag, 'sendredirects', _('Send ICMP redirects'));
758 o.default = o.enabled;
759 o.depends(simpledep);
760
761 o = this.addOption(s, advsection, form.Value, 'neighreachabletime', _('Neighbour cache validity'), _('Time in milliseconds'));
762 o.placeholder = '30000';
763 o.datatype = 'uinteger';
764 o.depends(simpledep);
765
766 o = this.addOption(s, advsection, form.Value, 'neighgcstaletime', _('Stale neighbour cache timeout'), _('Timeout in seconds'));
767 o.placeholder = '60';
768 o.datatype = 'uinteger';
769 o.depends(simpledep);
770
771 o = this.addOption(s, advsection, form.Value, 'neighlocktime', _('Minimum ARP validity time'), _('Minimum required time in seconds before an ARP entry may be replaced. Prevents ARP cache thrashing.'));
772 o.placeholder = '0';
773 o.datatype = 'uinteger';
774 o.depends(simpledep);
775
776 o = this.addOption(s, gensection, form.Flag, 'ipv6', _('Enable IPv6'));
777 o.default = o.enabled;
778 o.depends(simpledep);
779
780 o = this.addOption(s, gensection, form.Value, 'mtu6', _('IPv6 MTU'));
781 o.placeholder = getDeviceValue(ifc || dev, 'getMTU');
782 o.datatype = 'max(9200)';
783 o.depends(Object.assign({ ipv6: '1' }, simpledep));
784
785 o = this.addOption(s, gensection, form.Value, 'dadtransmits', _('DAD transmits'), _('Amount of Duplicate Address Detection probes to send'));
786 o.placeholder = '1';
787 o.datatype = 'uinteger';
788 o.depends(Object.assign({ ipv6: '1' }, simpledep));
789
790
791 o = this.addOption(s, advsection, form.Flag, 'multicast', _('Enable multicast support'));
792 o.default = o.enabled;
793 o.depends(simpledep);
794
795 o = this.addOption(s, advsection, form.ListValue, 'igmpversion', _('Force IGMP version'));
796 o.value('', _('No enforcement'));
797 o.value('1', _('Enforce IGMPv1'));
798 o.value('2', _('Enforce IGMPv2'));
799 o.value('3', _('Enforce IGMPv3'));
800 o.depends(Object.assign({ multicast: '1' }, simpledep));
801
802 o = this.addOption(s, advsection, form.ListValue, 'mldversion', _('Force MLD version'));
803 o.value('', _('No enforcement'));
804 o.value('1', _('Enforce MLD version 1'));
805 o.value('2', _('Enforce MLD version 2'));
806 o.depends(Object.assign({ multicast: '1' }, simpledep));
807
808 if (isBridgePort(dev)) {
809 o = this.addOption(s, 'brport', form.Flag, 'learning', _('Enable MAC address learning'));
810 o.default = o.enabled;
811 o.depends(simpledep);
812
813 o = this.addOption(s, 'brport', form.Flag, 'unicast_flood', _('Enable unicast flooding'));
814 o.default = o.enabled;
815 o.depends(simpledep);
816
817 o = this.addOption(s, 'brport', form.Flag, 'isolated', _('Port isolation'), _('Only allow communication with non-isolated bridge ports when enabled'));
818 o.default = o.disabled;
819 o.depends(simpledep);
820
821 o = this.addOption(s, 'brport', form.ListValue, 'multicast_router', _('Multicast routing'));
822 o.value('', _('Never'));
823 o.value('1', _('Learn'));
824 o.value('2', _('Always'));
825 o.depends(Object.assign({ multicast: '1' }, simpledep));
826
827 o = this.addOption(s, 'brport', form.Flag, 'multicast_to_unicast', _('Multicast to unicast'), _('Forward multicast packets as unicast packets on this device.'));
828 o.default = o.disabled;
829 o.depends(Object.assign({ multicast: '1' }, simpledep));
830
831 o = this.addOption(s, 'brport', form.Flag, 'multicast_fast_leave', _('Enable multicast fast leave'));
832 o.default = o.disabled;
833 o.depends(Object.assign({ multicast: '1' }, simpledep));
834 }
835
836 o = this.addOption(s, 'bridgevlan', form.Flag, 'vlan_filtering', _('Enable VLAN filterering'));
837 o.depends('type', 'bridge');
838 o.updateDefaultValue = function(section_id) {
839 var device = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name'),
840 uielem = this.getUIElement(section_id),
841 has_vlans = false;
842
843 uci.sections('network', 'bridge-vlan', function(bvs) {
844 has_vlans = has_vlans || (bvs.device == device);
845 });
846
847 this.default = has_vlans ? this.enabled : this.disabled;
848
849 if (uielem && !uielem.isChanged())
850 uielem.setValue(this.default);
851 };
852
853 o = this.addOption(s, 'bridgevlan', form.SectionValue, 'bridge-vlan', form.TableSection, 'bridge-vlan');
854 o.depends('type', 'bridge');
855 o.renderWidget = function(/* ... */) {
856 return form.SectionValue.prototype.renderWidget.apply(this, arguments).then(L.bind(function(node) {
857 node.style.overflowX = 'auto';
858 node.style.overflowY = 'visible';
859 node.style.paddingBottom = '100px';
860 node.style.marginBottom = '-100px';
861
862 return node;
863 }, this));
864 };
865
866 ss = o.subsection;
867 ss.addremove = true;
868 ss.anonymous = true;
869
870 ss.renderHeaderRows = function(/* ... */) {
871 var node = form.TableSection.prototype.renderHeaderRows.apply(this, arguments);
872
873 node.querySelectorAll('.th').forEach(function(th) {
874 th.classList.add('middle');
875 });
876
877 return node;
878 };
879
880 ss.filter = function(section_id) {
881 var devname = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name');
882 return (uci.get('network', section_id, 'device') == devname);
883 };
884
885 ss.render = function(/* ... */) {
886 return form.TableSection.prototype.render.apply(this, arguments).then(L.bind(function(node) {
887 if (this.node)
888 this.node.parentNode.replaceChild(node, this.node);
889
890 this.node = node;
891
892 return node;
893 }, this));
894 };
895
896 ss.redraw = function() {
897 return this.load().then(L.bind(this.render, this));
898 };
899
900 ss.updatePorts = function(ports) {
901 var devices = ports.map(function(port) {
902 return network.instantiateDevice(port)
903 }).filter(function(dev) {
904 return dev.getType() != 'wifi' || dev.isUp();
905 });
906
907 this.children = this.children.filter(function(opt) { return !opt.option.match(/^port_/) });
908
909 for (var i = 0; i < devices.length; i++) {
910 o = ss.option(cbiTagValue, 'port_%s'.format(sfh(devices[i].getName())), renderDevBadge(devices[i]));
911 o.port = devices[i].getName();
912 }
913
914 var section_ids = this.cfgsections(),
915 device_names = devices.reduce(function(names, dev) { names[dev.getName()] = true; return names }, {});
916
917 for (var i = 0; i < section_ids.length; i++) {
918 var old_spec = L.toArray(uci.get('network', section_ids[i], 'ports')),
919 new_spec = old_spec.filter(function(spec) { return device_names[spec.replace(/:[ut*]+$/, '')] });
920
921 if (old_spec.length != new_spec.length)
922 uci.set('network', section_ids[i], 'ports', new_spec.length ? new_spec : null);
923 }
924 };
925
926 ss.handleAdd = function(ev) {
927 return s.parse().then(L.bind(function() {
928 var device = isIface ? 'br-%s'.format(s.section) : uci.get('network', s.section, 'name'),
929 section_ids = this.cfgsections(),
930 section_id = null,
931 max_vlan_id = 0;
932
933 if (!device)
934 return;
935
936 for (var i = 0; i < section_ids.length; i++) {
937 var vid = +uci.get('network', section_ids[i], 'vlan');
938
939 if (vid > max_vlan_id)
940 max_vlan_id = vid;
941 }
942
943 section_id = uci.add('network', 'bridge-vlan');
944 uci.set('network', section_id, 'device', device);
945 uci.set('network', section_id, 'vlan', max_vlan_id + 1);
946
947 s.children.forEach(function(opt) {
948 switch (opt.option) {
949 case 'type':
950 case 'name_complex':
951 var input = opt.map.findElement('id', 'widget.%s'.format(opt.cbid(s.section)));
952 if (input)
953 input.disabled = true;
954 break;
955 }
956 });
957
958 s.getOption('vlan_filtering').updateDefaultValue(s.section);
959
960 return this.redraw();
961 }, this));
962 };
963
964 o = ss.option(form.Value, 'vlan', _('VLAN ID'));
965 o.datatype = 'range(1, 4094)';
966
967 o.renderWidget = function(/* ... */) {
968 var node = form.Value.prototype.renderWidget.apply(this, arguments);
969
970 node.style.width = '5em';
971
972 return node;
973 };
974
975 o.validate = function(section_id, value) {
976 var section_ids = this.section.cfgsections();
977
978 for (var i = 0; i < section_ids.length; i++) {
979 if (section_ids[i] == section_id)
980 continue;
981
982 if (uci.get('network', section_ids[i], 'vlan') == value)
983 return _('The VLAN ID must be unique');
984 }
985
986 return true;
987 };
988
989 o = ss.option(form.Flag, 'local', _('Local'));
990 o.default = o.enabled;
991
992 /* Do not touch bridge port state from interface config if legacy
993 * bridge config is disabled due to explicitely declared br-xxx
994 * device section... */
995 if (disableLegacyBridging)
996 return;
997
998 var ports = [];
999
1000 if (isIface) {
1001 Array.prototype.push.apply(ports, L.toArray(ifc.getDevices() || ifc.getDevice()).map(function(dev) {
1002 return dev.getName();
1003 }));
1004 }
1005 else {
1006 var seen_ports = {};
1007
1008 L.toArray(uci.get('network', s.section, 'ifname')).forEach(function(ifname) {
1009 seen_ports[ifname] = true;
1010 });
1011
1012 uci.sections('network', 'bridge-vlan', function(bvs) {
1013 L.toArray(bvs.ports).forEach(function(portspec) {
1014 var m = portspec.match(/^([^:]+)(?::[ut*]+)?$/);
1015
1016 if (m)
1017 seen_ports[m[1]] = true;
1018 });
1019 });
1020
1021 for (var port_name in seen_ports)
1022 ports.push(port_name);
1023 }
1024
1025 ports.sort();
1026
1027 ss.updatePorts(ports);
1028 }
1029 });