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