Merge pull request #5520 from acooler15/luci-lib-docker
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / interfaces.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require poll';
5 'require fs';
6 'require ui';
7 'require uci';
8 'require form';
9 'require network';
10 'require firewall';
11 'require tools.widgets as widgets';
12 'require tools.network as nettools';
13
14 var isReadonlyView = !L.hasViewPermission() || null;
15
16 function count_changes(section_id) {
17 var changes = ui.changes.changes, n = 0;
18
19 if (!L.isObject(changes))
20 return n;
21
22 if (Array.isArray(changes.network))
23 for (var i = 0; i < changes.network.length; i++)
24 n += (changes.network[i][1] == section_id);
25
26 if (Array.isArray(changes.dhcp))
27 for (var i = 0; i < changes.dhcp.length; i++)
28 n += (changes.dhcp[i][1] == section_id);
29
30 return n;
31 }
32
33 function render_iface(dev, alias) {
34 var type = dev ? dev.getType() : 'ethernet',
35 up = dev ? dev.isUp() : false;
36
37 return E('span', { class: 'cbi-tooltip-container' }, [
38 E('img', { 'class' : 'middle', 'src': L.resource('icons/%s%s.png').format(
39 alias ? 'alias' : type,
40 up ? '' : '_disabled') }),
41 E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [
42 E('img', { 'src': L.resource('icons/%s%s.png').format(
43 type, up ? '' : '_disabled') }),
44 L.itemlist(E('span', { 'class': 'left' }), [
45 _('Type'), dev ? dev.getTypeI18n() : null,
46 _('Device'), dev ? dev.getName() : _('Not present'),
47 _('Connected'), up ? _('yes') : _('no'),
48 _('MAC'), dev ? dev.getMAC() : null,
49 _('RX'), dev ? '%.2mB (%d %s)'.format(dev.getRXBytes(), dev.getRXPackets(), _('Pkts.')) : null,
50 _('TX'), dev ? '%.2mB (%d %s)'.format(dev.getTXBytes(), dev.getTXPackets(), _('Pkts.')) : null
51 ])
52 ])
53 ]);
54 }
55
56 function render_status(node, ifc, with_device) {
57 var desc = null, c = [];
58
59 if (ifc.isDynamic())
60 desc = _('Virtual dynamic interface');
61 else if (ifc.isAlias())
62 desc = _('Alias Interface');
63 else if (!uci.get('network', ifc.getName()))
64 return L.itemlist(node, [
65 null, E('em', _('Interface is marked for deletion'))
66 ]);
67
68 var i18n = ifc.getI18n();
69 if (i18n)
70 desc = desc ? '%s (%s)'.format(desc, i18n) : i18n;
71
72 var changecount = with_device ? 0 : count_changes(ifc.getName()),
73 ipaddrs = changecount ? [] : ifc.getIPAddrs(),
74 ip6addrs = changecount ? [] : ifc.getIP6Addrs(),
75 errors = ifc.getErrors(),
76 maindev = ifc.getL3Device() || ifc.getDevice(),
77 macaddr = maindev ? maindev.getMAC() : null;
78
79 return L.itemlist(node, [
80 _('Protocol'), with_device ? null : (desc || '?'),
81 _('Device'), with_device ? (maindev ? maindev.getShortName() : E('em', _('Not present'))) : null,
82 _('Uptime'), (!changecount && ifc.isUp()) ? '%t'.format(ifc.getUptime()) : null,
83 _('MAC'), (!changecount && !ifc.isDynamic() && !ifc.isAlias() && macaddr) ? macaddr : null,
84 _('RX'), (!changecount && !ifc.isDynamic() && !ifc.isAlias() && maindev) ? '%.2mB (%d %s)'.format(maindev.getRXBytes(), maindev.getRXPackets(), _('Pkts.')) : null,
85 _('TX'), (!changecount && !ifc.isDynamic() && !ifc.isAlias() && maindev) ? '%.2mB (%d %s)'.format(maindev.getTXBytes(), maindev.getTXPackets(), _('Pkts.')) : null,
86 _('IPv4'), ipaddrs[0],
87 _('IPv4'), ipaddrs[1],
88 _('IPv4'), ipaddrs[2],
89 _('IPv4'), ipaddrs[3],
90 _('IPv4'), ipaddrs[4],
91 _('IPv6'), ip6addrs[0],
92 _('IPv6'), ip6addrs[1],
93 _('IPv6'), ip6addrs[2],
94 _('IPv6'), ip6addrs[3],
95 _('IPv6'), ip6addrs[4],
96 _('IPv6'), ip6addrs[5],
97 _('IPv6'), ip6addrs[6],
98 _('IPv6'), ip6addrs[7],
99 _('IPv6'), ip6addrs[8],
100 _('IPv6'), ip6addrs[9],
101 _('IPv6-PD'), changecount ? null : ifc.getIP6Prefix(),
102 _('Information'), with_device ? null : (ifc.get('auto') != '0' ? null : _('Not started on boot')),
103 _('Error'), errors ? errors[0] : null,
104 _('Error'), errors ? errors[1] : null,
105 _('Error'), errors ? errors[2] : null,
106 _('Error'), errors ? errors[3] : null,
107 _('Error'), errors ? errors[4] : null,
108 null, changecount ? E('a', {
109 href: '#',
110 click: L.bind(ui.changes.displayChanges, ui.changes)
111 }, _('Interface has %d pending changes').format(changecount)) : null
112 ]);
113 }
114
115 function render_modal_status(node, ifc) {
116 var dev = ifc ? (ifc.getDevice() || ifc.getL3Device() || ifc.getL3Device()) : null;
117
118 dom.content(node, [
119 E('img', {
120 'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'),
121 'title': dev ? dev.getTypeI18n() : _('Not present')
122 }),
123 ifc ? render_status(E('span'), ifc, true) : E('em', _('Interface not present or not connected yet.'))
124 ]);
125
126 return node;
127 }
128
129 function render_ifacebox_status(node, ifc) {
130 var dev = ifc.getL3Device() || ifc.getDevice(),
131 subdevs = dev ? dev.getPorts() : null,
132 c = [ render_iface(dev, ifc.isAlias()) ];
133
134 if (subdevs && subdevs.length) {
135 var sifs = [ ' (' ];
136
137 for (var j = 0; j < subdevs.length; j++)
138 sifs.push(render_iface(subdevs[j]));
139
140 sifs.push(')');
141
142 c.push(E('span', {}, sifs));
143 }
144
145 c.push(E('br'));
146 c.push(E('small', {}, ifc.isAlias() ? _('Alias of "%s"').format(ifc.isAlias())
147 : (dev ? dev.getName() : E('em', _('Not present')))));
148
149 dom.content(node, c);
150
151 return firewall.getZoneByNetwork(ifc.getName()).then(L.bind(function(zone) {
152 this.style.backgroundColor = zone ? zone.getColor() : '#EEEEEE';
153 this.title = zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned');
154 }, node.previousElementSibling));
155 }
156
157 function iface_updown(up, id, ev, force) {
158 var row = document.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(id)),
159 dsc = row.querySelector('[data-name="_ifacestat"] > div'),
160 btns = row.querySelectorAll('.cbi-section-actions .reconnect, .cbi-section-actions .down');
161
162 btns[+!up].blur();
163 btns[+!up].classList.add('spinning');
164
165 btns[0].disabled = true;
166 btns[1].disabled = true;
167
168 if (!up) {
169 L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr')).then(function(res) {
170 var info = null; try { info = JSON.parse(res); } catch(e) {}
171
172 if (L.isObject(info) &&
173 Array.isArray(info.inbound_interfaces) &&
174 info.inbound_interfaces.filter(function(i) { return i == id })[0]) {
175
176 ui.showModal(_('Confirm disconnect'), [
177 E('p', _('You appear to be currently connected to the device via the "%h" interface. Do you really want to shut down the interface?').format(id)),
178 E('div', { 'class': 'right' }, [
179 E('button', {
180 'class': 'cbi-button cbi-button-neutral',
181 'click': function(ev) {
182 btns[1].classList.remove('spinning');
183 btns[1].disabled = false;
184 btns[0].disabled = false;
185
186 ui.hideModal();
187 }
188 }, _('Cancel')),
189 ' ',
190 E('button', {
191 'class': 'cbi-button cbi-button-negative important',
192 'click': function(ev) {
193 dsc.setAttribute('disconnect', '');
194 dom.content(dsc, E('em', _('Interface is shutting down...')));
195
196 ui.hideModal();
197 }
198 }, _('Disconnect'))
199 ])
200 ]);
201 }
202 else {
203 dsc.setAttribute('disconnect', '');
204 dom.content(dsc, E('em', _('Interface is shutting down...')));
205 }
206 });
207 }
208 else {
209 dsc.setAttribute(up ? 'reconnect' : 'disconnect', force ? 'force' : '');
210 dom.content(dsc, E('em', up ? _('Interface is reconnecting...') : _('Interface is shutting down...')));
211 }
212 }
213
214 function get_netmask(s, use_cfgvalue) {
215 var readfn = use_cfgvalue ? 'cfgvalue' : 'formvalue',
216 addrs = L.toArray(s[readfn](s.section, 'ipaddr')),
217 mask = s[readfn](s.section, 'netmask'),
218 firstsubnet = mask ? addrs[0] + '/' + mask : addrs.filter(function(a) { return a.indexOf('/') > 0 })[0];
219
220 if (firstsubnet == null)
221 return null;
222
223 var subnetmask = firstsubnet.split('/')[1];
224
225 if (!isNaN(subnetmask))
226 subnetmask = network.prefixToMask(+subnetmask);
227
228 return subnetmask;
229 }
230
231 function has_peerdns(proto) {
232 switch (proto) {
233 case 'dhcp':
234 case 'dhcpv6':
235 case 'qmi':
236 case 'ppp':
237 case 'pppoe':
238 case 'pppoa':
239 case 'pptp':
240 case 'openvpn':
241 case 'sstp':
242 return true;
243 }
244
245 return false;
246 }
247
248 var cbiRichListValue = form.ListValue.extend({
249 renderWidget: function(section_id, option_index, cfgvalue) {
250 var choices = this.transformChoices();
251 var widget = new ui.Dropdown((cfgvalue != null) ? cfgvalue : this.default, choices, {
252 id: this.cbid(section_id),
253 sort: this.keylist,
254 optional: true,
255 select_placeholder: this.select_placeholder || this.placeholder,
256 custom_placeholder: this.custom_placeholder || this.placeholder,
257 validate: L.bind(this.validate, this, section_id),
258 disabled: (this.readonly != null) ? this.readonly : this.map.readonly
259 });
260
261 return widget.render();
262 },
263
264 value: function(value, title, description) {
265 if (description) {
266 form.ListValue.prototype.value.call(this, value, E([], [
267 E('span', { 'class': 'hide-open' }, [ title ]),
268 E('div', { 'class': 'hide-close', 'style': 'min-width:25vw' }, [
269 E('strong', [ title ]),
270 E('br'),
271 E('span', { 'style': 'white-space:normal' }, description)
272 ])
273 ]));
274 }
275 else {
276 form.ListValue.prototype.value.call(this, value, title);
277 }
278 }
279 });
280
281 return view.extend({
282 poll_status: function(map, networks) {
283 var resolveZone = null;
284
285 for (var i = 0; i < networks.length; i++) {
286 var ifc = networks[i],
287 row = map.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(ifc.getName()));
288
289 if (row == null)
290 continue;
291
292 var dsc = row.querySelector('[data-name="_ifacestat"] > div'),
293 box = row.querySelector('[data-name="_ifacebox"] .ifacebox-body'),
294 btn1 = row.querySelector('.cbi-section-actions .reconnect'),
295 btn2 = row.querySelector('.cbi-section-actions .down'),
296 stat = document.querySelector('[id="%s-ifc-status"]'.format(ifc.getName())),
297 resolveZone = render_ifacebox_status(box, ifc),
298 disabled = ifc ? !ifc.isUp() : true,
299 dynamic = ifc ? ifc.isDynamic() : false;
300
301 if (dsc.hasAttribute('reconnect')) {
302 dom.content(dsc, E('em', _('Interface is starting...')));
303 }
304 else if (dsc.hasAttribute('disconnect')) {
305 dom.content(dsc, E('em', _('Interface is stopping...')));
306 }
307 else if (ifc.getProtocol() || uci.get('network', ifc.getName()) == null) {
308 render_status(dsc, ifc, false);
309 }
310 else if (!ifc.getProtocol()) {
311 var e = map.querySelector('[id="cbi-network-%s"] .cbi-button-edit'.format(ifc.getName()));
312 if (e) e.disabled = true;
313
314 var link = L.url('admin/system/opkg') + '?query=luci-proto';
315 dom.content(dsc, [
316 E('em', _('Unsupported protocol type.')), E('br'),
317 E('a', { href: link }, _('Install protocol extensions...'))
318 ]);
319 }
320 else {
321 dom.content(dsc, E('em', _('Interface not present or not connected yet.')));
322 }
323
324 if (stat) {
325 var dev = ifc.getDevice();
326 dom.content(stat, [
327 E('img', {
328 'src': L.resource('icons/%s%s.png').format(dev ? dev.getType() : 'ethernet', (dev && dev.isUp()) ? '' : '_disabled'),
329 'title': dev ? dev.getTypeI18n() : _('Not present')
330 }),
331 render_status(E('span'), ifc, true)
332 ]);
333 }
334
335 btn1.disabled = isReadonlyView || btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic;
336 btn2.disabled = isReadonlyView || btn1.classList.contains('spinning') || btn2.classList.contains('spinning') || dynamic || disabled;
337 }
338
339 document.querySelectorAll('.port-status-device[data-device]').forEach(function(node) {
340 nettools.updateDevBadge(node, network.instantiateDevice(node.getAttribute('data-device')));
341 });
342
343 document.querySelectorAll('.port-status-link[data-device]').forEach(function(node) {
344 nettools.updatePortStatus(node, network.instantiateDevice(node.getAttribute('data-device')));
345 });
346
347 return Promise.all([ resolveZone, network.flushCache() ]);
348 },
349
350 load: function() {
351 return Promise.all([
352 network.getDSLModemType(),
353 network.getDevices(),
354 fs.lines('/etc/iproute2/rt_tables'),
355 L.resolveDefault(fs.read('/usr/lib/opkg/info/netifd.control')),
356 uci.changes()
357 ]);
358 },
359
360 interfaceBridgeWithIfnameSections: function() {
361 return uci.sections('network', 'interface').filter(function(ns) {
362 return ns.type == 'bridge' && !ns.ports && ns.ifname;
363 });
364 },
365
366 deviceWithIfnameSections: function() {
367 return uci.sections('network', 'device').filter(function(ns) {
368 return ns.type == 'bridge' && !ns.ports && ns.ifname;
369 });
370 },
371
372 interfaceWithIfnameSections: function() {
373 return uci.sections('network', 'interface').filter(function(ns) {
374 return !ns.device && ns.ifname;
375 });
376 },
377
378 handleBridgeMigration: function(ev) {
379 var tasks = [];
380
381 this.interfaceBridgeWithIfnameSections().forEach(function(ns) {
382 var device_name = 'br-' + ns['.name'];
383
384 tasks.push(uci.callAdd('network', 'device', null, {
385 'name': device_name,
386 'type': 'bridge',
387 'ports': L.toArray(ns.ifname),
388 'mtu': ns.mtu,
389 'macaddr': ns.macaddr,
390 'igmp_snooping': ns.igmp_snooping
391 }));
392
393 tasks.push(uci.callSet('network', ns['.name'], {
394 'type': '',
395 'ifname': '',
396 'mtu': '',
397 'macaddr': '',
398 'igmp_snooping': '',
399 'device': device_name
400 }));
401 });
402
403 return Promise.all(tasks)
404 .then(L.bind(ui.changes.init, ui.changes))
405 .then(L.bind(ui.changes.apply, ui.changes));
406 },
407
408 renderBridgeMigration: function() {
409 ui.showModal(_('Network bridge configuration migration'), [
410 E('p', _('The existing network configuration needs to be changed for LuCI to function properly.')),
411 E('p', _('Upon pressing "Continue", bridges configuration will be updated and the network will be restarted to apply the updated configuration.')),
412 E('div', { 'class': 'right' },
413 E('button', {
414 'class': 'btn cbi-button-action important',
415 'click': ui.createHandlerFn(this, 'handleBridgeMigration')
416 }, _('Continue')))
417 ]);
418 },
419
420 handleIfnameMigration: function(ev) {
421 var tasks = [];
422
423 this.deviceWithIfnameSections().forEach(function(ds) {
424 tasks.push(uci.callSet('network', ds['.name'], {
425 'ifname': '',
426 'ports': L.toArray(ds.ifname)
427 }));
428 });
429
430 this.interfaceWithIfnameSections().forEach(function(ns) {
431 tasks.push(uci.callSet('network', ns['.name'], {
432 'ifname': '',
433 'device': ns.ifname
434 }));
435 });
436
437 return Promise.all(tasks)
438 .then(L.bind(ui.changes.init, ui.changes))
439 .then(L.bind(ui.changes.apply, ui.changes));
440 },
441
442 renderIfnameMigration: function() {
443 ui.showModal(_('Network ifname configuration migration'), [
444 E('p', _('The existing network configuration needs to be changed for LuCI to function properly.')),
445 E('p', _('Upon pressing "Continue", ifname options will get renamed and the network will be restarted to apply the updated configuration.')),
446 E('div', { 'class': 'right' },
447 E('button', {
448 'class': 'btn cbi-button-action important',
449 'click': ui.createHandlerFn(this, 'handleIfnameMigration')
450 }, _('Continue')))
451 ]);
452 },
453
454 render: function(data) {
455 var netifdVersion = (data[3] || '').match(/Version: ([^\n]+)/);
456
457 if (netifdVersion && netifdVersion[1] >= "2021-05-26") {
458 if (this.interfaceBridgeWithIfnameSections().length)
459 return this.renderBridgeMigration();
460 else if (this.deviceWithIfnameSections().length || this.interfaceWithIfnameSections().length)
461 return this.renderIfnameMigration();
462 }
463
464 var dslModemType = data[0],
465 netDevs = data[1],
466 m, s, o;
467
468 var rtTables = data[2].map(function(l) {
469 var m = l.trim().match(/^(\d+)\s+(\S+)$/);
470 return m ? [ +m[1], m[2] ] : null;
471 }).filter(function(e) {
472 return e && e[0] > 0;
473 });
474
475 m = new form.Map('network');
476 m.tabbed = true;
477 m.chain('dhcp');
478
479 s = m.section(form.GridSection, 'interface', _('Interfaces'));
480 s.anonymous = true;
481 s.addremove = true;
482 s.addbtntitle = _('Add new interface...');
483
484 s.load = function() {
485 return Promise.all([
486 network.getNetworks(),
487 firewall.getZones()
488 ]).then(L.bind(function(data) {
489 this.networks = data[0];
490 this.zones = data[1];
491 }, this));
492 };
493
494 s.tab('general', _('General Settings'));
495 s.tab('advanced', _('Advanced Settings'));
496 s.tab('physical', _('Physical Settings'));
497 s.tab('brport', _('Bridge port specific options'));
498 s.tab('bridgevlan', _('Bridge VLAN filtering'));
499 s.tab('firewall', _('Firewall Settings'));
500 s.tab('dhcp', _('DHCP Server'));
501
502 s.cfgsections = function() {
503 return this.networks.map(function(n) { return n.getName() })
504 .filter(function(n) { return n != 'loopback' });
505 };
506
507 s.modaltitle = function(section_id) {
508 return _('Interfaces') + ' » ' + section_id;
509 };
510
511 s.renderRowActions = function(section_id) {
512 var tdEl = this.super('renderRowActions', [ section_id, _('Edit') ]),
513 net = this.networks.filter(function(n) { return n.getName() == section_id })[0],
514 disabled = net ? !net.isUp() : true,
515 dynamic = net ? net.isDynamic() : false;
516
517 dom.content(tdEl.lastChild, [
518 E('button', {
519 'class': 'cbi-button cbi-button-neutral reconnect',
520 'click': iface_updown.bind(this, true, section_id),
521 'title': _('Reconnect this interface'),
522 'disabled': dynamic ? 'disabled' : null
523 }, _('Restart')),
524 E('button', {
525 'class': 'cbi-button cbi-button-neutral down',
526 'click': iface_updown.bind(this, false, section_id),
527 'title': _('Shutdown this interface'),
528 'disabled': (dynamic || disabled) ? 'disabled' : null
529 }, _('Stop')),
530 tdEl.lastChild.firstChild,
531 tdEl.lastChild.lastChild
532 ]);
533
534 if (!dynamic && net && !uci.get('network', net.getName())) {
535 tdEl.lastChild.childNodes[0].disabled = true;
536 tdEl.lastChild.childNodes[2].disabled = true;
537 tdEl.lastChild.childNodes[3].disabled = true;
538 }
539
540 return tdEl;
541 };
542
543 s.addModalOptions = function(s) {
544 var protoval = uci.get('network', s.section, 'proto'),
545 protoclass = protoval ? network.getProtocol(protoval) : null,
546 o, proto_select, proto_switch, type, stp, igmp, ss, so;
547
548 if (!protoval)
549 return;
550
551 return network.getNetwork(s.section).then(L.bind(function(ifc) {
552 var protocols = network.getProtocols();
553
554 protocols.sort(function(a, b) {
555 return L.naturalCompare(a.getProtocol(), b.getProtocol());
556 });
557
558 o = s.taboption('general', form.DummyValue, '_ifacestat_modal', _('Status'));
559 o.modalonly = true;
560 o.cfgvalue = L.bind(function(section_id) {
561 var net = this.networks.filter(function(n) { return n.getName() == section_id })[0];
562
563 return render_modal_status(E('div', {
564 'id': '%s-ifc-status'.format(section_id),
565 'class': 'ifacebadge large'
566 }), net);
567 }, this);
568 o.write = function() {};
569
570
571 proto_select = s.taboption('general', form.ListValue, 'proto', _('Protocol'));
572 proto_select.modalonly = true;
573
574 proto_switch = s.taboption('general', form.Button, '_switch_proto');
575 proto_switch.modalonly = true;
576 proto_switch.title = _('Really switch protocol?');
577 proto_switch.inputtitle = _('Switch protocol');
578 proto_switch.inputstyle = 'apply';
579 proto_switch.onclick = L.bind(function(ev) {
580 s.map.save()
581 .then(L.bind(m.load, m))
582 .then(L.bind(m.render, m))
583 .then(L.bind(this.renderMoreOptionsModal, this, s.section));
584 }, this);
585
586 o = s.taboption('general', widgets.DeviceSelect, '_net_device', _('Device'));
587 o.ucioption = 'device';
588 o.nobridges = false;
589 o.optional = false;
590 o.network = ifc.getName();
591
592 o = s.taboption('general', form.Flag, 'auto', _('Bring up on boot'));
593 o.modalonly = true;
594 o.default = o.enabled;
595
596 if (L.hasSystemFeature('firewall')) {
597 o = s.taboption('firewall', widgets.ZoneSelect, '_zone', _('Create / Assign firewall-zone'), _('Choose the firewall zone you want to assign to this interface. Select <em>unspecified</em> to remove the interface from the associated zone or fill out the <em>custom</em> field to define a new zone and attach the interface to it.'));
598 o.network = ifc.getName();
599 o.optional = true;
600
601 o.cfgvalue = function(section_id) {
602 return firewall.getZoneByNetwork(ifc.getName()).then(function(zone) {
603 return (zone != null ? zone.getName() : null);
604 });
605 };
606
607 o.write = o.remove = function(section_id, value) {
608 return Promise.all([
609 firewall.getZoneByNetwork(ifc.getName()),
610 (value != null) ? firewall.getZone(value) : null
611 ]).then(function(data) {
612 var old_zone = data[0],
613 new_zone = data[1];
614
615 if (old_zone == null && new_zone == null && (value == null || value == ''))
616 return;
617
618 if (old_zone != null && new_zone != null && old_zone.getName() == new_zone.getName())
619 return;
620
621 if (old_zone != null)
622 old_zone.deleteNetwork(ifc.getName());
623
624 if (new_zone != null)
625 new_zone.addNetwork(ifc.getName());
626 else if (value != null)
627 return firewall.addZone(value).then(function(new_zone) {
628 new_zone.addNetwork(ifc.getName());
629 });
630 });
631 };
632 }
633
634 for (var i = 0; i < protocols.length; i++) {
635 proto_select.value(protocols[i].getProtocol(), protocols[i].getI18n());
636
637 if (protocols[i].getProtocol() != uci.get('network', s.section, 'proto'))
638 proto_switch.depends('proto', protocols[i].getProtocol());
639 }
640
641 if (L.hasSystemFeature('dnsmasq') || L.hasSystemFeature('odhcpd')) {
642 o = s.taboption('dhcp', form.SectionValue, '_dhcp', form.TypedSection, 'dhcp');
643
644 ss = o.subsection;
645 ss.uciconfig = 'dhcp';
646 ss.addremove = false;
647 ss.anonymous = true;
648
649 ss.tab('general', _('General Setup'));
650 ss.tab('advanced', _('Advanced Settings'));
651 ss.tab('ipv6', _('IPv6 Settings'));
652 ss.tab('ipv6-ra', _('IPv6 RA Settings'));
653
654 ss.filter = function(section_id) {
655 return (uci.get('dhcp', section_id, 'interface') == ifc.getName());
656 };
657
658 ss.renderSectionPlaceholder = function() {
659 return E('div', { 'class': 'cbi-section-create' }, [
660 E('p', _('No DHCP Server configured for this interface') + ' &#160; '),
661 E('button', {
662 'class': 'cbi-button cbi-button-add',
663 'title': _('Set up DHCP Server'),
664 'click': ui.createHandlerFn(this, function(section_id, ev) {
665 this.map.save(function() {
666 uci.add('dhcp', 'dhcp', section_id);
667 uci.set('dhcp', section_id, 'interface', section_id);
668
669 if (protoval == 'static') {
670 uci.set('dhcp', section_id, 'start', 100);
671 uci.set('dhcp', section_id, 'limit', 150);
672 uci.set('dhcp', section_id, 'leasetime', '12h');
673 }
674 else {
675 uci.set('dhcp', section_id, 'ignore', 1);
676 }
677 });
678 }, ifc.getName())
679 }, _('Set up DHCP Server'))
680 ]);
681 };
682
683 ss.taboption('general', form.Flag, 'ignore', _('Ignore interface'), _('Disable <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> for this interface.'));
684
685 if (protoval == 'static') {
686 so = ss.taboption('general', form.Value, 'start', _('Start'), _('Lowest leased address as offset from the network address.'));
687 so.optional = true;
688 so.datatype = 'or(uinteger,ip4addr("nomask"))';
689 so.default = '100';
690
691 so = ss.taboption('general', form.Value, 'limit', _('Limit'), _('Maximum number of leased addresses.'));
692 so.optional = true;
693 so.datatype = 'uinteger';
694 so.default = '150';
695
696 so = ss.taboption('general', form.Value, 'leasetime', _('Lease time'), _('Expiry time of leased addresses, minimum is 2 minutes (<code>2m</code>).'));
697 so.optional = true;
698 so.default = '12h';
699
700 so = ss.taboption('advanced', form.Flag, 'dynamicdhcp', _('Dynamic <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>'), _('Dynamically allocate DHCP addresses for clients. If disabled, only clients having static leases will be served.'));
701 so.default = so.enabled;
702
703 ss.taboption('advanced', form.Flag, 'force', _('Force'), _('Force DHCP on this network even if another server is detected.'));
704
705 // XXX: is this actually useful?
706 //ss.taboption('advanced', form.Value, 'name', _('Name'), _('Define a name for this network.'));
707
708 so = ss.taboption('advanced', form.Value, 'netmask', _('<abbr title="Internet Protocol Version 4">IPv4</abbr>-Netmask'), _('Override the netmask sent to clients. Normally it is calculated from the subnet that is served.'));
709 so.optional = true;
710 so.datatype = 'ip4addr';
711
712 so.render = function(option_index, section_id, in_table) {
713 this.placeholder = get_netmask(s, true);
714 return form.Value.prototype.render.apply(this, [ option_index, section_id, in_table ]);
715 };
716
717 so.validate = function(section_id, value) {
718 var uielem = this.getUIElement(section_id);
719 if (uielem)
720 uielem.setPlaceholder(get_netmask(s, false));
721 return form.Value.prototype.validate.apply(this, [ section_id, value ]);
722 };
723
724 ss.taboption('advanced', form.DynamicList, 'dhcp_option', _('DHCP-Options'), _('Define additional DHCP options, for example "<code>6,192.168.2.1,192.168.2.2</code>" which advertises different DNS servers to clients.'));
725 }
726
727
728 var has_other_master = uci.sections('dhcp', 'dhcp').filter(function(s) {
729 return (s.interface != ifc.getName() && s.master == '1');
730 })[0];
731
732 so = ss.taboption('ipv6', form.Flag , 'master', _('Designated master'));
733 so.readonly = has_other_master ? true : false;
734 so.description = has_other_master
735 ? _('Interface "%h" is already marked as designated master.').format(has_other_master.interface || has_other_master['.name'])
736 : _('Set this interface as master for RA and DHCPv6 relaying as well as NDP proxying.')
737 ;
738
739 so.validate = function(section_id, value) {
740 var hybrid_downstream_desc = _('Operate in <em>relay mode</em> if a designated master interface is configured and active, otherwise fall back to <em>server mode</em>.'),
741 ndp_downstream_desc = _('Operate in <em>relay mode</em> if a designated master interface is configured and active, otherwise disable <abbr title="Neighbour Discovery Protocol">NDP</abbr> proxying.'),
742 hybrid_master_desc = _('Operate in <em>relay mode</em> if an upstream IPv6 prefix is present, otherwise disable service.'),
743 ra_server_allowed = true,
744 checked = this.formvalue(section_id),
745 dhcpv6 = this.section.getOption('dhcpv6').getUIElement(section_id),
746 ndp = this.section.getOption('ndp').getUIElement(section_id),
747 ra = this.section.getOption('ra').getUIElement(section_id);
748
749 /* Assume that serving RAs by default is fine, but disallow it for certain
750 interface protocols such as DHCP, DHCPv6 or the various PPP flavors.
751 The intent is to only allow RA serving for interface protocols doing
752 some kind of static IP config over something resembling a layer 2
753 ethernet device. */
754 switch (protoval) {
755 case 'dhcp':
756 case 'dhcpv6':
757 case '3g':
758 case 'l2tp':
759 case 'ppp':
760 case 'pppoa':
761 case 'pppoe':
762 case 'pptp':
763 case 'pppossh':
764 case 'ipip':
765 case 'gre':
766 case 'grev6':
767 ra_server_allowed = false;
768 break;
769 }
770
771 if (checked == '1' || !ra_server_allowed) {
772 dhcpv6.node.querySelector('li[data-value="server"]').setAttribute('unselectable', '');
773
774 if (dhcpv6.getValue() == 'server')
775 dhcpv6.setValue('hybrid');
776
777 ra.node.querySelector('li[data-value="server"]').setAttribute('unselectable', '');
778
779 if (ra.getValue() == 'server')
780 ra.setValue('hybrid');
781 }
782
783 if (checked == '1') {
784 dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
785 ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
786 ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_master_desc;
787 }
788 else {
789 if (ra_server_allowed) {
790 dhcpv6.node.querySelector('li[data-value="server"]').removeAttribute('unselectable');
791 ra.node.querySelector('li[data-value="server"]').removeAttribute('unselectable');
792 }
793
794 dhcpv6.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc;
795 ra.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = hybrid_downstream_desc;
796 ndp.node.querySelector('li[data-value="hybrid"] > div > span').innerHTML = ndp_downstream_desc ;
797 }
798
799 return true;
800 };
801
802
803 so = ss.taboption('ipv6', cbiRichListValue, 'ra', _('<abbr title="Router Advertisement">RA</abbr>-Service'),
804 _('Configures the operation mode of the <abbr title="Router Advertisement">RA</abbr> service on this interface.'));
805 so.value('', _('disabled'),
806 _('Do not send any <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages on this interface.'));
807 so.value('server', _('server mode'),
808 _('Send <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages advertising this device as IPv6 router.'));
809 so.value('relay', _('relay mode'),
810 _('Forward <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages received on the designated master interface to downstream interfaces.'));
811 so.value('hybrid', _('hybrid mode'), ' ');
812
813
814 so = ss.taboption('ipv6-ra', cbiRichListValue, 'ra_default', _('Default router'),
815 _('Configures the default router advertisement in <abbr title="Router Advertisement">RA</abbr> messages.'));
816 so.value('', _('automatic'),
817 _('Announce this device as default router if a local IPv6 default route is present.'));
818 so.value('1', _('on available prefix'),
819 _('Announce this device as default router if a public IPv6 prefix is available, regardless of local default route availability.'));
820 so.value('2', _('forced'),
821 _('Announce this device as default router regardless of whether a prefix or default route is present.'));
822 so.depends('ra', 'server');
823 so.depends({ ra: 'hybrid', master: '0' });
824
825 so = ss.taboption('ipv6-ra', form.Flag, 'ra_slaac', _('Enable <abbr title="Stateless Address Auto Config">SLAAC</abbr>'),
826 _('Set the autonomous address-configuration flag in the prefix information options of sent <abbr title="Router Advertisement">RA</abbr> messages. When enabled, clients will perform stateless IPv6 address autoconfiguration.'));
827 so.default = so.enabled;
828 so.depends('ra', 'server');
829 so.depends({ ra: 'hybrid', master: '0' });
830
831 so = ss.taboption('ipv6-ra', cbiRichListValue, 'ra_flags', _('<abbr title="Router Advertisement">RA</abbr> Flags'),
832 _('Specifies the flags sent in <abbr title="Router Advertisement">RA</abbr> messages, for example to instruct clients to request further information via stateful DHCPv6.'));
833 so.value('managed-config', _('managed config (M)'),
834 _('The <em>Managed address configuration</em> (M) flag indicates that IPv6 addresses are available via DHCPv6.'));
835 so.value('other-config', _('other config (O)'),
836 _('The <em>Other configuration</em> (O) flag indicates that other information, such as DNS servers, is available via DHCPv6.'));
837 so.value('home-agent', _('mobile home agent (H)'),
838 _('The <em>Mobile IPv6 Home Agent</em> (H) flag indicates that the device is also acting as Mobile IPv6 home agent on this link.'));
839 so.multiple = true;
840 so.select_placeholder = _('none');
841 so.depends('ra', 'server');
842 so.depends({ ra: 'hybrid', master: '0' });
843 so.cfgvalue = function(section_id) {
844 var flags = L.toArray(uci.get('dhcp', section_id, 'ra_flags'));
845 return flags.length ? flags : [ 'other-config' ];
846 };
847 so.remove = function(section_id) {
848 var existing = L.toArray(uci.get('dhcp', section_id, 'ra_flags'));
849 if (this.isActive(section_id)) {
850 if (existing.length != 1 || existing[0] != 'none')
851 uci.set('dhcp', section_id, 'ra_flags', [ 'none' ]);
852 }
853 else if (existing.length) {
854 uci.unset('dhcp', section_id, 'ra_flags');
855 }
856 };
857
858 so = ss.taboption('ipv6-ra', form.Value, 'ra_pref64', _('NAT64 prefix'), _('Announce NAT64 prefix in <abbr title="Router Advertisement">RA</abbr> messages.'));
859 so.optional = true;
860 so.datatype = 'cidr6';
861 so.placeholder = '64:ff9b::/96';
862 so.depends('ra', 'server');
863 so.depends({ ra: 'hybrid', master: '0' });
864
865 so = ss.taboption('ipv6-ra', form.Value, 'ra_maxinterval', _('Max <abbr title="Router Advertisement">RA</abbr> interval'), _('Maximum time allowed between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 600 seconds.'));
866 so.optional = true;
867 so.datatype = 'uinteger';
868 so.placeholder = '600';
869 so.depends('ra', 'server');
870 so.depends({ ra: 'hybrid', master: '0' });
871
872 so = ss.taboption('ipv6-ra', form.Value, 'ra_mininterval', _('Min <abbr title="Router Advertisement">RA</abbr> interval'), _('Minimum time allowed between sending unsolicited <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr>. Default is 200 seconds.'));
873 so.optional = true;
874 so.datatype = 'uinteger';
875 so.placeholder = '200';
876 so.depends('ra', 'server');
877 so.depends({ ra: 'hybrid', master: '0' });
878
879 so = ss.taboption('ipv6-ra', form.Value, 'ra_lifetime', _('<abbr title="Router Advertisement">RA</abbr> Lifetime'), _('Router Lifetime published in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages. Maximum is 9000 seconds.'));
880 so.optional = true;
881 so.datatype = 'range(0, 9000)';
882 so.placeholder = '1800';
883 so.depends('ra', 'server');
884 so.depends({ ra: 'hybrid', master: '0' });
885
886 so = ss.taboption('ipv6-ra', form.Value, 'ra_mtu', _('<abbr title="Router Advertisement">RA</abbr> MTU'), _('The <abbr title="Maximum Transmission Unit">MTU</abbr> to be published in <abbr title="Router Advertisement, ICMPv6 Type 134">RA</abbr> messages. Minimum is 1280 bytes.'));
887 so.optional = true;
888 so.datatype = 'range(1280, 65535)';
889 so.depends('ra', 'server');
890 so.depends({ ra: 'hybrid', master: '0' });
891 so.load = function(section_id) {
892 var dev = ifc.getL3Device(),
893 path = dev ? "/proc/sys/net/ipv6/conf/%s/mtu".format(dev.getName()) : null;
894
895 return Promise.all([
896 dev ? L.resolveDefault(fs.read(path), dev.getMTU()) : null,
897 this.super('load', [section_id])
898 ]).then(L.bind(function(res) {
899 this.placeholder = +res[0];
900
901 return res[1];
902 }, this));
903 };
904
905 so = ss.taboption('ipv6-ra', form.Value, 'ra_hoplimit', _('<abbr title="Router Advertisement">RA</abbr> Hop Limit'), _('The maximum hops to be published in <abbr title="Router Advertisement">RA</abbr> messages. Maximum is 255 hops.'));
906 so.optional = true;
907 so.datatype = 'range(0, 255)';
908 so.depends('ra', 'server');
909 so.depends({ ra: 'hybrid', master: '0' });
910 so.load = function(section_id) {
911 var dev = ifc.getL3Device(),
912 path = dev ? "/proc/sys/net/ipv6/conf/%s/hop_limit".format(dev.getName()) : null;
913
914 return Promise.all([
915 dev ? L.resolveDefault(fs.read(path), 64) : null,
916 this.super('load', [section_id])
917 ]).then(L.bind(function(res) {
918 this.placeholder = +res[0];
919
920 return res[1];
921 }, this));
922 };
923
924
925 so = ss.taboption('ipv6', cbiRichListValue, 'dhcpv6', _('DHCPv6-Service'),
926 _('Configures the operation mode of the DHCPv6 service on this interface.'));
927 so.value('', _('disabled'),
928 _('Do not offer DHCPv6 service on this interface.'));
929 so.value('server', _('server mode'),
930 _('Provide a DHCPv6 server on this interface and reply to DHCPv6 solicitations and requests.'));
931 so.value('relay', _('relay mode'),
932 _('Forward DHCPv6 messages between the designated master interface and downstream interfaces.'));
933 so.value('hybrid', _('hybrid mode'), ' ');
934
935 so = ss.taboption('ipv6', form.Value, 'dhcpv6_pd_min_len', _('<abbr title="Prefix Delegation">PD</abbr> minimum length'),
936 _('Configures the minimum delegated prefix length assigned to a requesting downstream router, potentially overriding a requested prefix length. If left unspecified, the device will assign the smallest available prefix greater than or equal to the requested prefix.'));
937 so.datatype = 'range(1,62)';
938 so.depends('dhcpv6', 'server');
939
940 so = ss.taboption('ipv6', form.DynamicList, 'dns', _('Announced IPv6 DNS servers'),
941 _('Specifies a fixed list of IPv6 DNS server addresses to announce via DHCPv6. If left unspecified, the device will announce itself as IPv6 DNS server unless the <em>Local IPv6 DNS server</em> option is disabled.'));
942 so.datatype = 'ip6addr("nomask")'; /* restrict to IPv6 only for now since dnsmasq (DHCPv4) does not honour this option */
943 so.depends('ra', 'server');
944 so.depends({ ra: 'hybrid', master: '0' });
945 so.depends('dhcpv6', 'server');
946 so.depends({ dhcpv6: 'hybrid', master: '0' });
947
948 so = ss.taboption('ipv6', form.Flag, 'dns_service', _('Local IPv6 DNS server'),
949 _('Announce this device as IPv6 DNS server.'));
950 so.default = so.enabled;
951 so.depends({ ra: 'server', dns: /^$/ });
952 so.depends({ ra: 'hybrid', dns: /^$/, master: '0' });
953 so.depends({ dhcpv6: 'server', dns: /^$/ });
954 so.depends({ dhcpv6: 'hybrid', dns: /^$/, master: '0' });
955
956 so = ss.taboption('ipv6', form.DynamicList, 'domain', _('Announced DNS domains'),
957 _('Specifies a fixed list of DNS search domains to announce via DHCPv6. If left unspecified, the local device DNS search domain will be announced.'));
958 so.datatype = 'hostname';
959 so.depends('ra', 'server');
960 so.depends({ ra: 'hybrid', master: '0' });
961 so.depends('dhcpv6', 'server');
962 so.depends({ dhcpv6: 'hybrid', master: '0' });
963
964
965 so = ss.taboption('ipv6', cbiRichListValue, 'ndp', _('<abbr title="Neighbour Discovery Protocol">NDP</abbr>-Proxy'),
966 _('Configures the operation mode of the NDP proxy service on this interface.'));
967 so.value('', _('disabled'),
968 _('Do not proxy any <abbr title="Neighbour Discovery Protocol">NDP</abbr> packets.'));
969 so.value('relay', _('relay mode'),
970 _('Forward <abbr title="Neighbour Discovery Protocol">NDP</abbr> <abbr title="Neighbour Solicitation, Type 135">NS</abbr> and <abbr title="Neighbour Advertisement, Type 136">NA</abbr> messages between the designated master interface and downstream interfaces.'));
971 so.value('hybrid', _('hybrid mode'), ' ');
972
973
974 so = ss.taboption('ipv6', form.Flag, 'ndproxy_routing', _('Learn routes'), _('Set up routes for proxied IPv6 neighbours.'));
975 so.default = so.enabled;
976 so.depends('ndp', 'relay');
977 so.depends('ndp', 'hybrid');
978
979 so = ss.taboption('ipv6', form.Flag, 'ndproxy_slave', _('NDP-Proxy slave'), _('Set interface as NDP-Proxy external slave. Default is off.'));
980 so.depends({ ndp: 'relay', master: '0' });
981 so.depends({ ndp: 'hybrid', master: '0' });
982
983 so = ss.taboption('ipv6', form.Value, 'preferred_lifetime', _('IPv6 Prefix Lifetime'), _('Preferred lifetime for a prefix.'));
984 so.optional = true;
985 so.placeholder = '12h';
986 so.value('5m', _('5m (5 minutes)'));
987 so.value('3h', _('3h (3 hours)'));
988 so.value('12h', _('12h (12 hours - default)'));
989 so.value('7d', _('7d (7 days)'));
990
991 //This is a ra_* setting, but its placement is more logical/findable under IPv6 settings.
992 so = ss.taboption('ipv6', form.Flag, 'ra_useleasetime', _('Follow IPv4 Lifetime'), _('DHCPv4 <code>leasetime</code> is used as limit and preferred lifetime of the IPv6 prefix.'));
993 so.optional = true;
994 }
995
996 ifc.renderFormOptions(s);
997
998 // Common interface options
999 o = nettools.replaceOption(s, 'advanced', form.Flag, 'defaultroute', _('Use default gateway'), _('If unchecked, no default route is configured'));
1000 o.default = o.enabled;
1001
1002 if (has_peerdns(protoval)) {
1003 o = nettools.replaceOption(s, 'advanced', form.Flag, 'peerdns', _('Use DNS servers advertised by peer'), _('If unchecked, the advertised DNS server addresses are ignored'));
1004 o.default = o.enabled;
1005 }
1006
1007 o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'dns', _('Use custom DNS servers'));
1008 if (has_peerdns(protoval))
1009 o.depends('peerdns', '0');
1010 o.datatype = 'ipaddr';
1011
1012 o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'dns_search', _('DNS search domains'));
1013 if (protoval != 'static')
1014 o.depends('peerdns', '0');
1015 o.datatype = 'hostname';
1016
1017 o = nettools.replaceOption(s, 'advanced', form.Value, 'dns_metric', _('DNS weight'), _('The DNS server entries in the local resolv.conf are primarily sorted by the weight specified here'));
1018 o.datatype = 'uinteger';
1019 o.placeholder = '0';
1020
1021 o = nettools.replaceOption(s, 'advanced', form.Value, 'metric', _('Use gateway metric'));
1022 o.datatype = 'uinteger';
1023 o.placeholder = '0';
1024
1025 o = nettools.replaceOption(s, 'advanced', form.Value, 'ip4table', _('Override IPv4 routing table'));
1026 o.datatype = 'or(uinteger, string)';
1027 for (var i = 0; i < rtTables.length; i++)
1028 o.value(rtTables[i][1], '%s (%d)'.format(rtTables[i][1], rtTables[i][0]));
1029
1030 o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6table', _('Override IPv6 routing table'));
1031 o.datatype = 'or(uinteger, string)';
1032 for (var i = 0; i < rtTables.length; i++)
1033 o.value(rtTables[i][1], '%s (%d)'.format(rtTables[i][1], rtTables[i][0]));
1034
1035 if (protoval == 'dhcpv6') {
1036 o = nettools.replaceOption(s, 'advanced', form.Flag, 'sourcefilter', _('IPv6 source routing'), _('Automatically handle multiple uplink interfaces using source-based policy routing.'));
1037 o.default = o.enabled;
1038 }
1039
1040 o = nettools.replaceOption(s, 'advanced', form.Flag, 'delegate', _('Delegate IPv6 prefixes'), _('Enable downstream delegation of IPv6 prefixes available on this interface'));
1041 o.default = o.enabled;
1042
1043 o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6assign', _('IPv6 assignment length'), _('Assign a part of given length of every public IPv6-prefix to this interface'));
1044 o.value('', _('disabled'));
1045 o.value('64');
1046 o.datatype = 'max(128)';
1047
1048 o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6hint', _('IPv6 assignment hint'), _('Assign prefix parts using this hexadecimal subprefix ID for this interface.'));
1049 o.placeholder = '0';
1050 o.validate = function(section_id, value) {
1051 if (value == null || value == '')
1052 return true;
1053
1054 var n = parseInt(value, 16);
1055
1056 if (!/^(0x)?[0-9a-fA-F]+$/.test(value) || isNaN(n) || n >= 0xffffffff)
1057 return _('Expecting a hexadecimal assignment hint');
1058
1059 return true;
1060 };
1061 for (var i = 33; i <= 64; i++)
1062 o.depends('ip6assign', String(i));
1063
1064
1065 o = nettools.replaceOption(s, 'advanced', form.DynamicList, 'ip6class', _('IPv6 prefix filter'), _('If set, downstream subnets are only allocated from the given IPv6 prefix classes.'));
1066 o.value('local', 'local (%s)'.format(_('Local ULA')));
1067
1068 var prefixClasses = {};
1069
1070 this.networks.forEach(function(net) {
1071 var prefixes = net._ubus('ipv6-prefix');
1072 if (Array.isArray(prefixes)) {
1073 prefixes.forEach(function(pfx) {
1074 if (L.isObject(pfx) && typeof(pfx['class']) == 'string') {
1075 prefixClasses[pfx['class']] = prefixClasses[pfx['class']] || {};
1076 prefixClasses[pfx['class']][net.getName()] = true;
1077 }
1078 });
1079 }
1080 });
1081
1082 Object.keys(prefixClasses).sort().forEach(function(c) {
1083 var networks = Object.keys(prefixClasses[c]).sort().join(', ');
1084 o.value(c, (c != networks) ? '%s (%s)'.format(c, networks) : c);
1085 });
1086
1087
1088 o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6ifaceid', _('IPv6 suffix'), _("Optional. Allowed values: 'eui64', 'random', fixed value like '::1' or '::1:2'. When IPv6 prefix (like 'a:b:c:d::') is received from a delegating server, use the suffix (like '::1') to form the IPv6 address ('a:b:c:d::1') for the interface."));
1089 o.datatype = 'ip6hostid';
1090 o.placeholder = '::1';
1091
1092 o = nettools.replaceOption(s, 'advanced', form.Value, 'ip6weight', _('IPv6 preference'), _('When delegating prefixes to multiple downstreams, interfaces with a higher preference value are considered first when allocating subnets.'));
1093 o.datatype = 'uinteger';
1094 o.placeholder = '0';
1095
1096 for (var i = 0; i < s.children.length; i++) {
1097 o = s.children[i];
1098
1099 switch (o.option) {
1100 case 'proto':
1101 case 'auto':
1102 case '_dhcp':
1103 case '_zone':
1104 case '_switch_proto':
1105 case '_ifacestat_modal':
1106 continue;
1107
1108 case 'igmp_snooping':
1109 case 'stp':
1110 case 'type':
1111 case '_net_device':
1112 var deps = [];
1113 for (var j = 0; j < protocols.length; j++) {
1114 if (!protocols[j].isVirtual()) {
1115 if (o.deps.length)
1116 for (var k = 0; k < o.deps.length; k++)
1117 deps.push(Object.assign({ proto: protocols[j].getProtocol() }, o.deps[k]));
1118 else
1119 deps.push({ proto: protocols[j].getProtocol() });
1120 }
1121 }
1122 o.deps = deps;
1123 break;
1124
1125 default:
1126 if (o.deps.length)
1127 for (var j = 0; j < o.deps.length; j++)
1128 o.deps[j].proto = protoval;
1129 else
1130 o.depends('proto', protoval);
1131 }
1132 }
1133
1134 this.activeSection = s.section;
1135 }, this));
1136 };
1137
1138 s.handleModalCancel = function(/* ... */) {
1139 var type = uci.get('network', this.activeSection || this.addedSection, 'type'),
1140 device = (type == 'bridge') ? 'br-%s'.format(this.activeSection || this.addedSection) : null;
1141
1142 uci.sections('network', 'bridge-vlan', function(bvs) {
1143 if (device != null && bvs.device == device)
1144 uci.remove('network', bvs['.name']);
1145 });
1146
1147 return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
1148 };
1149
1150 s.handleAdd = function(ev) {
1151 var m2 = new form.Map('network'),
1152 s2 = m2.section(form.NamedSection, '_new_'),
1153 protocols = network.getProtocols(),
1154 proto, name, device;
1155
1156 protocols.sort(function(a, b) {
1157 return L.naturalCompare(a.getProtocol(), b.getProtocol());
1158 });
1159
1160 s2.render = function() {
1161 return Promise.all([
1162 {},
1163 this.renderUCISection('_new_')
1164 ]).then(this.renderContents.bind(this));
1165 };
1166
1167 name = s2.option(form.Value, 'name', _('Name'));
1168 name.rmempty = false;
1169 name.datatype = 'uciname';
1170 name.placeholder = _('New interface name…');
1171 name.validate = function(section_id, value) {
1172 if (uci.get('network', value) != null)
1173 return _('The interface name is already used');
1174
1175 var pr = network.getProtocol(proto.formvalue(section_id), value),
1176 ifname = pr.isVirtual() ? '%s-%s'.format(pr.getProtocol(), value) : 'br-%s'.format(value);
1177
1178 if (value.length > 15)
1179 return _('The interface name is too long');
1180
1181 return true;
1182 };
1183
1184 proto = s2.option(form.ListValue, 'proto', _('Protocol'));
1185 proto.validate = name.validate;
1186
1187 device = s2.option(widgets.DeviceSelect, 'device', _('Device'));
1188 device.noaliases = false;
1189 device.optional = false;
1190
1191 for (var i = 0; i < protocols.length; i++) {
1192 proto.value(protocols[i].getProtocol(), protocols[i].getI18n());
1193
1194 if (!protocols[i].isVirtual())
1195 device.depends('proto', protocols[i].getProtocol());
1196 }
1197
1198 m2.render().then(L.bind(function(nodes) {
1199 ui.showModal(_('Add new interface...'), [
1200 nodes,
1201 E('div', { 'class': 'right' }, [
1202 E('button', {
1203 'class': 'btn',
1204 'click': ui.hideModal
1205 }, _('Cancel')), ' ',
1206 E('button', {
1207 'class': 'cbi-button cbi-button-positive important',
1208 'click': ui.createHandlerFn(this, function(ev) {
1209 var nameval = name.isValid('_new_') ? name.formvalue('_new_') : null,
1210 protoval = proto.isValid('_new_') ? proto.formvalue('_new_') : null,
1211 protoclass = protoval ? network.getProtocol(protoval, nameval) : null;
1212
1213 if (nameval == null || protoval == null || nameval == '' || protoval == '')
1214 return;
1215
1216 return protoclass.isCreateable(nameval).then(function(checkval) {
1217 if (checkval != null) {
1218 ui.addNotification(null,
1219 E('p', _('New interface for "%s" can not be created: %s').format(protoclass.getI18n(), checkval)));
1220 ui.hideModal();
1221 return;
1222 }
1223
1224 return m.save(function() {
1225 var section_id = uci.add('network', 'interface', nameval);
1226
1227 protoclass.set('proto', protoval);
1228 protoclass.addDevice(device.formvalue('_new_'));
1229
1230 m.children[0].addedSection = section_id;
1231
1232 ui.hideModal();
1233 ui.showModal(null, E('p', { 'class': 'spinning' }, [ _('Loading data…') ]));
1234 }).then(L.bind(m.children[0].renderMoreOptionsModal, m.children[0], nameval));
1235 });
1236 })
1237 }, _('Create interface'))
1238 ])
1239 ], 'cbi-modal');
1240
1241 nodes.querySelector('[id="%s"] input[type="text"]'.format(name.cbid('_new_'))).focus();
1242 }, this));
1243 };
1244
1245 s.handleRemove = function(section_id, ev) {
1246 return network.deleteNetwork(section_id).then(L.bind(function(section_id, ev) {
1247 return form.GridSection.prototype.handleRemove.apply(this, [section_id, ev]);
1248 }, this, section_id, ev));
1249 };
1250
1251 o = s.option(form.DummyValue, '_ifacebox');
1252 o.modalonly = false;
1253 o.textvalue = function(section_id) {
1254 var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0],
1255 zone = net ? this.section.zones.filter(function(z) { return !!z.getNetworks().filter(function(n) { return n == section_id })[0] })[0] : null;
1256
1257 if (!net)
1258 return;
1259
1260 var node = E('div', { 'class': 'ifacebox' }, [
1261 E('div', {
1262 'class': 'ifacebox-head',
1263 'style': firewall.getZoneColorStyle(zone),
1264 'title': zone ? _('Part of zone %q').format(zone.getName()) : _('No zone assigned')
1265 }, E('strong', net.getName())),
1266 E('div', {
1267 'class': 'ifacebox-body',
1268 'id': '%s-ifc-devices'.format(section_id),
1269 'data-network': section_id
1270 }, [
1271 E('img', {
1272 'src': L.resource('icons/ethernet_disabled.png'),
1273 'style': 'width:16px; height:16px'
1274 }),
1275 E('br'), E('small', '?')
1276 ])
1277 ]);
1278
1279 render_ifacebox_status(node.childNodes[1], net);
1280
1281 return node;
1282 };
1283
1284 o = s.option(form.DummyValue, '_ifacestat');
1285 o.modalonly = false;
1286 o.textvalue = function(section_id) {
1287 var net = this.section.networks.filter(function(n) { return n.getName() == section_id })[0];
1288
1289 if (!net)
1290 return;
1291
1292 var node = E('div', { 'id': '%s-ifc-description'.format(section_id) });
1293
1294 render_status(node, net, false);
1295
1296 return node;
1297 };
1298
1299 o = s.taboption('advanced', form.Flag, 'delegate', _('Use builtin IPv6-management'));
1300 o.modalonly = true;
1301 o.default = o.enabled;
1302
1303 o = s.taboption('advanced', form.Flag, 'force_link', _('Force link'), _('Set interface properties regardless of the link carrier (If set, carrier sense events do not invoke hotplug handlers).'));
1304 o.modalonly = true;
1305 o.defaults = {
1306 '1': [{ proto: 'static' }],
1307 '0': []
1308 };
1309
1310
1311 // Device configuration
1312 s = m.section(form.GridSection, 'device', _('Devices'));
1313 s.addremove = true;
1314 s.anonymous = true;
1315 s.addbtntitle = _('Add device configuration…');
1316
1317 s.cfgsections = function() {
1318 var sections = uci.sections('network', 'device'),
1319 section_ids = sections.sort(function(a, b) { return L.naturalCompare(a.name, b.name) }).map(function(s) { return s['.name'] });
1320
1321 for (var i = 0; i < netDevs.length; i++) {
1322 if (sections.filter(function(s) { return s.name == netDevs[i].getName() }).length)
1323 continue;
1324
1325 if (netDevs[i].getType() == 'wifi' && !netDevs[i].isUp())
1326 continue;
1327
1328 /* Unless http://lists.openwrt.org/pipermail/openwrt-devel/2020-July/030397.html is implemented,
1329 we cannot properly redefine bridges as devices, so filter them away for now... */
1330
1331 var m = netDevs[i].isBridge() ? netDevs[i].getName().match(/^br-([A-Za-z0-9_]+)$/) : null,
1332 s = m ? uci.get('network', m[1]) : null;
1333
1334 if (s && s['.type'] == 'interface' && s.type == 'bridge')
1335 continue;
1336
1337 section_ids.push('dev:%s'.format(netDevs[i].getName()));
1338 }
1339
1340 return section_ids;
1341 };
1342
1343 s.renderMoreOptionsModal = function(section_id, ev) {
1344 var m = section_id.match(/^dev:(.+)$/);
1345
1346 if (m) {
1347 var devtype = getDevType(section_id);
1348
1349 section_id = uci.add('network', 'device');
1350
1351 uci.set('network', section_id, 'name', m[1]);
1352 uci.set('network', section_id, 'type', (devtype != 'ethernet') ? devtype : null);
1353
1354 this.addedSection = section_id;
1355 }
1356
1357 return this.super('renderMoreOptionsModal', [section_id, ev]);
1358 };
1359
1360 s.renderRowActions = function(section_id) {
1361 var trEl = this.super('renderRowActions', [ section_id, _('Configure…') ]),
1362 deleteBtn = trEl.querySelector('button:last-child');
1363
1364 deleteBtn.firstChild.data = _('Unconfigure');
1365 deleteBtn.setAttribute('title', _('Remove related device settings from the configuration'));
1366 deleteBtn.disabled = section_id.match(/^dev:/) ? true : null;
1367
1368 return trEl;
1369 };
1370
1371 s.modaltitle = function(section_id) {
1372 var m = section_id.match(/^dev:(.+)$/),
1373 name = m ? m[1] : uci.get('network', section_id, 'name');
1374
1375 return name ? '%s: %q'.format(getDevTypeDesc(section_id), name) : _('Add device configuration');
1376 };
1377
1378 s.addModalOptions = function(s) {
1379 var isNew = (uci.get('network', s.section, 'name') == null),
1380 dev = getDevice(s.section);
1381
1382 nettools.addDeviceOptions(s, dev, isNew);
1383 };
1384
1385 s.handleModalCancel = function(map /*, ... */) {
1386 var name = uci.get('network', this.addedSection, 'name')
1387
1388 uci.sections('network', 'bridge-vlan', function(bvs) {
1389 if (name != null && bvs.device == name)
1390 uci.remove('network', bvs['.name']);
1391 });
1392
1393 if (map.addedVLANs)
1394 for (var i = 0; i < map.addedVLANs.length; i++)
1395 uci.remove('network', map.addedVLANs[i]);
1396
1397 if (this.addedSection)
1398 uci.remove('network', this.addedSection);
1399
1400 return form.GridSection.prototype.handleModalCancel.apply(this, arguments);
1401 };
1402
1403 s.handleRemove = function(section_id /*, ... */) {
1404 var name = uci.get('network', section_id, 'name'),
1405 type = uci.get('network', section_id, 'type');
1406
1407 if (name != null && type == 'bridge') {
1408 uci.sections('network', 'bridge-vlan', function(bvs) {
1409 if (bvs.device == name)
1410 uci.remove('network', bvs['.name']);
1411 });
1412 }
1413
1414 return form.GridSection.prototype.handleRemove.apply(this, arguments);
1415 };
1416
1417 function getDevice(section_id) {
1418 var m = section_id.match(/^dev:(.+)$/),
1419 name = m ? m[1] : uci.get('network', section_id, 'name');
1420
1421 return netDevs.filter(function(d) { return d.getName() == name })[0];
1422 }
1423
1424 function getDevType(section_id) {
1425 var dev = getDevice(section_id),
1426 cfg = uci.get('network', section_id),
1427 type = cfg ? (uci.get('network', section_id, 'type') || 'ethernet') : (dev ? dev.getType() : '');
1428
1429 switch (type) {
1430 case '':
1431 return null;
1432
1433 case 'vlan':
1434 case '8021q':
1435 return '8021q';
1436
1437 case '8021ad':
1438 return '8021ad';
1439
1440 case 'bridge':
1441 return 'bridge';
1442
1443 case 'tunnel':
1444 return 'tunnel';
1445
1446 case 'macvlan':
1447 return 'macvlan';
1448
1449 case 'veth':
1450 return 'veth';
1451
1452 case 'wifi':
1453 case 'alias':
1454 case 'switch':
1455 case 'ethernet':
1456 default:
1457 return 'ethernet';
1458 }
1459 }
1460
1461 function getDevTypeDesc(section_id) {
1462 switch (getDevType(section_id) || '') {
1463 case '':
1464 return E('em', [ _('Device not present') ]);
1465
1466 case '8021q':
1467 return _('VLAN (802.1q)');
1468
1469 case '8021ad':
1470 return _('VLAN (802.1ad)');
1471
1472 case 'bridge':
1473 return _('Bridge device');
1474
1475 case 'tunnel':
1476 return _('Tunnel device');
1477
1478 case 'macvlan':
1479 return _('MAC VLAN');
1480
1481 case 'veth':
1482 return _('Virtual Ethernet');
1483
1484 default:
1485 return _('Network device');
1486 }
1487 }
1488
1489 o = s.option(form.DummyValue, 'name', _('Device'));
1490 o.modalonly = false;
1491 o.textvalue = function(section_id) {
1492 var dev = getDevice(section_id),
1493 ext = section_id.match(/^dev:/),
1494 icon = render_iface(dev);
1495
1496 if (ext)
1497 icon.querySelector('img').style.opacity = '.5';
1498
1499 return E('span', { 'class': 'ifacebadge' }, [
1500 icon,
1501 E('span', { 'style': ext ? 'opacity:.5' : null }, [
1502 dev ? dev.getName() : (uci.get('network', section_id, 'name') || '?')
1503 ])
1504 ]);
1505 };
1506
1507 o = s.option(form.DummyValue, 'type', _('Type'));
1508 o.textvalue = getDevTypeDesc;
1509 o.modalonly = false;
1510
1511 o = s.option(form.DummyValue, 'macaddr', _('MAC Address'));
1512 o.modalonly = false;
1513 o.textvalue = function(section_id) {
1514 var dev = getDevice(section_id),
1515 val = uci.get('network', section_id, 'macaddr'),
1516 mac = dev ? dev.getMAC() : null;
1517
1518 return val ? E('strong', {
1519 'data-tooltip': _('The value is overridden by configuration.')
1520 }, [ val.toUpperCase() ]) : (mac || '-');
1521 };
1522
1523 o = s.option(form.DummyValue, 'mtu', _('MTU'));
1524 o.modalonly = false;
1525 o.textvalue = function(section_id) {
1526 var dev = getDevice(section_id),
1527 val = uci.get('network', section_id, 'mtu'),
1528 mtu = dev ? dev.getMTU() : null;
1529
1530 return val ? E('strong', {
1531 'data-tooltip': _('The value is overridden by configuration.')
1532 }, [ val ]) : (mtu || '-').toString();
1533 };
1534
1535 s = m.section(form.TypedSection, 'globals', _('Global network options'));
1536 s.addremove = false;
1537 s.anonymous = true;
1538
1539 o = s.option(form.Value, 'ula_prefix', _('IPv6 ULA-Prefix'), _('Unique Local Address - in the range <code>fc00::/7</code>. Typically only within the &#8216;local&#8217; half <code>fd00::/8</code>. ULA for IPv6 is analogous to IPv4 private network addressing. This prefix is randomly generated at first install.'));
1540 o.datatype = 'cidr6';
1541
1542 o = s.option(form.Flag, 'packet_steering', _('Packet Steering'), _('Enable packet steering across all CPUs. May help or hinder network speed.'));
1543 o.optional = true;
1544
1545
1546 if (dslModemType != null) {
1547 s = m.section(form.TypedSection, 'dsl', _('DSL'));
1548 s.anonymous = true;
1549
1550 o = s.option(form.ListValue, 'annex', _('Annex'));
1551 if (dslModemType == 'vdsl') {
1552 o.value('a', _('ADSL (all variants) Annex A/L/M + VDSL2 Annex A/B/C'));
1553 o.value('b', _('ADSL (all variants) Annex B + VDSL2 Annex A/B/C'));
1554 o.value('j', _('ADSL (all variants) Annex B/J + VDSL2 Annex A/B/C'));
1555 } else {
1556 o.value('a', _('ADSL (all variants) Annex A/L/M'));
1557 o.value('b', _('ADSL (all variants) Annex B'));
1558 o.value('j', _('ADSL (all variants) Annex B/J'));
1559 }
1560 o.value('m', _('ADSL (all variants) Annex M'));
1561 o.value('at1', _('ANSI T1.413'));
1562 o.value('admt', _('ADSL (G.992.1) Annex A'));
1563 o.value('bdmt', _('ADSL (G.992.1) Annex B'));
1564 o.value('alite', _('Splitterless ADSL (G.992.2) Annex A'));
1565 o.value('a2', _('ADSL2 (G.992.3) Annex A'));
1566 o.value('b2', _('ADSL2 (G.992.3) Annex B'));
1567 o.value('l', _('ADSL2 (G.992.3) Annex L'));
1568 o.value('m2', _('ADSL2 (G.992.3) Annex M'));
1569 o.value('a2p', _('ADSL2+ (G.992.5) Annex A'));
1570 o.value('b2p', _('ADSL2+ (G.992.5) Annex B'));
1571 o.value('m2p', _('ADSL2+ (G.992.5) Annex M'));
1572
1573 o = s.option(form.ListValue, 'tone', _('Tone'));
1574 o.value('', _('auto'));
1575 o.value('a', _('A43C + J43 + A43'));
1576 o.value('av', _('A43C + J43 + A43 + V43'));
1577 o.value('b', _('B43 + B43C'));
1578 o.value('bv', _('B43 + B43C + V43'));
1579
1580 if (dslModemType == 'vdsl') {
1581 o = s.option(form.ListValue, 'xfer_mode', _('Encapsulation mode'));
1582 o.value('', _('auto'));
1583 o.value('atm', _('ATM (Asynchronous Transfer Mode)'));
1584 o.value('ptm', _('PTM/EFM (Packet Transfer Mode)'));
1585
1586 o = s.option(form.ListValue, 'line_mode', _('DSL line mode'));
1587 o.value('', _('auto'));
1588 o.value('adsl', _('ADSL'));
1589 o.value('vdsl', _('VDSL'));
1590
1591 o = s.option(form.ListValue, 'ds_snr_offset', _('Downstream SNR offset'));
1592 o.default = '0';
1593
1594 for (var i = -100; i <= 100; i += 5)
1595 o.value(i, _('%.1f dB').format(i / 10));
1596 }
1597
1598 s.option(form.Value, 'firmware', _('Firmware File'));
1599 }
1600
1601
1602 // Show ATM bridge section if we have the capabilities
1603 if (L.hasSystemFeature('br2684ctl')) {
1604 s = m.section(form.TypedSection, 'atm-bridge', _('ATM Bridges'), _('ATM bridges expose encapsulated ethernet in AAL5 connections as virtual Linux network interfaces which can be used in conjunction with DHCP or PPP to dial into the provider network.'));
1605
1606 s.addremove = true;
1607 s.anonymous = true;
1608 s.addbtntitle = _('Add ATM Bridge');
1609
1610 s.handleAdd = function(ev) {
1611 var sections = uci.sections('network', 'atm-bridge'),
1612 max_unit = -1;
1613
1614 for (var i = 0; i < sections.length; i++) {
1615 var unit = +sections[i].unit;
1616
1617 if (!isNaN(unit) && unit > max_unit)
1618 max_unit = unit;
1619 }
1620
1621 return this.map.save(function() {
1622 var sid = uci.add('network', 'atm-bridge');
1623
1624 uci.set('network', sid, 'unit', max_unit + 1);
1625 uci.set('network', sid, 'atmdev', 0);
1626 uci.set('network', sid, 'encaps', 'llc');
1627 uci.set('network', sid, 'payload', 'bridged');
1628 uci.set('network', sid, 'vci', 35);
1629 uci.set('network', sid, 'vpi', 8);
1630 });
1631 };
1632
1633 s.tab('general', _('General Setup'));
1634 s.tab('advanced', _('Advanced Settings'));
1635
1636 o = s.taboption('general', form.Value, 'vci', _('ATM Virtual Channel Identifier (VCI)'));
1637 s.taboption('general', form.Value, 'vpi', _('ATM Virtual Path Identifier (VPI)'));
1638
1639 o = s.taboption('general', form.ListValue, 'encaps', _('Encapsulation mode'));
1640 o.value('llc', _('LLC'));
1641 o.value('vc', _('VC-Mux'));
1642
1643 s.taboption('advanced', form.Value, 'atmdev', _('ATM device number'));
1644 s.taboption('advanced', form.Value, 'unit', _('Bridge unit number'));
1645
1646 o = s.taboption('advanced', form.ListValue, 'payload', _('Forwarding mode'));
1647 o.value('bridged', _('bridged'));
1648 o.value('routed', _('routed'));
1649 }
1650
1651
1652 return m.render().then(L.bind(function(m, nodes) {
1653 poll.add(L.bind(function() {
1654 var section_ids = m.children[0].cfgsections(),
1655 tasks = [];
1656
1657 for (var i = 0; i < section_ids.length; i++) {
1658 var row = nodes.querySelector('.cbi-section-table-row[data-sid="%s"]'.format(section_ids[i])),
1659 dsc = row.querySelector('[data-name="_ifacestat"] > div'),
1660 btn1 = row.querySelector('.cbi-section-actions .reconnect'),
1661 btn2 = row.querySelector('.cbi-section-actions .down');
1662
1663 if (dsc.getAttribute('reconnect') == '') {
1664 dsc.setAttribute('reconnect', '1');
1665 tasks.push(fs.exec('/sbin/ifup', [section_ids[i]]).catch(function(e) {
1666 ui.addNotification(null, E('p', e.message));
1667 }));
1668 }
1669 else if (dsc.getAttribute('disconnect') == '') {
1670 dsc.setAttribute('disconnect', '1');
1671 tasks.push(fs.exec('/sbin/ifdown', [section_ids[i]]).catch(function(e) {
1672 ui.addNotification(null, E('p', e.message));
1673 }));
1674 }
1675 else if (dsc.getAttribute('reconnect') == '1') {
1676 dsc.removeAttribute('reconnect');
1677 btn1.classList.remove('spinning');
1678 btn1.disabled = false;
1679 }
1680 else if (dsc.getAttribute('disconnect') == '1') {
1681 dsc.removeAttribute('disconnect');
1682 btn2.classList.remove('spinning');
1683 btn2.disabled = false;
1684 }
1685 }
1686
1687 return Promise.all(tasks)
1688 .then(L.bind(network.getNetworks, network))
1689 .then(L.bind(this.poll_status, this, nodes));
1690 }, this), 5);
1691
1692 return nodes;
1693 }, this, m));
1694 }
1695 });