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