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