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