luci-mod-network: fix look and feel for DNSMASQ IP-Sets generation
[project/luci.git] / modules / luci-mod-network / htdocs / luci-static / resources / view / network / dhcp.js
1 'use strict';
2 'require view';
3 'require dom';
4 'require poll';
5 'require rpc';
6 'require uci';
7 'require form';
8 'require network';
9 'require validation';
10 'require tools.widgets as widgets';
11
12 var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status;
13
14 callHostHints = rpc.declare({
15 object: 'luci-rpc',
16 method: 'getHostHints',
17 expect: { '': {} }
18 });
19
20 callDUIDHints = rpc.declare({
21 object: 'luci-rpc',
22 method: 'getDUIDHints',
23 expect: { '': {} }
24 });
25
26 callDHCPLeases = rpc.declare({
27 object: 'luci-rpc',
28 method: 'getDHCPLeases',
29 expect: { '': {} }
30 });
31
32 CBILeaseStatus = form.DummyValue.extend({
33 renderWidget: function(section_id, option_id, cfgvalue) {
34 return E([
35 E('h4', _('Active DHCP Leases')),
36 E('table', { 'id': 'lease_status_table', 'class': 'table' }, [
37 E('tr', { 'class': 'tr table-titles' }, [
38 E('th', { 'class': 'th' }, _('Hostname')),
39 E('th', { 'class': 'th' }, _('IPv4 address')),
40 E('th', { 'class': 'th' }, _('MAC address')),
41 E('th', { 'class': 'th' }, _('Lease time remaining'))
42 ]),
43 E('tr', { 'class': 'tr placeholder' }, [
44 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
45 ])
46 ])
47 ]);
48 }
49 });
50
51 CBILease6Status = form.DummyValue.extend({
52 renderWidget: function(section_id, option_id, cfgvalue) {
53 return E([
54 E('h4', _('Active DHCPv6 Leases')),
55 E('table', { 'id': 'lease6_status_table', 'class': 'table' }, [
56 E('tr', { 'class': 'tr table-titles' }, [
57 E('th', { 'class': 'th' }, _('Host')),
58 E('th', { 'class': 'th' }, _('IPv6 address')),
59 E('th', { 'class': 'th' }, _('DUID')),
60 E('th', { 'class': 'th' }, _('Lease time remaining'))
61 ]),
62 E('tr', { 'class': 'tr placeholder' }, [
63 E('td', { 'class': 'td' }, E('em', _('Collecting data...')))
64 ])
65 ])
66 ]);
67 }
68 });
69
70 function calculateNetwork(addr, mask) {
71 addr = validation.parseIPv4(String(addr));
72
73 if (!isNaN(mask))
74 mask = validation.parseIPv4(network.prefixToMask(+mask));
75 else
76 mask = validation.parseIPv4(String(mask));
77
78 if (addr == null || mask == null)
79 return null;
80
81 return [
82 [
83 addr[0] & (mask[0] >>> 0 & 255),
84 addr[1] & (mask[1] >>> 0 & 255),
85 addr[2] & (mask[2] >>> 0 & 255),
86 addr[3] & (mask[3] >>> 0 & 255)
87 ].join('.'),
88 mask.join('.')
89 ];
90 }
91
92 function generateDnsmasqInstanceEntry(data) {
93 const nameValueMap = new Map(Object.entries(data));
94 let formatString = nameValueMap.get('.index') + ' (' + _('Name') + (nameValueMap.get('.anonymous') ? ': dnsmasq[' + nameValueMap.get('.index') + ']': ': ' + nameValueMap.get('.name'));
95
96 if (data.domain) {
97 formatString += ', ' + _('Domain') + ': ' + data.domain;
98 }
99 if (data.local) {
100 formatString += ', ' + _('Local') + ': ' + data.local;
101 }
102 formatString += ')';
103
104 return nameValueMap.get('.name'), formatString;
105 }
106
107 function getDHCPPools() {
108 return uci.load('dhcp').then(function() {
109 let sections = uci.sections('dhcp', 'dhcp'),
110 tasks = [], pools = [];
111
112 for (var i = 0; i < sections.length; i++) {
113 if (sections[i].ignore == '1' || !sections[i].interface)
114 continue;
115
116 tasks.push(network.getNetwork(sections[i].interface).then(L.bind(function(section_id, net) {
117 var cidr = net ? (net.getIPAddrs()[0] || '').split('/') : null;
118
119 if (cidr && cidr.length == 2) {
120 var net_mask = calculateNetwork(cidr[0], cidr[1]);
121
122 pools.push({
123 section_id: section_id,
124 network: net_mask[0],
125 netmask: net_mask[1]
126 });
127 }
128 }, null, sections[i]['.name'])));
129 }
130
131 return Promise.all(tasks).then(function() {
132 return pools;
133 });
134 });
135 }
136
137 function validateHostname(sid, s) {
138 if (s == null || s == '')
139 return true;
140
141 if (s.length > 256)
142 return _('Expecting: %s').format(_('valid hostname'));
143
144 var labels = s.replace(/^\*?\.?|\.$/g, '').split(/\./);
145
146 for (var i = 0; i < labels.length; i++)
147 if (!labels[i].match(/^[a-z0-9_](?:[a-z0-9-]{0,61}[a-z0-9])?$/i))
148 return _('Expecting: %s').format(_('valid hostname'));
149
150 return true;
151 }
152
153 function validateAddressList(sid, s) {
154 if (s == null || s == '')
155 return true;
156
157 var m = s.match(/^\/(.+)\/$/),
158 names = m ? m[1].split(/\//) : [ s ];
159
160 for (var i = 0; i < names.length; i++) {
161 var res = validateHostname(sid, names[i]);
162
163 if (res !== true)
164 return res;
165 }
166
167 return true;
168 }
169
170 function validateServerSpec(sid, s) {
171 if (s == null || s == '')
172 return true;
173
174 var m = s.match(/^(\/.*\/)?(.*)$/);
175 if (!m)
176 return _('Expecting: %s').format(_('valid hostname'));
177
178 if (m[1] != '//' && m[1] != '/#/') {
179 var res = validateAddressList(sid, m[1]);
180 if (res !== true)
181 return res;
182 }
183
184 if (m[2] == '' || m[2] == '#')
185 return true;
186
187 // ipaddr%scopeid#srvport@source@interface#srcport
188
189 m = m[2].match(/^([0-9a-f:.]+)(?:%[^#@]+)?(?:#(\d+))?(?:@([0-9a-f:.]+)(?:@[^#]+)?(?:#(\d+))?)?$/);
190
191 if (!m)
192 return _('Expecting: %s').format(_('valid IP address'));
193
194 if (validation.parseIPv4(m[1])) {
195 if (m[3] != null && !validation.parseIPv4(m[3]))
196 return _('Expecting: %s').format(_('valid IPv4 address'));
197 }
198 else if (validation.parseIPv6(m[1])) {
199 if (m[3] != null && !validation.parseIPv6(m[3]))
200 return _('Expecting: %s').format(_('valid IPv6 address'));
201 }
202 else {
203 return _('Expecting: %s').format(_('valid IP address'));
204 }
205
206 if ((m[2] != null && +m[2] > 65535) || (m[4] != null && +m[4] > 65535))
207 return _('Expecting: %s').format(_('valid port value'));
208
209 return true;
210 }
211
212 function expandAndFormatMAC(macs) {
213 let result = [];
214
215 macs.forEach(mac => {
216 if (isValidMAC(mac)) {
217 const expandedMac = mac.split(':').map(part => {
218 return (part.length === 1 && part !== '*') ? '0' + part : part;
219 }).join(':').toUpperCase();
220 result.push(expandedMac);
221 }
222 });
223
224 return result.length ? result.join(' ') : null;
225 }
226
227 function isValidMAC(sid, s) {
228 if (!s)
229 return true;
230
231 let macaddrs = L.toArray(s);
232
233 for (var i = 0; i < macaddrs.length; i++)
234 if (!macaddrs[i].match(/^(([0-9a-f]{1,2}|\*)[:-]){5}([0-9a-f]{1,2}|\*)$/i))
235 return _('Expecting a valid MAC address, optionally including wildcards') + _('; invalid MAC: ') + macaddrs[i];
236
237 return true;
238 }
239
240 function validateMACAddr(pools, sid, s) {
241 if (s == null || s == '')
242 return true;
243
244 var leases = uci.sections('dhcp', 'host'),
245 this_macs = L.toArray(s).map(function(m) { return m.toUpperCase() });
246
247 for (var i = 0; i < pools.length; i++) {
248 var this_net_mask = calculateNetwork(this.section.formvalue(sid, 'ip'), pools[i].netmask);
249
250 if (!this_net_mask)
251 continue;
252
253 for (var j = 0; j < leases.length; j++) {
254 if (leases[j]['.name'] == sid || !leases[j].ip)
255 continue;
256
257 var lease_net_mask = calculateNetwork(leases[j].ip, pools[i].netmask);
258
259 if (!lease_net_mask || this_net_mask[0] != lease_net_mask[0])
260 continue;
261
262 var lease_macs = L.toArray(leases[j].mac).map(function(m) { return m.toUpperCase() });
263
264 for (var k = 0; k < lease_macs.length; k++)
265 for (var l = 0; l < this_macs.length; l++)
266 if (lease_macs[k] == this_macs[l])
267 return _('The MAC address %h is already used by another static lease in the same DHCP pool').format(this_macs[l]);
268 }
269 }
270
271 return isValidMAC(sid, s);
272 }
273
274 return view.extend({
275 load: function() {
276 return Promise.all([
277 callHostHints(),
278 callDUIDHints(),
279 getDHCPPools(),
280 network.getNetworks()
281 ]);
282 },
283
284 render: function(hosts_duids_pools) {
285 var has_dhcpv6 = L.hasSystemFeature('dnsmasq', 'dhcpv6') || L.hasSystemFeature('odhcpd'),
286 hosts = hosts_duids_pools[0],
287 duids = hosts_duids_pools[1],
288 pools = hosts_duids_pools[2],
289 networks = hosts_duids_pools[3],
290 m, s, o, ss, so, dnss;
291
292 let noi18nstrings = {
293 etc_hosts: '<code>/etc/hosts</code>',
294 etc_ethers: '<code>/etc/ethers</code>',
295 localhost_v6: '<code>::1</code>',
296 loopback_slash_8_v4: '<code>127.0.0.0/8</code>',
297 not_found: '<code>Not found</code>',
298 nxdomain: '<code>NXDOMAIN</code>',
299 rfc_1918_link: '<a href="https://www.rfc-editor.org/rfc/rfc1918">RFC1918</a>',
300 rfc_4193_link: '<a href="https://www.rfc-editor.org/rfc/rfc4193">RFC4193</a>',
301 rfc_4291_link: '<a href="https://www.rfc-editor.org/rfc/rfc4291">RFC4291</a>',
302 rfc_6303_link: '<a href="https://www.rfc-editor.org/rfc/rfc6303">RFC6303</a>',
303 reverse_arpa: '<code>*.IN-ADDR.ARPA,*.IP6.ARPA</code>',
304 servers_file_entry01: '<code>server=1.2.3.4</code>',
305 servers_file_entry02: '<code>server=/domain/1.2.3.4</code>',
306
307 };
308
309 function customi18n(template, values) {
310 if (!values)
311 values = noi18nstrings;
312 return template.replace(/\{(\w+)\}/g, (match, key) => values[key] || match);
313 };
314
315 m = new form.Map('dhcp', _('DHCP and DNS'),
316 _('Dnsmasq is a lightweight <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> server and <abbr title="Domain Name System">DNS</abbr> forwarder.'));
317
318 s = m.section(form.TypedSection, 'dnsmasq');
319 s.anonymous = false;
320 s.addremove = true;
321 s.addbtntitle = _('Add server instance', 'Dnsmasq instance');
322
323 s.renderContents = function(/* ... */) {
324 var renderTask = form.TypedSection.prototype.renderContents.apply(this, arguments),
325 sections = this.cfgsections();
326
327 return Promise.resolve(renderTask).then(function(nodes) {
328 if (sections.length < 2) {
329 nodes.querySelector('#cbi-dhcp-dnsmasq > h3').remove();
330 nodes.querySelector('#cbi-dhcp-dnsmasq > .cbi-section-remove').remove();
331 }
332 else {
333 nodes.querySelectorAll('#cbi-dhcp-dnsmasq > .cbi-section-remove').forEach(function(div, i) {
334 var section = uci.get('dhcp', sections[i]),
335 hline = div.nextElementSibling,
336 btn = div.firstElementChild;
337
338 if (!section || section['.anonymous']) {
339 hline.innerText = i ? _('Unnamed instance #%d', 'Dnsmasq instance').format(i+1) : _('Default instance', 'Dnsmasq instance');
340 btn.innerText = i ? _('Remove instance #%d', 'Dnsmasq instance').format(i+1) : _('Remove default instance', 'Dnsmasq instance');
341 }
342 else {
343 hline.innerText = _('Instance "%q"', 'Dnsmasq instance').format(section['.name']);
344 btn.innerText = _('Remove instance "%q"', 'Dnsmasq instance').format(section['.name']);
345 }
346 });
347 }
348
349 nodes.querySelector('#cbi-dhcp-dnsmasq > .cbi-section-create input').placeholder = _('New instance name…', 'Dnsmasq instance');
350
351 return nodes;
352 });
353 };
354
355
356 s.tab('general', _('General'));
357 s.tab('devices', _('Devices &amp; Ports'));
358 s.tab('dnsrecords', _('DNS Records'));
359 s.tab('dnssecopt', _('DNSSEC'));
360 s.tab('filteropts', _('Filter'));
361 s.tab('forward', _('Forwards'));
362 s.tab('limits', _('Limits'));
363 s.tab('logging', _('Log'));
364 s.tab('files', _('Resolv &amp; Hosts Files'));
365 s.tab('leases', _('Static Leases'));
366 s.tab('ipsets', _('IP Sets'));
367 s.tab('relay', _('Relay'));
368 s.tab('pxe_tftp', _('PXE/TFTP'));
369
370 s.taboption('filteropts', form.Flag, 'domainneeded',
371 _('Domain required'),
372 _('Never forward DNS queries which lack dots or domain parts.') + '<br />' +
373 customi18n(_('Names not in {etc_hosts} are answered {not_found}.') )
374 );
375 s.taboption('general', form.Flag, 'authoritative',
376 _('Authoritative'),
377 _('This is the only DHCP server in the local network.'));
378
379 o = s.taboption('general', form.Value, 'local',
380 _('Resolve these locally'),
381 _('Never forward these matching domains or subdomains; resolve from DHCP or hosts files only.'));
382 o.placeholder = '/internal.example.com/private.example.com/example.org';
383
384 s.taboption('general', form.Value, 'domain',
385 _('Local domain'),
386 _('Local domain suffix appended to DHCP names and hosts file entries.'));
387
388 s.taboption('general', form.Flag, 'expandhosts',
389 _('Expand hosts'),
390 _('Add local domain suffix to names served from hosts files.'));
391
392 o = s.taboption('logging', form.Flag, 'logqueries',
393 _('Log queries'),
394 _('Write received DNS queries to syslog.') + ' ' + _('Dump cache on SIGUSR1, include requesting IP.'));
395 o.optional = true;
396
397 o = s.taboption('logging', form.Flag, 'logdhcp',
398 _('Extra DHCP logging'),
399 _('Log all options sent to DHCP clients and the tags used to determine them.'));
400 o.optional = true;
401
402 o = s.taboption('logging', form.Value, 'logfacility',
403 _('Log facility'),
404 _('Set log class/facility for syslog entries.'));
405 o.optional = true;
406 o.value('KERN');
407 o.value('USER');
408 o.value('MAIL');
409 o.value('DAEMON');
410 o.value('AUTH');
411 o.value('LPR');
412 o.value('NEWS');
413 o.value('UUCP');
414 o.value('CRON');
415 o.value('LOCAL0');
416 o.value('LOCAL1');
417 o.value('LOCAL2');
418 o.value('LOCAL3');
419 o.value('LOCAL4');
420 o.value('LOCAL5');
421 o.value('LOCAL6');
422 o.value('LOCAL7');
423 o.value('-', _('stderr'));
424
425 o = s.taboption('forward', form.DynamicList, 'server',
426 _('DNS Forwards'),
427 _('Forward specific domain queries to specific upstream servers.'));
428 o.optional = true;
429 o.placeholder = '/*.example.org/10.1.2.3';
430 o.validate = validateServerSpec;
431
432 o = s.taboption('general', form.DynamicList, 'address',
433 _('Addresses'),
434 _('Resolve specified FQDNs to an IP.') + '<br />' +
435 customi18n(_('Syntax: {code_syntax}.'),
436 {code_syntax: '<code>/fqdn[/fqdn…]/[ipaddr]</code>'}) + '<br />' +
437 customi18n(_('{example_nx} returns {nxdomain}.',
438 'hint: <code>/example.com/</code> returns <code>NXDOMAIN</code>.'),
439 {example_nx: '<code>/example.com/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
440 customi18n(_('{any_domain} matches any domain (and returns {nxdomain}).',
441 'hint: <code>/#/</code> matches any domain (and returns NXDOMAIN).'),
442 {any_domain:'<code>/#/</code>', nxdomain: '<code>NXDOMAIN</code>'}) + '<br />' +
443 customi18n(
444 _('{example_null} returns {null_addr} addresses ({null_ipv4}, {null_ipv6}) for {example_com} and its subdomains.',
445 'hint: <code>/example.com/#</code> returns NULL addresses (<code>0.0.0.0</code>, <code>::</code>) for example.com and its subdomains.'),
446 { example_null: '<code>/example.com/#</code>',
447 null_addr: '<code>NULL</code>',
448 null_ipv4: '<code>0.0.0.0</code>',
449 null_ipv6: '<code>::</code>',
450 example_com: '<code>example.com</code>',
451 }
452 )
453 );
454 o.optional = true;
455 o.placeholder = '/router.local/router.lan/192.168.0.1';
456
457 o = s.taboption('general', form.DynamicList, 'ipset',
458 _('IP sets'),
459 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.'));
460 o.optional = true;
461 o.placeholder = '/example.org/ipset,ipset6';
462
463 o = s.taboption('filteropts', form.Flag, 'rebind_protection',
464 _('Rebind protection'),
465 customi18n(_('Discard upstream responses containing {rfc_1918_link} addresses.') ) + '<br />' +
466 customi18n(_('Discard also upstream responses containing {rfc_4193_link}, Link-Local and private IPv4-Mapped {rfc_4291_link} IPv6 Addresses.') )
467 );
468 o.rmempty = false;
469
470 o = s.taboption('filteropts', form.Flag, 'rebind_localhost',
471 _('Allow localhost'),
472 customi18n(
473 _('Exempt {loopback_slash_8_v4} and {localhost_v6} from rebinding checks, e.g. for <abbr title="Real-time Block List">RBL</abbr> services.')
474 )
475 );
476 o.depends('rebind_protection', '1');
477
478 o = s.taboption('filteropts', form.DynamicList, 'rebind_domain',
479 _('Domain whitelist'),
480 customi18n(_('List of domains to allow {rfc_1918_link} responses for.') )
481 );
482 o.depends('rebind_protection', '1');
483 o.optional = true;
484 o.placeholder = 'ihost.netflix.com';
485 o.validate = validateAddressList;
486
487 o = s.taboption('filteropts', form.Flag, 'localservice',
488 _('Local service only'),
489 _('Accept DNS queries only from hosts whose address is on a local subnet.'));
490 o.optional = false;
491 o.rmempty = false;
492
493 o = s.taboption('devices', form.Flag, 'nonwildcard',
494 _('Non-wildcard'),
495 _('Bind only to configured interface addresses, instead of the wildcard address.'));
496 o.default = o.enabled;
497 o.optional = false;
498 o.rmempty = true;
499
500 o = s.taboption('devices', widgets.NetworkSelect, 'interface',
501 _('Listen interfaces'),
502 _('Listen only on the specified interfaces, and loopback if not excluded explicitly.'));
503 o.multiple = true;
504 o.nocreate = true;
505
506 o = s.taboption('devices', widgets.NetworkSelect, 'notinterface',
507 _('Exclude interfaces'),
508 _('Do not listen on the specified interfaces.'));
509 o.loopback = true;
510 o.multiple = true;
511 o.nocreate = true;
512
513 o = s.taboption('relay', form.SectionValue, '__relays__', form.TableSection, 'relay', null,
514 _('Relay DHCP requests elsewhere. OK: v4↔v4, v6↔v6. Not OK: v4↔v6, v6↔v4.')
515 + '<br />' + _('Note: you may also need a DHCP Proxy (currently unavailable) when specifying a non-standard Relay To port(<code>addr#port</code>).')
516 + '<br />' + _('You may add multiple unique Relay To on the same Listen addr.'));
517
518 ss = o.subsection;
519
520 ss.addremove = true;
521 ss.anonymous = true;
522 ss.sortable = true;
523 ss.rowcolors = true;
524 ss.nodescriptions = true;
525
526 so = ss.option(form.Value, 'local_addr', _('Relay from'));
527 so.rmempty = false;
528 so.datatype = 'ipaddr';
529
530 for (var family = 4; family <= 6; family += 2) {
531 for (var i = 0; i < networks.length; i++) {
532 if (networks[i].getName() != 'loopback') {
533 var addrs = (family == 6) ? networks[i].getIP6Addrs() : networks[i].getIPAddrs();
534 for (var j = 0; j < addrs.length; j++) {
535 var addr = addrs[j].split('/')[0];
536 so.value(addr, E([], [
537 addr, ' (',
538 widgets.NetworkSelect.prototype.renderIfaceBadge(networks[i]),
539 ')'
540 ]));
541 }
542 }
543 }
544 }
545
546 so = ss.option(form.Value, 'server_addr', _('Relay to address'));
547 so.rmempty = false;
548 so.optional = false;
549 so.placeholder = '192.168.10.1#535';
550
551 so.validate = function(section, value) {
552 var m = this.section.formvalue(section, 'local_addr'),
553 n = this.section.formvalue(section, 'server_addr'),
554 p;
555
556 if (!m || !n) {
557 return _('Both "Relay from" and "Relay to address" must be specified.');
558 }
559 else {
560 p = n.split('#');
561 if (p.length > 1 && !/^[0-9]+$/.test(p[1]))
562 return _('Expected port number.');
563 else
564 n = p[0];
565
566 if ((validation.parseIPv6(m) && validation.parseIPv6(n)) ||
567 validation.parseIPv4(m) && validation.parseIPv4(n))
568 return true;
569 else
570 return _('Address families of "Relay from" and "Relay to address" must match.')
571 }
572 return true;
573 };
574
575
576 so = ss.option(widgets.NetworkSelect, 'interface', _('Only accept replies via'));
577 so.optional = true;
578 so.rmempty = false;
579 so.placeholder = 'lan';
580
581 s.taboption('files', form.Flag, 'readethers',
582 customi18n(_('Use {etc_ethers}') ),
583 customi18n(_('Read {etc_ethers} to configure the DHCP server.') )
584 );
585
586 s.taboption('files', form.Value, 'leasefile',
587 _('Lease file'),
588 _('File to store DHCP lease information.'));
589
590 o = s.taboption('files', form.Flag, 'noresolv',
591 _('Ignore resolv file'));
592 o.optional = true;
593
594 o = s.taboption('files', form.Value, 'resolvfile',
595 _('Resolv file'),
596 _('File with upstream resolvers.'));
597 o.depends('noresolv', '0');
598 o.placeholder = '/tmp/resolv.conf.d/resolv.conf.auto';
599 o.optional = true;
600
601 o = s.taboption('files', form.Flag, 'strictorder',
602 _('Strict order'),
603 _('Query upstream resolvers in the order they appear in the resolv file.'));
604 o.optional = true;
605
606 o = s.taboption('files', form.Flag, 'nohosts',
607 customi18n(_('Ignore {etc_hosts}') )
608 );
609 o.optional = true;
610
611 o = s.taboption('files', form.DynamicList, 'addnhosts',
612 _('Additional hosts files'));
613 o.optional = true;
614 o.placeholder = '/etc/dnsmasq.hosts';
615
616 o = s.taboption('logging', form.Flag, 'quietdhcp',
617 _('Suppress logging'),
618 _('Suppress logging of the routine operation for the DHCP protocol.'));
619 o.optional = true;
620 o.depends('logdhcp', '0');
621
622 o = s.taboption('general', form.Flag, 'sequential_ip',
623 _('Allocate IPs sequentially'),
624 _('Allocate IP addresses sequentially, starting from the lowest available address.'));
625 o.optional = true;
626
627 o = s.taboption('filteropts', form.Flag, 'boguspriv',
628 _('Filter private'),
629 customi18n(
630 _('Reject reverse lookups to {rfc_6303_link} IP ranges ({reverse_arpa}) not in {etc_hosts}.') )
631 );
632 o.default = o.enabled;
633
634 s.taboption('filteropts', form.Flag, 'filterwin2k',
635 _('Filter SRV/SOA service discovery'),
636 _('Filters SRV/SOA service discovery, to avoid triggering dial-on-demand links.') + '<br />' +
637 _('May prevent VoIP or other services from working.'));
638
639 o = s.taboption('filteropts', form.Flag, 'filter_aaaa',
640 _('Filter IPv6 AAAA records'),
641 _('Remove IPv6 addresses from the results and only return IPv4 addresses.') + '<br />' +
642 _('Can be useful if ISP has IPv6 nameservers but does not provide IPv6 routing.'));
643 o.optional = true;
644
645 o = s.taboption('filteropts', form.Flag, 'filter_a',
646 _('Filter IPv4 A records'),
647 _('Remove IPv4 addresses from the results and only return IPv6 addresses.'));
648 o.optional = true;
649
650 s.taboption('filteropts', form.Flag, 'localise_queries',
651 _('Localise queries'),
652 customi18n(_('Limit response records (from {etc_hosts}) to those that fall within the subnet of the querying interface.') ) + '<br />' +
653 _('This prevents unreachable IPs in subnets not accessible to you.') + '<br />' +
654 _('Note: IPv4 only.'));
655
656 if (L.hasSystemFeature('dnsmasq', 'dnssec')) {
657 o = s.taboption('dnssecopt', form.Flag, 'dnssec',
658 _('DNSSEC'),
659 _('Validate DNS replies and cache DNSSEC data, requires upstream to support DNSSEC.'));
660 o.optional = true;
661
662 o = s.taboption('dnssecopt', form.Flag, 'dnsseccheckunsigned',
663 _('DNSSEC check unsigned'),
664 _('Verify unsigned domain responses really come from unsigned domains.'));
665 o.default = o.enabled;
666 o.optional = true;
667 }
668
669 s.taboption('filteropts', form.Flag, 'nonegcache',
670 _('No negative cache'),
671 _('Do not cache negative replies, e.g. for non-existent domains.'));
672
673 o = s.taboption('forward', form.Value, 'serversfile',
674 _('Additional servers file'),
675 customi18n(_('File listing upstream resolvers, optionally domain-specific, e.g. {servers_file_entry01}, {servers_file_entry02}.') )
676 );
677 o.placeholder = '/etc/dnsmasq.servers';
678
679 o = s.taboption('general', form.Flag, 'allservers',
680 _('All servers'),
681 _('Query all available upstream resolvers.') + ' ' + _('First answer wins.'));
682 o.optional = true;
683
684 o = s.taboption('filteropts', form.DynamicList, 'bogusnxdomain',
685 customi18n(_('IPs to override with {nxdomain}') ),
686 customi18n(_('Transform replies which contain the specified addresses or subnets into {nxdomain} responses.') )
687 );
688 o.optional = true;
689 o.placeholder = '64.94.110.11';
690
691 o = s.taboption('devices', form.Value, 'port',
692 _('DNS server port'),
693 _('Listening port for inbound DNS queries.'));
694 o.optional = true;
695 o.datatype = 'port';
696 o.placeholder = 53;
697
698 o = s.taboption('devices', form.Value, 'queryport',
699 _('DNS query port'),
700 _('Fixed source port for outbound DNS queries.'));
701 o.optional = true;
702 o.datatype = 'port';
703 o.placeholder = _('any');
704
705 o = s.taboption('devices', form.Value, 'minport',
706 _('Minimum source port #'),
707 _('Min valid value %s.').format('<code>1024</code>') + ' ' + _('Useful for systems behind firewalls.'));
708 o.optional = true;
709 o.datatype = 'port';
710 o.placeholder = 1024;
711 o.depends('queryport', '');
712
713 o = s.taboption('devices', form.Value, 'maxport',
714 _('Maximum source port #'),
715 _('Max valid value %s.').format('<code>65535</code>') + ' ' + _('Useful for systems behind firewalls.'));
716 o.optional = true;
717 o.datatype = 'port';
718 o.placeholder = 50000;
719 o.depends('queryport', '');
720
721 o = s.taboption('limits', form.Value, 'dhcpleasemax',
722 _('Max. DHCP leases'),
723 _('Maximum allowed number of active DHCP leases.'));
724 o.optional = true;
725 o.datatype = 'uinteger';
726 o.placeholder = 150;
727
728 o = s.taboption('limits', form.Value, 'ednspacket_max',
729 _('Max. EDNS0 packet size'),
730 _('Maximum allowed size of EDNS0 UDP packets.'));
731 o.optional = true;
732 o.datatype = 'uinteger';
733 o.placeholder = 1280;
734
735 o = s.taboption('limits', form.Value, 'dnsforwardmax',
736 _('Max. concurrent queries'),
737 _('Maximum allowed number of concurrent DNS queries.'));
738 o.optional = true;
739 o.datatype = 'uinteger';
740 o.placeholder = 150;
741
742 o = s.taboption('limits', form.Value, 'cachesize',
743 _('Size of DNS query cache'),
744 _('Number of cached DNS entries, 10000 is maximum, 0 is no caching.'));
745 o.optional = true;
746 o.datatype = 'range(0,10000)';
747 o.placeholder = 1000;
748
749 o = s.taboption('limits', form.Value, 'min_cache_ttl',
750 _('Min cache TTL'),
751 _('Extend short TTL values to the seconds value given when caching them. Use with caution.') +
752 _(' (Max 1h == 3600)'));
753 o.optional = true;
754 o.placeholder = 60;
755
756 o = s.taboption('limits', form.Value, 'max_cache_ttl',
757 _('Max cache TTL'),
758 _('Set a maximum seconds TTL value for entries in the cache.'));
759 o.optional = true;
760 o.placeholder = 3600;
761
762 o = s.taboption('pxe_tftp', form.Flag, 'enable_tftp',
763 _('Enable TFTP server'),
764 _('Enable the built-in single-instance TFTP server.'));
765 o.optional = true;
766
767 o = s.taboption('pxe_tftp', form.Value, 'tftp_root',
768 _('TFTP server root'),
769 _('Root directory for files served via TFTP. <em>Enable TFTP server</em> and <em>TFTP server root</em> turn on the TFTP server and serve files from <em>TFTP server root</em>.'));
770 o.depends('enable_tftp', '1');
771 o.optional = true;
772 o.placeholder = '/';
773
774 o = s.taboption('pxe_tftp', form.Value, 'dhcp_boot',
775 _('Network boot image'),
776 _('Filename of the boot image advertised to clients.'));
777 o.depends('enable_tftp', '1');
778 o.optional = true;
779 o.placeholder = 'pxelinux.0';
780
781 /* PXE - https://openwrt.org/docs/guide-user/base-system/dhcp#booting_options */
782 o = s.taboption('pxe_tftp', form.SectionValue, '__pxe__', form.GridSection, 'boot', null,
783 _('Special <abbr title="Preboot eXecution Environment">PXE</abbr> boot options for Dnsmasq.'));
784 ss = o.subsection;
785 ss.addremove = true;
786 ss.anonymous = true;
787 ss.modaltitle = _('Edit PXE/TFTP/BOOTP Host');
788 ss.nodescriptions = true;
789
790 so = ss.option(form.Value, 'filename',
791 _('Filename'),
792 _('Host requests this filename from the boot server.'));
793 so.optional = false;
794 so.placeholder = 'pxelinux.0';
795
796 so = ss.option(form.Value, 'servername',
797 _('Server name'),
798 _('The hostname of the boot server'));
799 so.optional = false;
800 so.placeholder = 'myNAS';
801
802 so = ss.option(form.Value, 'serveraddress',
803 _('Server address'),
804 _('The IP address of the boot server'));
805 so.optional = false;
806 so.placeholder = '192.168.1.2';
807
808 so = ss.option(form.DynamicList, 'dhcp_option',
809 _('DHCP Options'),
810 _('Additional options to send to the below match tags.') + '<br />' +
811 _('%s means "the address of the system running dnsmasq".').format('<code>0.0.0.0</code>'));
812 so.optional = true;
813 so.placeholder = 'option:root-path,192.168.1.2:/data/netboot/root';
814
815 so = ss.option(form.Value, 'networkid',
816 _('Match this Tag'),
817 _('Only DHCP Clients with this tag are sent this boot option.'));
818 so.optional = true;
819 so.noaliases = true;
820
821 so = ss.option(form.Flag, 'force',
822 _('Force'),
823 _('Always send the chosen DHCP options. Sometimes needed, with e.g. PXELinux.'));
824 so.optional = true;
825
826 so = ss.option(form.Value, 'instance',
827 _('Instance'),
828 _('Dnsmasq instance to which this boot section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
829 so.optional = true;
830
831 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
832 var name, display_str = generateDnsmasqInstanceEntry(val);
833 so.value(index, display_str);
834 });
835
836 o = s.taboption('dnsrecords', form.SectionValue, '__dnsrecords__', form.TypedSection, '__dnsrecords__');
837
838 dnss = o.subsection;
839
840 dnss.anonymous = true;
841 dnss.cfgsections = function() { return [ '__dnsrecords__' ] };
842
843 dnss.tab('hosts', _('Hostnames'));
844 dnss.tab('srvhosts', _('SRV'));
845 dnss.tab('mxhosts', _('MX'));
846 dnss.tab('cnamehosts', _('CNAME'));
847
848 o = dnss.taboption('srvhosts', form.SectionValue, '__srvhosts__', form.TableSection, 'srvhost', null,
849 _('Bind service records to a domain name: specify the location of services. See <a href="%s">RFC2782</a>.').format('https://datatracker.ietf.org/doc/html/rfc2782')
850 + '<br />' + _('_service: _sip, _ldap, _imap, _stun, _xmpp-client, … . (Note: while _http is possible, no browsers support SRV records.)')
851 + '<br />' + _('_proto: _tcp, _udp, _sctp, _quic, … .')
852 + '<br />' + _('You may add multiple records for the same Target.')
853 + '<br />' + _('Larger weights (of the same prio) are given a proportionately higher probability of being selected.'));
854
855 ss = o.subsection;
856
857 ss.addremove = true;
858 ss.anonymous = true;
859 ss.sortable = true;
860 ss.rowcolors = true;
861
862 so = ss.option(form.Value, 'srv', _('SRV'), _('Syntax:') + ' ' + '<code>_service._proto.example.com.</code>');
863 so.rmempty = false;
864 so.datatype = 'hostname';
865 so.placeholder = '_sip._tcp.example.com.';
866
867 so = ss.option(form.Value, 'target', _('Target'), _('CNAME or fqdn'));
868 so.rmempty = false;
869 so.datatype = 'hostname';
870 so.placeholder = 'sip.example.com.';
871
872 so = ss.option(form.Value, 'port', _('Port'));
873 so.rmempty = false;
874 so.datatype = 'port';
875 so.placeholder = '5060';
876
877 so = ss.option(form.Value, 'class', _('Priority'), _('Ordinal: lower comes first.'));
878 so.rmempty = true;
879 so.datatype = 'range(0,65535)';
880 so.placeholder = '10';
881
882 so = ss.option(form.Value, 'weight', _('Weight'));
883 so.rmempty = true;
884 so.datatype = 'range(0,65535)';
885 so.placeholder = '50';
886
887 o = dnss.taboption('mxhosts', form.SectionValue, '__mxhosts__', form.TableSection, 'mxhost', null,
888 _('Bind service records to a domain name: specify the location of services.')
889 + '<br />' + _('You may add multiple records for the same domain.'));
890
891 ss = o.subsection;
892
893 ss.addremove = true;
894 ss.anonymous = true;
895 ss.sortable = true;
896 ss.rowcolors = true;
897 ss.nodescriptions = true;
898
899 so = ss.option(form.Value, 'domain', _('Domain'));
900 so.rmempty = false;
901 so.datatype = 'hostname';
902 so.placeholder = 'example.com.';
903
904 so = ss.option(form.Value, 'relay', _('Relay'));
905 so.rmempty = false;
906 so.datatype = 'hostname';
907 so.placeholder = 'relay.example.com.';
908
909 so = ss.option(form.Value, 'pref', _('Priority'), _('Ordinal: lower comes first.'));
910 so.rmempty = true;
911 so.datatype = 'range(0,65535)';
912 so.placeholder = '0';
913
914 o = dnss.taboption('cnamehosts', form.SectionValue, '__cname__', form.TableSection, 'cname', null,
915 _('Set an alias for a hostname.'));
916
917 ss = o.subsection;
918
919 ss.addremove = true;
920 ss.anonymous = true;
921 ss.sortable = true;
922 ss.rowcolors = true;
923 ss.nodescriptions = true;
924
925 so = ss.option(form.Value, 'cname', _('Domain'));
926 so.rmempty = false;
927 so.validate = validateHostname;
928 so.placeholder = 'www.example.com.';
929
930 so = ss.option(form.Value, 'target', _('Target'));
931 so.rmempty = false;
932 so.datatype = 'hostname';
933 so.placeholder = 'example.com.';
934
935 o = dnss.taboption('hosts', form.SectionValue, '__hosts__', form.GridSection, 'domain', null,
936 _('Hostnames are used to bind a domain name to an IP address. This setting is redundant for hostnames already configured with static leases, but it can be useful to rebind an FQDN.'));
937
938 ss = o.subsection;
939
940 ss.addremove = true;
941 ss.anonymous = true;
942 ss.sortable = true;
943
944 so = ss.option(form.Value, 'name', _('Hostname'));
945 so.rmempty = false;
946 so.datatype = 'hostname';
947
948 so = ss.option(form.Value, 'ip', _('IP address'));
949 so.rmempty = false;
950 so.datatype = 'ipaddr';
951
952 var ipaddrs = {};
953
954 Object.keys(hosts).forEach(function(mac) {
955 var addrs = L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4);
956
957 for (var i = 0; i < addrs.length; i++)
958 ipaddrs[addrs[i]] = hosts[mac].name || mac;
959 });
960
961 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
962 so.value(ipv4, '%s (%s)'.format(ipv4, ipaddrs[ipv4]));
963 });
964
965 o = s.taboption('ipsets', form.SectionValue, '__ipsets__', form.GridSection, 'ipset', null,
966 _('List of IP sets to populate with the IPs of DNS lookup results of the FQDNs also specified here.') + '<br />' +
967 _('The netfilter components below are only regarded when running fw4.'));
968
969 ss = o.subsection;
970
971 ss.addremove = true;
972 ss.anonymous = true;
973 ss.sortable = true;
974 ss.rowcolors = true;
975 ss.nodescriptions = true;
976 ss.modaltitle = _('Edit IP set');
977
978 so = ss.option(form.DynamicList, 'name', _('Name of the set'));
979 so.rmempty = false;
980 so.editable = false;
981 so.datatype = 'string';
982
983 so = ss.option(form.DynamicList, 'domain', _('FQDN'));
984 so.rmempty = false;
985 so.editable = false;
986 so.datatype = 'hostname';
987
988 so = ss.option(form.Value, 'table', _('Netfilter table name'), _('Defaults to fw4.'));
989 so.editable = false;
990 so.placeholder = 'fw4';
991 so.rmempty = true;
992
993 so = ss.option(form.ListValue, 'table_family', _('Table IP family'), _('Defaults to IPv4+6.') + ' ' + _('Can be hinted by adding 4 or 6 to the name.') + '<br />' +
994 _('Adding an IPv6 to an IPv4 set and vice-versa silently fails.'));
995 so.editable = false;
996 so.rmempty = true;
997 so.value('inet', _('IPv4+6'));
998 so.value('ip', _('IPv4'));
999 so.value('ip6', _('IPv6'));
1000
1001 o = s.taboption('leases', form.SectionValue, '__leases__', form.GridSection, 'host', null,
1002 _('Static leases are used to assign fixed IP addresses and symbolic hostnames to DHCP clients. They are also required for non-dynamic interface configurations where only hosts with a corresponding lease are served.') + '<br /><br />' +
1003 _('Use the <em>Add</em> Button to add a new lease entry. The <em>MAC address</em> identifies the host, the <em>IPv4 address</em> specifies the fixed address to use, and the <em>Hostname</em> is assigned as a symbolic name to the requesting host. The optional <em>Lease time</em> can be used to set non-standard host-specific lease time, e.g. 12h, 3d or infinite.') + '<br /><br />' +
1004 _('The tag construct filters which host directives are used; more than one tag can be provided, in this case the request must match all of them. Tagged directives are used in preference to untagged ones. Note that one of mac, duid or hostname still needs to be specified (can be a wildcard).'));
1005
1006 ss = o.subsection;
1007
1008 ss.addremove = true;
1009 ss.anonymous = true;
1010 ss.sortable = true;
1011 ss.nodescriptions = true;
1012 ss.max_cols = 8;
1013 ss.modaltitle = _('Edit static lease');
1014
1015 so = ss.option(form.Value, 'name',
1016 _('Hostname'),
1017 _('Optional hostname to assign'));
1018 so.validate = validateHostname;
1019 so.rmempty = true;
1020 so.write = function(section, value) {
1021 uci.set('dhcp', section, 'name', value);
1022 uci.set('dhcp', section, 'dns', '1');
1023 };
1024 so.remove = function(section) {
1025 uci.unset('dhcp', section, 'name');
1026 uci.unset('dhcp', section, 'dns');
1027 };
1028
1029 //this can be a .DynamicList or a .Value with a widget and dnsmasq handles multimac OK.
1030 so = ss.option(form.DynamicList, 'mac',
1031 _('MAC address(es)'),
1032 _('The hardware address(es) of this entry/host.') + '<br /><br />' +
1033 _('In DHCPv4, it is possible to include more than one mac address. This allows an IP address to be associated with multiple macaddrs, and dnsmasq abandons a DHCP lease to one of the macaddrs when another asks for a lease. It only works reliably if only one of the macaddrs is active at any time.'));
1034 //As a special case, in DHCPv4, it is possible to include more than one hardware address. eg: --dhcp-host=11:22:33:44:55:66,12:34:56:78:90:12,192.168.0.2 This allows an IP address to be associated with multiple hardware addresses, and gives dnsmasq permission to abandon a DHCP lease to one of the hardware addresses when another one asks for a lease
1035 so.rmempty = true;
1036 so.cfgvalue = function(section) {
1037 var macs = L.toArray(uci.get('dhcp', section, 'mac'));
1038 return expandAndFormatMAC(macs);
1039 };
1040 //removed jows renderwidget function which hindered multi-mac entry
1041 so.validate = validateMACAddr.bind(so, pools);
1042 Object.keys(hosts).forEach(function(mac) {
1043 var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0];
1044 so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac);
1045 });
1046
1047 so = ss.option(form.Value, 'ip', _('IPv4 address'), _('The IP address to be used for this host, or <em>ignore</em> to ignore any DHCP request from this host.'));
1048 so.value('ignore', _('Ignore'));
1049 so.datatype = 'or(ip4addr,"ignore")';
1050 so.validate = function(section, value) {
1051 var m = this.section.formvalue(section, 'mac'),
1052 n = this.section.formvalue(section, 'name');
1053
1054 if ((m && !m.length > 0) && !n)
1055 return _('One of hostname or MAC address must be specified!');
1056
1057 if (!value || value == 'ignore')
1058 return true;
1059
1060 var leases = uci.sections('dhcp', 'host');
1061
1062 for (var i = 0; i < leases.length; i++)
1063 if (leases[i]['.name'] != section && leases[i].ip == value)
1064 return _('The IP address %h is already used by another static lease').format(value);
1065
1066 for (var i = 0; i < pools.length; i++) {
1067 var net_mask = calculateNetwork(value, pools[i].netmask);
1068
1069 if (net_mask && net_mask[0] == pools[i].network)
1070 return true;
1071 }
1072
1073 return _('The IP address is outside of any DHCP pool address range');
1074 };
1075
1076 L.sortedKeys(ipaddrs, null, 'addr').forEach(function(ipv4) {
1077 so.value(ipv4, ipaddrs[ipv4] ? '%s (%s)'.format(ipv4, ipaddrs[ipv4]) : ipv4);
1078 });
1079
1080 so = ss.option(form.Value, 'leasetime',
1081 _('Lease time'),
1082 _('Host-specific lease time, e.g. <code>5m</code>, <code>3h</code>, <code>7d</code>.'));
1083 so.rmempty = true;
1084 so.value('5m', _('5m (5 minutes)'));
1085 so.value('3h', _('3h (3 hours)'));
1086 so.value('12h', _('12h (12 hours - default)'));
1087 so.value('7d', _('7d (7 days)'));
1088 so.value('infinite', _('infinite (lease does not expire)'));
1089
1090 so = ss.option(form.Value, 'duid',
1091 _('DUID'),
1092 _('The DHCPv6-DUID (DHCP unique identifier) of this host.'));
1093 so.datatype = 'and(rangelength(20,36),hexstring)';
1094 Object.keys(duids).forEach(function(duid) {
1095 so.value(duid, '%s (%s)'.format(duid, duids[duid].hostname || duids[duid].macaddr || duids[duid].ip6addr || '?'));
1096 });
1097
1098 so = ss.option(form.Value, 'hostid',
1099 _('IPv6-Suffix (hex)'),
1100 _('The IPv6 interface identifier (address suffix) as hexadecimal number (max. 16 chars).'));
1101 so.datatype = 'and(rangelength(0,16),hexstring)';
1102
1103 so = ss.option(form.DynamicList, 'tag',
1104 _('Tag'),
1105 _('Assign new, freeform tags to this entry.'));
1106
1107 so = ss.option(form.DynamicList, 'match_tag',
1108 _('Match Tag'),
1109 _('When a host matches an entry then the special tag %s is set. Use %s to match all known hosts.').format('<code>known</code>', '<code>known</code>') + '<br /><br />' +
1110 _('Ignore requests from unknown machines using %s.').format('<code>!known</code>') + '<br /><br />' +
1111 _('If a host matches an entry which cannot be used because it specifies an address on a different subnet, the tag %s is set.').format('<code>known-othernet</code>'));
1112 so.value('known', _('known'));
1113 so.value('!known', _('!known (not known)'));
1114 so.value('known-othernet', _('known-othernet (on different subnet)'));
1115 so.optional = true;
1116
1117 so = ss.option(form.Value, 'instance',
1118 _('Instance'),
1119 _('Dnsmasq instance to which this DHCP host section is bound. If unspecified, the section is valid for all dnsmasq instances.'));
1120 so.optional = true;
1121
1122 Object.values(L.uci.sections('dhcp', 'dnsmasq')).forEach(function(val, index) {
1123 var name, display_str = generateDnsmasqInstanceEntry(val);
1124 so.value(index, display_str);
1125 });
1126
1127
1128 so = ss.option(form.Flag, 'broadcast',
1129 _('Broadcast'),
1130 _('Force broadcast DHCP response.'));
1131
1132 so = ss.option(form.Flag, 'dns',
1133 _('Forward/reverse DNS'),
1134 _('Add static forward and reverse DNS entries for this host.'));
1135
1136 o = s.taboption('leases', CBILeaseStatus, '__status__');
1137
1138 if (has_dhcpv6)
1139 o = s.taboption('leases', CBILease6Status, '__status6__');
1140
1141 return m.render().then(function(mapEl) {
1142 poll.add(function() {
1143 return callDHCPLeases().then(function(leaseinfo) {
1144 var leases = Array.isArray(leaseinfo.dhcp_leases) ? leaseinfo.dhcp_leases : [],
1145 leases6 = Array.isArray(leaseinfo.dhcp6_leases) ? leaseinfo.dhcp6_leases : [];
1146
1147 cbi_update_table(mapEl.querySelector('#lease_status_table'),
1148 leases.map(function(lease) {
1149 var exp;
1150
1151 if (lease.expires === false)
1152 exp = E('em', _('unlimited'));
1153 else if (lease.expires <= 0)
1154 exp = E('em', _('expired'));
1155 else
1156 exp = '%t'.format(lease.expires);
1157
1158 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
1159 name = hint ? hint.name : null,
1160 host = null;
1161
1162 if (name && lease.hostname && lease.hostname != name)
1163 host = '%s (%s)'.format(lease.hostname, name);
1164 else if (lease.hostname)
1165 host = lease.hostname;
1166
1167 return [
1168 host || '-',
1169 lease.ipaddr,
1170 lease.macaddr,
1171 exp
1172 ];
1173 }),
1174 E('em', _('There are no active leases')));
1175
1176 if (has_dhcpv6) {
1177 cbi_update_table(mapEl.querySelector('#lease6_status_table'),
1178 leases6.map(function(lease) {
1179 var exp;
1180
1181 if (lease.expires === false)
1182 exp = E('em', _('unlimited'));
1183 else if (lease.expires <= 0)
1184 exp = E('em', _('expired'));
1185 else
1186 exp = '%t'.format(lease.expires);
1187
1188 var hint = lease.macaddr ? hosts[lease.macaddr] : null,
1189 name = hint ? (hint.name || L.toArray(hint.ipaddrs || hint.ipv4)[0] || L.toArray(hint.ip6addrs || hint.ipv6)[0]) : null,
1190 host = null;
1191
1192 if (name && lease.hostname && lease.hostname != name && lease.ip6addr != name)
1193 host = '%s (%s)'.format(lease.hostname, name);
1194 else if (lease.hostname)
1195 host = lease.hostname;
1196 else if (name)
1197 host = name;
1198
1199 return [
1200 host || '-',
1201 lease.ip6addrs ? lease.ip6addrs.join('<br />') : lease.ip6addr,
1202 lease.duid,
1203 exp
1204 ];
1205 }),
1206 E('em', _('There are no active leases')));
1207 }
1208 });
1209 });
1210
1211 return mapEl;
1212 });
1213 }
1214 });